Skip to content

Commit c8efee1

Browse files
committed
add output format strategy interface
1 parent 373ae50 commit c8efee1

File tree

10 files changed

+304
-18
lines changed

10 files changed

+304
-18
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
AndroidTranscoder
1+
android-transcoder
22
=================
33

44
Hardware accelerated transcoder for Android, written in pure Java.

example/src/main/java/net/ypresto/androidtranscoder/example/TranscoderActivity.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import android.widget.Toast;
1515

1616
import net.ypresto.androidtranscoder.MediaTranscoder;
17+
import net.ypresto.androidtranscoder.format.MediaFormatStrategyPresets;
1718

1819
import java.io.File;
1920
import java.io.FileDescriptor;
@@ -60,7 +61,7 @@ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
6061
final FileDescriptor fileDescriptor = parcelFileDescriptor.getFileDescriptor();
6162
final ProgressBar progressBar = (ProgressBar) findViewById(R.id.progress_bar);
6263
progressBar.setMax(1000);
63-
MediaTranscoder.getInstance().transcodeVideo(fileDescriptor, file.getAbsolutePath(), new MediaTranscoder.Listener() {
64+
MediaTranscoder.Listener listener = new MediaTranscoder.Listener() {
6465
@Override
6566
public void onTranscodeProgress(double progress) {
6667
if (progress < 0) {
@@ -96,7 +97,9 @@ public void onTranscodeFailed(Exception exception) {
9697
Log.w("Error while closing", e);
9798
}
9899
}
99-
});
100+
};
101+
MediaTranscoder.getInstance().transcodeVideo(fileDescriptor, file.getAbsolutePath(),
102+
MediaFormatStrategyPresets.EXPORT_PRESET_960x540, listener);
100103
findViewById(R.id.select_video_button).setEnabled(false);
101104
}
102105
break;

example/src/main/res/values/strings.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<?xml version="1.0" encoding="utf-8"?>
22
<resources>
33

4-
<string name="app_name">AndroidTranscoder</string>
4+
<string name="app_name">android-transcoder</string>
55
<string name="hello_world">Hello world!</string>
66
<string name="action_settings">Settings</string>
77

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/*
2+
* Copyright (C) 2014 Yuya Tanaka
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package net.ypresto.androidtranscoder.format;
17+
18+
import android.media.MediaFormat;
19+
20+
import junit.framework.TestCase;
21+
22+
public class MediaFormatPresetsTest extends TestCase {
23+
24+
public void testGetExportPreset960x540() throws Exception {
25+
assertNull(MediaFormatPresets.getExportPreset960x540(960, 540));
26+
assertNull(MediaFormatPresets.getExportPreset960x540(540, 960));
27+
assertNull(MediaFormatPresets.getExportPreset960x540(480, 320));
28+
assertWidthAndHeightEquals(MediaFormatPresets.getExportPreset960x540(1024, 768), 960, 720);
29+
assertWidthAndHeightEquals(MediaFormatPresets.getExportPreset960x540(768, 1024), 720, 960);
30+
assertWidthAndHeightEquals(MediaFormatPresets.getExportPreset960x540(768, 1024), 720, 960);
31+
assertWidthAndHeightEquals(MediaFormatPresets.getExportPreset960x540(1920, 1080), 960, 540);
32+
assertWidthAndHeightEquals(MediaFormatPresets.getExportPreset960x540(1080, 1920), 540, 960);
33+
assertWidthAndHeightEquals(MediaFormatPresets.getExportPreset960x540(1280, 720), 960, 540);
34+
assertWidthAndHeightEquals(MediaFormatPresets.getExportPreset960x540(720, 1280), 540, 960);
35+
try {
36+
MediaFormatPresets.getExportPreset960x540(1024, 769);
37+
fail("should throw if indivisible");
38+
} catch (IllegalArgumentException e) {
39+
assertTrue(e.getStackTrace()[0].getClassName().equals(MediaFormatPresets.class.getName()));
40+
}
41+
}
42+
43+
private void assertWidthAndHeightEquals(MediaFormat format, int width, int height) throws Exception {
44+
assertEquals(width, format.getInteger(MediaFormat.KEY_WIDTH));
45+
assertEquals(height, format.getInteger(MediaFormat.KEY_HEIGHT));
46+
}
47+
}

lib/src/main/java/net/ypresto/androidtranscoder/MediaTranscoder.java

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,17 @@
1515
*/
1616
package net.ypresto.androidtranscoder;
1717

