Skip to content

Commit 48afb1f

Browse files
committed
Restrict output format, abort if no transcoding necessary
Fixes ypresto#6
1 parent 4a081c2 commit 48afb1f

10 files changed

+174
-11
lines changed

lib/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,5 +40,6 @@ publish {
4040
}
4141

4242
dependencies {
43+
compile 'org.jcodec:jcodec:0.1.9'
4344
compile fileTree(dir: 'libs', include: ['*.jar'])
4445
}

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ public static MediaTranscoder getInstance() {
6868
* @param inFileDescriptor FileDescriptor for input.
6969
* @param outPath File path for output.
7070
* @param listener Listener instance for callback.
71-
* @deprecated Use {@link #transcodeVideo(java.io.FileDescriptor, String, net.ypresto.androidtranscoder.format.MediaFormatStrategy, net.ypresto.androidtranscoder.MediaTranscoder.Listener)} which accepts output video format.
71+
* @deprecated Use {@link #transcodeVideo(FileDescriptor, String, MediaFormatStrategy, MediaTranscoder.Listener)} which accepts output video format.
7272
*/
7373
@Deprecated
7474
public void transcodeVideo(final FileDescriptor inFileDescriptor, final String outPath, final Listener listener) {
@@ -177,7 +177,7 @@ public void run() {
177177
+ " or could not open output file ('" + outPath + "') .", e);
178178
caughtException = e;
179179
} catch (RuntimeException e) {
180-
Log.e(TAG, "Fatal error while transcoding, this might be invalid output format or bug in engine or Android.", e);
180+
Log.e(TAG, "Fatal error while transcoding, this might be invalid format or bug in engine or Android.", e);
181181
caughtException = e;
182182
}
183183

@@ -212,8 +212,8 @@ public interface Listener {
212212
/**
213213
* Called when transcode failed.
214214
*
215-
* @param exception Exception caused failure. Note that it IS NOT {@link java.lang.Throwable}.
216-
* This means {@link java.lang.Error} won't be caught.
215+
* @param exception Exception thrown from {@link MediaTranscoderEngine#transcodeVideo(String, MediaFormatStrategy)}.
216+
* Note that it IS NOT {@link java.lang.Throwable}. This means {@link java.lang.Error} won't be caught.
217217
*/
218218
void onTranscodeFailed(Exception exception);
219219
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/*
2+
* Copyright (C) 2015 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.engine;
17+
18+
public class InvalidOutputFormatException extends RuntimeException {
19+
public InvalidOutputFormatException(String detailMessage) {
20+
super(detailMessage);
21+
}
22+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*
2+
* Copyright (C) 2015 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.engine;
17+
18+
import android.media.MediaFormat;
19+
20+
import net.ypresto.androidtranscoder.format.MediaFormatExtraConstants;
21+
import net.ypresto.androidtranscoder.utils.AvcCsdUtils;
22+
23+
import org.jcodec.codecs.h264.H264Utils;
24+
import org.jcodec.codecs.h264.io.model.SeqParameterSet;
25+
26+
import java.nio.ByteBuffer;
27+
28+
class MediaFormatValidator {
29+
// Refer: http://en.wikipedia.org/wiki/H.264/MPEG-4_AVC#Profiles
30+
private static final int PROFILE_IDC_BASELINE = 66;
31+
32+
public static void validateVideoOutputFormat(MediaFormat format) {
33+
String mime = format.getString(MediaFormat.KEY_MIME);
34+
// Refer: http://developer.android.com/guide/appendix/media-formats.html#core
35+
// Refer: http://en.wikipedia.org/wiki/MPEG-4_Part_14#Data_streams
36+
if (!MediaFormatExtraConstants.MIMETYPE_VIDEO_AVC.equals(mime)) {
37+
throw new InvalidOutputFormatException("Video codecs other than AVC is not supported, actual mime type: " + mime);
38+
}
39+
ByteBuffer spsBuffer = AvcCsdUtils.getSpsBuffer(format);
40+
SeqParameterSet sps = H264Utils.readSPS(spsBuffer);
41+
if (sps.profile_idc != PROFILE_IDC_BASELINE) {
42+
throw new InvalidOutputFormatException("Non-baseline AVC video profile is not supported by Android OS, actual profile_idc: " + sps.profile_idc);
43+
}
44+
}
45+
46+
public static void validateAudioOutputFormat(MediaFormat format) {
47+
String mime = format.getString(MediaFormat.KEY_MIME);
48+
if (!MediaFormatExtraConstants.MIMETYPE_AUDIO_AAC.equals(mime)) {
49+
throw new InvalidOutputFormatException("Audio codecs other than AAC is not supported, actual mime type: " + mime);
50+
}
51+
}
52+
}

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ public double getProgress() {
7777
* @param outputPath File path to output transcoded video file.
7878
* @param formatStrategy Output format strategy.
7979
* @throws IOException when input or output file could not be opened.
80+
* @throws InvalidOutputFormatException when output format is not supported.
8081
*/
8182
public void transcodeVideo(String outputPath, MediaFormatStrategy formatStrategy) throws IOException {
8283
if (outputPath == null) {
@@ -152,6 +153,10 @@ private void setupTrackTranscoders(MediaFormatStrategy formatStrategy) {
152153
MediaExtractorUtils.TrackResult trackResult = MediaExtractorUtils.getFirstVideoAndAudioTrack(mExtractor);
153154
MediaFormat videoOutputFormat = formatStrategy.createVideoOutputFormat(trackResult.mVideoTrackFormat);
154155
MediaFormat audioOutputFormat = formatStrategy.createAudioOutputFormat(trackResult.mAudioTrackFormat);
156+
if (videoOutputFormat == null && audioOutputFormat == null) {
157+
throw new InvalidOutputFormatException("MediaFormatStrategy returned pass-through for both video and audio. No transcoding is necessary.");
158+
}
159+
155160
if (videoOutputFormat == null) {
156161
mVideoTrackTranscoder = new PassThroughTrackTranscoder(mExtractor, trackResult.mVideoTrackIndex, mMuxer);
157162
} else {
@@ -166,6 +171,8 @@ private void setupTrackTranscoders(MediaFormatStrategy formatStrategy) {
166171
mAudioTrackTranscoder.setup();
167172
mVideoTrackTranscoder.determineFormat();
168173
mAudioTrackTranscoder.determineFormat();
174+
MediaFormatValidator.validateVideoOutputFormat(mVideoTrackTranscoder.getDeterminedFormat());
175+
MediaFormatValidator.validateAudioOutputFormat(mAudioTrackTranscoder.getDeterminedFormat());
169176
mVideoTrackTranscoder.addTrackToMuxer();
170177
mAudioTrackTranscoder.addTrackToMuxer();
171178
mExtractor.selectTrack(trackResult.mVideoTrackIndex);

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
import android.media.MediaMuxer;
2222
import android.util.Log;
2323

24+
import net.ypresto.androidtranscoder.format.MediaFormatExtraConstants;
25+
2426
import java.io.IOException;
2527
import java.nio.ByteBuffer;
2628

@@ -77,11 +79,11 @@ public void setup() {
7779
mEncoderOutputBuffers = mEncoder.getOutputBuffers();
7880

7981
MediaFormat inputFormat = mExtractor.getTrackFormat(mTrackIndex);
80-
if (inputFormat.containsKey("rotation-degrees")) {
82+
if (inputFormat.containsKey(MediaFormatExtraConstants.KEY_ROTATION_DEGREES)) {
8183
// Decoded video is rotated automatically in Android 5.0 lollipop.
8284
// Turn off here because we don't want to encode rotated one.
8385
// refer: https://android.googlesource.com/platform/frameworks/av/+blame/lollipop-release/media/libstagefright/Utils.cpp
84-
inputFormat.setInteger("rotation-degrees", 0);
86+
inputFormat.setInteger(MediaFormatExtraConstants.KEY_ROTATION_DEGREES, 0);
8587
}
8688
mDecoderOutputSurfaceWrapper = new OutputSurface();
8789
try {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
import android.util.Log;
2121

2222
class Android720pFormatStrategy implements MediaFormatStrategy {
23-
private static final String TAG = "Android720pFormatStrategy";
23+
private static final String TAG = "720pFormatStrategy";
2424
private static final int LONGER_LENGTH = 1280;
2525
private static final int SHORTER_LENGTH = 720;
2626
private static final int DEFAULT_BITRATE = 8000 * 1000; // From Nexus 4 Camera in 720p

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

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,41 @@
1616
package net.ypresto.androidtranscoder.format;
1717

1818
public class MediaFormatExtraConstants {
19-
// from API level >= 21, but might be usable in older APIs as native code implementation exists.
20-
public static final String KEY_PROFILE = "profile"; // MediaCodecInfo.CodecProfileLevel
21-
// from (ANDROID ROOT)/media/libstagefright/ACodec.cpp
22-
public static final String KEY_LEVEL = "level"; // MediaCodecInfo.CodecProfileLevel
19+
// from MediaFormat of API level >= 21, but might be usable in older APIs as native code implementation exists.
20+
// https://android.googlesource.com/platform/frameworks/av/+/lollipop-release/media/libstagefright/ACodec.cpp#2621
21+
// NOTE: native code enforces baseline profile.
22+
// https://android.googlesource.com/platform/frameworks/av/+/lollipop-release/media/libstagefright/ACodec.cpp#2638
23+
/** For encoder parameter. Use value of MediaCodecInfo.CodecProfileLevel.AVCProfile* . */
24+
public static final String KEY_PROFILE = "profile";
25+
26+
// from https://android.googlesource.com/platform/frameworks/av/+/lollipop-release/media/libstagefright/ACodec.cpp#2623
27+
/** For encoder parameter. Use value of MediaCodecInfo.CodecProfileLevel.AVCLevel* . */
28+
public static final String KEY_LEVEL = "level";
29+
30+
// from https://android.googlesource.com/platform/frameworks/av/+/lollipop-release/media/libstagefright/MediaCodec.cpp#2197
31+
/** Included in MediaFormat from {@link android.media.MediaExtractor#getTrackFormat(int)}. Value is {@link java.nio.ByteBuffer}. */
32+
public static final String KEY_AVC_SPS = "csd-0";
33+
/** Included in MediaFormat from {@link android.media.MediaExtractor#getTrackFormat(int)}. Value is {@link java.nio.ByteBuffer}. */
34+
public static final String KEY_AVC_PPS = "csd-1";
35+
36+
/**
37+
* For decoder parameter and included in MediaFormat from {@link android.media.MediaExtractor#getTrackFormat(int)}.
38+
* Decoder rotates specified degrees before rendering video to surface.
39+
* NOTE: Only included in track format of API >= 21.
40+
*/
41+
public static final String KEY_ROTATION_DEGREES = "rotation-degrees";
42+
43+
// Video formats
44+
// from MediaFormat of API level >= 21
45+
public static final String MIMETYPE_VIDEO_AVC = "video/avc";
46+
public static final String MIMETYPE_VIDEO_H263 = "video/3gpp";
47+
public static final String MIMETYPE_VIDEO_VP8 = "video/x-vnd.on2.vp8";
48+
49+
// Audio formats
50+
// from MediaFormat of API level >= 21
51+
public static final String MIMETYPE_AUDIO_AAC = "audio/mp4a-latm";
52+
2353
private MediaFormatExtraConstants() {
54+
throw new RuntimeException();
2455
}
2556
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ public class MediaFormatStrategyPresets {
2525
/**
2626
* Preset based on Nexus 4 camera recording with 720p quality.
2727
* This preset is ensured to work on any Android >=4.3 devices by Android CTS (if codec is available).
28+
* Default bitrate is 8Mbps. {@link #createAndroid720pStrategy(int)} to specify bitrate.
2829
*/
2930
public static MediaFormatStrategy createAndroid720pStrategy() {
3031
return new Android720pFormatStrategy();
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/*
2+
* Copyright (C) 2015 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.utils;
17+
18+
import android.media.MediaFormat;
19+
20+
import net.ypresto.androidtranscoder.format.MediaFormatExtraConstants;
21+
22+
import java.nio.ByteBuffer;
23+
import java.util.Arrays;
24+
25+
public class AvcCsdUtils {
26+
// Refer: https://android.googlesource.com/platform/frameworks/av/+/lollipop-release/media/libstagefright/MediaCodec.cpp#2198
27+
private static final byte[] AVC_CSD_PREFIX = {0x00, 0x00, 0x00, 0x01};
28+
// Refer: http://www.cardinalpeak.com/blog/the-h-264-sequence-parameter-set/
29+
private static final byte AVC_SPS_NAL = 103; // 0<<7 + 3<<5 + 7<<0
30+
31+
public static ByteBuffer getSpsBuffer(MediaFormat format) {
32+
ByteBuffer prefixedSpsBuffer = format.getByteBuffer(MediaFormatExtraConstants.KEY_AVC_SPS).asReadOnlyBuffer();
33+
byte[] csdPrefix = new byte[4];
34+
prefixedSpsBuffer.get(csdPrefix);
35+
if (!Arrays.equals(csdPrefix, AVC_CSD_PREFIX)) {
36+
throw new IllegalStateException("Wrong csd-0 prefix.");
37+
}
38+
if (prefixedSpsBuffer.get() != AVC_SPS_NAL) {
39+
throw new IllegalStateException("Got non SPS NAL data.");
40+
}
41+
return prefixedSpsBuffer.slice();
42+
}
43+
44+
private AvcCsdUtils() {
45+
throw new RuntimeException();
46+
}
47+
}

0 commit comments

Comments
 (0)