18+
import android.media.MediaFormat;
1819
import android.os.Handler;
1920
import android.os.Looper;
2021
import android.util.Log;
2122

2223
import net.ypresto.androidtranscoder.engine.MediaTranscoderEngine;
2324
import net.ypresto.androidtranscoder.format.MediaFormatPresets;
25+
import net.ypresto.androidtranscoder.format.MediaFormatStrategy;
2426

2527
import java.io.FileDescriptor;
28+
import java.io.FileInputStream;
2629
import java.io.IOException;
2730
import java.util.concurrent.LinkedBlockingQueue;
2831
import java.util.concurrent.ThreadFactory;
@@ -60,12 +63,93 @@ public static MediaTranscoder getInstance() {
6063

6164
/**
6265
* Transcodes video file asynchronously.
66+
* Audio track will be kept unchanged.
6367
*
6468
* @param inFileDescriptor FileDescriptor for input.
6569
* @param outPath File path for output.
6670
* @param listener Listener instance for callback.
71+
* @deprecated Use {@link #transcodeVideo(java.io.FileDescriptor, String, android.media.MediaFormat, net.ypresto.androidtranscoder.MediaTranscoder.Listener)} which accepts output video format.
6772
*/
73+
@Deprecated
6874
public void transcodeVideo(final FileDescriptor inFileDescriptor, final String outPath, final Listener listener) {
75+
transcodeVideo(inFileDescriptor, outPath, new MediaFormatStrategy() {
76+
@Override
77+
public MediaFormat createVideoOutputFormat(MediaFormat inputFormat) {
78+
return MediaFormatPresets.getExportPreset960x540();
79+
}
80+
81+
@Override
82+
public MediaFormat createAudioOutputFormat(MediaFormat inputFormat) {
83+
return null;
84+
}
85+
}, listener);
86+
}
87+
88+
/**
89+
* Transcodes video file asynchronously.
90+
* Audio track will be kept unchanged.
91+
*
92+
* @param inPath File path for input.
93+
* @param outPath File path for output.
94+
* @param outFormatStrategy Strategy for output video format.
95+
* @param listener Listener instance for callback.
96+
* @throws IOException if input file could not be read.
97+
*/
98+
public void transcodeVideo(final String inPath, final String outPath, final MediaFormatStrategy outFormatStrategy, final Listener listener) throws IOException {
99+
FileInputStream fileInputStream = null;
100+
FileDescriptor inFileDescriptor;
101+
try {
102+
fileInputStream = new FileInputStream(inPath);
103+
inFileDescriptor = fileInputStream.getFD();
104+
} catch (IOException e) {
105+
if (fileInputStream != null) {
106+
try {
107+
fileInputStream.close();
108+
} catch (IOException eClose) {
109+
Log.e(TAG, "Can't close input stream: ", eClose);
110+
}
111+
}
112+
throw e;
113+
}
114+
final FileInputStream finalFileInputStream = fileInputStream;
115+
transcodeVideo(inFileDescriptor, outPath, outFormatStrategy, new Listener() {
116+
@Override
117+
public void onTranscodeProgress(double progress) {
118+
listener.onTranscodeProgress(progress);
119+
}
120+
121+
@Override
122+
public void onTranscodeCompleted() {
123+
listener.onTranscodeCompleted();
124+
closeStream();
125+
}
126+
127+
@Override
128+
public void onTranscodeFailed(Exception exception) {
129+
listener.onTranscodeFailed(exception);
130+
closeStream();
131+
}
132+
133+
private void closeStream() {
134+
try {
135+
finalFileInputStream.close();
136+
} catch (IOException e) {
137+
Log.e(TAG, "Can't close input stream: ", e);
138+
}
139+
}
140+
});
141+
}
142+
143+
/**
144+
* Transcodes video file asynchronously.
145+
* Audio track will be kept unchanged.
146+
*
147+
* @param inFileDescriptor FileDescriptor for input.
148+
* @param outPath File path for output.
149+
* @param outFormatStrategy Strategy for output video format.
150+
* @param listener Listener instance for callback.
151+
*/
152+
public void transcodeVideo(final FileDescriptor inFileDescriptor, final String outPath, final MediaFormatStrategy outFormatStrategy, final Listener listener) {
69153
Looper looper = Looper.myLooper();
70154
if (looper == null) looper = Looper.getMainLooper();
71155
final Handler handler = new Handler(looper);
@@ -87,7 +171,7 @@ public void run() {
87171
}
88172
});
89173
engine.setDataSource(inFileDescriptor);
90-
engine.transcodeVideo(outPath, MediaFormatPresets.getExportPreset960x540());
174+
engine.transcodeVideo(outPath, outFormatStrategy);
91175
} catch (IOException e) {
92176
Log.w(TAG, "Transcode failed: input file (fd: " + inFileDescriptor.toString() + ") not found"
93177
+ " or could not open output file ('" + outPath + "') .", e);

lib/src/main/java/net/ypresto/androidtranscoder/engine/MediaTranscoderEngine.java

Lines changed: 41 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,24 +21,33 @@
2121
import android.media.MediaMuxer;
2222
import android.util.Log;
2323

24+
import net.ypresto.androidtranscoder.format.MediaFormatStrategy;
2425
import net.ypresto.androidtranscoder.utils.MediaExtractorUtils;
2526

2627
import java.io.FileDescriptor;
2728
import java.io.IOException;
2829

30+
/**
31+
* Internal engine, do not use this directly.
32+
*/
2933
// TODO: treat encrypted data
3034
public class MediaTranscoderEngine {
3135
private static final String TAG = "MediaTranscoderEngine";
36+
private static final double PROGRESS_UNKNOWN = -1.0;
3237
private static final long SLEEP_TO_WAIT_TRACK_TRANSCODERS = 10;
3338
private static final long PROGRESS_INTERVAL_STEPS = 10;
3439
private FileDescriptor mInputFileDescriptor;
3540
private TrackTranscoder mVideoTrackTranscoder;
3641
private TrackTranscoder mAudioTrackTranscoder;
3742
private MediaExtractor mExtractor;
3843
private MediaMuxer mMuxer;
44+
private volatile double mProgress;
3945
private ProgressCallback mProgressCallback;
4046
private long mDurationUs;
4147

48+
/**
49+
* Do not use this constructor unless you know what you are doing.
50+
*/
4251
public MediaTranscoderEngine() {
4352
}
4453

@@ -54,15 +63,22 @@ public void setProgressCallback(ProgressCallback progressCallback) {
5463
mProgressCallback = progressCallback;
5564
}
5665

66+
/**
67+
* NOTE: This method is thread safe.
68+
*/
69+
public double getProgress() {
70+
return mProgress;
71+
}
72+
5773
/**
5874
* Run video transcoding. Blocks current thread.
5975
* Audio data will not be transcoded; original stream will be wrote to output file.
6076
*
61-
* @param outputPath File path to output transcoded video file.
62-
* @param videoFormat Output video format.
77+
* @param outputPath File path to output transcoded video file.
78+
* @param formatStrategy Output format strategy.
6379
* @throws IOException when input or output file could not be opened.
6480
*/
65-
public void transcodeVideo(String outputPath, MediaFormat videoFormat) throws IOException {
81+
public void transcodeVideo(String outputPath, MediaFormatStrategy formatStrategy) throws IOException {
6682
if (outputPath == null) {
6783
throw new NullPointerException("Output path cannot be null.");
6884
}
@@ -75,7 +91,7 @@ public void transcodeVideo(String outputPath, MediaFormat videoFormat) throws IO
7591
mExtractor.setDataSource(mInputFileDescriptor);
7692
mMuxer = new MediaMuxer(outputPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
7793
setupMetadata();
78-
setupTrackTranscoders(videoFormat);
94+
setupTrackTranscoders(formatStrategy);
7995
mMuxer.start();
8096
runPipelines();
8197
mMuxer.stop();
@@ -132,11 +148,21 @@ private void setupMetadata() throws IOException {
132148
Log.d(TAG, "Duration (us): " + mDurationUs);
133149
}
134150

135-
private void setupTrackTranscoders(MediaFormat outputFormat) {
151+
private void setupTrackTranscoders(MediaFormatStrategy formatStrategy) {
136152
MediaExtractorUtils.TrackResult trackResult = MediaExtractorUtils.getFirstVideoAndAudioTrack(mExtractor);
137-
mVideoTrackTranscoder = new VideoTrackTranscoder(mExtractor, trackResult.mVideoTrackIndex, outputFormat, mMuxer);
153+
MediaFormat videoOutputFormat = formatStrategy.createVideoOutputFormat(trackResult.mVideoTrackFormat);
154+
MediaFormat audioOutputFormat = formatStrategy.createAudioOutputFormat(trackResult.mAudioTrackFormat);
155+
if (videoOutputFormat == null) {
156+
mVideoTrackTranscoder = new PassThroughTrackTranscoder(mExtractor, trackResult.mVideoTrackIndex, mMuxer);
157+
} else {
158+
mVideoTrackTranscoder = new VideoTrackTranscoder(mExtractor, trackResult.mVideoTrackIndex, videoOutputFormat, mMuxer);
159+
}
138160
mVideoTrackTranscoder.setup();
139-
mAudioTrackTranscoder = new PassThroughTrackTranscoder(mExtractor, trackResult.mAudioTrackIndex, mMuxer);
161+
if (audioOutputFormat == null) {
162+
mAudioTrackTranscoder = new PassThroughTrackTranscoder(mExtractor, trackResult.mAudioTrackIndex, mMuxer);
163+
} else {
164+
throw new UnsupportedOperationException("Transcoding audio tracks currently not supported.");
165+
}
140166
mAudioTrackTranscoder.setup();
141167
mVideoTrackTranscoder.determineFormat();
142168
mAudioTrackTranscoder.determineFormat();
@@ -148,17 +174,21 @@ private void setupTrackTranscoders(MediaFormat outputFormat) {
148174

149175
private void runPipelines() {
150176
long loopCount = 0;
151-
if (mDurationUs <= 0 && mProgressCallback != null) {
152-
mProgressCallback.onProgress(-1.0); // unknown
177+
if (mDurationUs <= 0) {
178+
double progress = PROGRESS_UNKNOWN;
179+
mProgress = progress;
180+
if (mProgressCallback != null) mProgressCallback.onProgress(progress); // unknown
153181
}
154182
while (!(mVideoTrackTranscoder.isFinished() && mAudioTrackTranscoder.isFinished())) {
155183
boolean stepped = mVideoTrackTranscoder.stepPipeline()
156184
|| mAudioTrackTranscoder.stepPipeline();
157185
loopCount++;
158-
if (mDurationUs > 0 && mProgressCallback != null && loopCount % PROGRESS_INTERVAL_STEPS == 0) {
186+
if (mDurationUs > 0 && loopCount % PROGRESS_INTERVAL_STEPS == 0) {
159187
double videoProgress = mVideoTrackTranscoder.isFinished() ? 1.0 : Math.min(1.0, (double) mVideoTrackTranscoder.getWrittenPresentationTimeUs() / mDurationUs);
160188
double audioProgress = mAudioTrackTranscoder.isFinished() ? 1.0 : Math.min(1.0, (double) mAudioTrackTranscoder.getWrittenPresentationTimeUs() / mDurationUs);
161-
mProgressCallback.onProgress((videoProgress + audioProgress) / 2.0);
189+
double progress = (videoProgress + audioProgress) / 2.0;
190+
mProgress = progress;
191+
if (mProgressCallback != null) mProgressCallback.onProgress(progress);
162192
}
163193
if (!stepped) {
164194
try {

lib/src/main/java/net/ypresto/androidtranscoder/format/MediaFormatPresets.java

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,54 @@
2222
// Refer for preferred parameters: https://developer.apple.com/library/ios/documentation/networkinginternet/conceptual/streamingmediaguide/UsingHTTPLiveStreaming/UsingHTTPLiveStreaming.html#//apple_ref/doc/uid/TP40008332-CH102-SW8
2323
// Refer for available keys: (ANDROID ROOT)/media/libstagefright/ACodec.cpp
2424
public class MediaFormatPresets {
25+
private static final int LONGER_LENGTH_960x540 = 960;
2526

2627
private MediaFormatPresets() {
2728
}
2829

2930
// preset similar to iOS SDK's AVAssetExportPreset960x540
31+
@Deprecated
3032
public static MediaFormat getExportPreset960x540() {
3133
MediaFormat format = MediaFormat.createVideoFormat("video/avc", 960, 540);
32-
format.setInteger(MediaFormat.KEY_BIT_RATE, 5400 * 1000);
34+
format.setInteger(MediaFormat.KEY_BIT_RATE, 5500 * 1000);
35+
format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
36+
format.setInteger(MediaFormat.KEY_FRAME_RATE, 30);
37+
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);
38+
return format;
39+
}
40+
41+
/**
42+
* Preset similar to iOS SDK's AVAssetExportPreset960x540
43+
* @param originalWidth Input video width.
44+
* @param originalHeight Input video height.
45+
* @return MediaFormat instance, or null if pass through.
46+
*/
47+
public static MediaFormat getExportPreset960x540(int originalWidth, int originalHeight) {
48+
int longerLength = Math.max(originalWidth, originalHeight);
49+
int shorterLength = Math.min(originalWidth, originalHeight);
50+
51+
if (longerLength <= LONGER_LENGTH_960x540) return null; // don't upscale
52+
53+
int residue = LONGER_LENGTH_960x540 * shorterLength % longerLength;
54+
if (residue != 0) {
55+
double ambiguousShorter = (double) LONGER_LENGTH_960x540 * shorterLength / longerLength;
56+
throw new IllegalArgumentException(String.format(
57+
"Could not fit to integer, original: (%d, %d), scaled: (%d, %f)",
58+
longerLength, shorterLength, LONGER_LENGTH_960x540, ambiguousShorter));
59+
}
60+
61+
int scaledShorter = LONGER_LENGTH_960x540 * shorterLength / longerLength;
62+
int width, height;
63+
if (originalWidth >= originalHeight) {
64+
width = LONGER_LENGTH_960x540;
65+
height = scaledShorter;
66+
} else {
67+
width = scaledShorter;
68+
height = LONGER_LENGTH_960x540;
69+
}
70+
71+
MediaFormat format = MediaFormat.createVideoFormat("video/avc", width, height);
72+
format.setInteger(MediaFormat.KEY_BIT_RATE, 5500 * 1000);
3373
format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
3474
format.setInteger(MediaFormat.KEY_FRAME_RATE, 30);
3575
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);

0 commit comments

Comments
 (0)