diff --git a/example/src/main/java/net/ypresto/androidtranscoder/example/TranscoderActivity.java b/example/src/main/java/net/ypresto/androidtranscoder/example/TranscoderActivity.java index ae29ea26..0df6c1e0 100644 --- a/example/src/main/java/net/ypresto/androidtranscoder/example/TranscoderActivity.java +++ b/example/src/main/java/net/ypresto/androidtranscoder/example/TranscoderActivity.java @@ -15,6 +15,7 @@ import android.widget.Toast; import net.ypresto.androidtranscoder.MediaTranscoder; +import net.ypresto.androidtranscoder.engine.MediaTrimTime; import net.ypresto.androidtranscoder.format.MediaFormatStrategyPresets; import java.io.File; @@ -104,7 +105,7 @@ public void onTranscodeFailed(Exception exception) { }; Log.d(TAG, "transcoding into " + file); mFuture = MediaTranscoder.getInstance().transcodeVideo(fileDescriptor, file.getAbsolutePath(), - MediaFormatStrategyPresets.createAndroid720pStrategy(), listener); + MediaFormatStrategyPresets.createAndroid720pStrategy(), new MediaTrimTime(5 * 1000000, 10 * 1000000), listener); switchButtonEnabled(true); } break; diff --git a/lib/src/main/java/net/ypresto/androidtranscoder/MediaTranscoder.java b/lib/src/main/java/net/ypresto/androidtranscoder/MediaTranscoder.java index 3a758677..230a1740 100644 --- a/lib/src/main/java/net/ypresto/androidtranscoder/MediaTranscoder.java +++ b/lib/src/main/java/net/ypresto/androidtranscoder/MediaTranscoder.java @@ -21,6 +21,7 @@ import android.util.Log; import net.ypresto.androidtranscoder.engine.MediaTranscoderEngine; +import net.ypresto.androidtranscoder.engine.MediaTrimTime; import net.ypresto.androidtranscoder.format.MediaFormatPresets; import net.ypresto.androidtranscoder.format.MediaFormatStrategy; @@ -85,7 +86,7 @@ public MediaFormat createVideoOutputFormat(MediaFormat inputFormat) { public MediaFormat createAudioOutputFormat(MediaFormat inputFormat) { return null; } - }, listener); + }, null, listener); } /** @@ -115,7 +116,7 @@ public Future transcodeVideo(final String inPath, final String outPath, fi throw e; } final FileInputStream finalFileInputStream = fileInputStream; - return transcodeVideo(inFileDescriptor, outPath, outFormatStrategy, new Listener() { + return transcodeVideo(inFileDescriptor, outPath, outFormatStrategy, null, new Listener() { @Override public void onTranscodeProgress(double progress) { listener.onTranscodeProgress(progress); @@ -156,9 +157,10 @@ private void closeStream() { * @param inFileDescriptor FileDescriptor for input. * @param outPath File path for output. * @param outFormatStrategy Strategy for output video format. + * @param mediaTrimTime Media trim time. * @param listener Listener instance for callback. */ - public Future transcodeVideo(final FileDescriptor inFileDescriptor, final String outPath, final MediaFormatStrategy outFormatStrategy, final Listener listener) { + public Future transcodeVideo(final FileDescriptor inFileDescriptor, final String outPath, final MediaFormatStrategy outFormatStrategy, final MediaTrimTime mediaTrimTime, final Listener listener) { Looper looper = Looper.myLooper(); if (looper == null) looper = Looper.getMainLooper(); final Handler handler = new Handler(looper); @@ -181,7 +183,7 @@ public void run() { } }); engine.setDataSource(inFileDescriptor); - engine.transcodeVideo(outPath, outFormatStrategy); + engine.transcodeVideo(outPath, outFormatStrategy, mediaTrimTime); } catch (IOException e) { Log.w(TAG, "Transcode failed: input file (fd: " + inFileDescriptor.toString() + ") not found" + " or could not open output file ('" + outPath + "') .", e); diff --git a/lib/src/main/java/net/ypresto/androidtranscoder/engine/MediaTranscoderEngine.java b/lib/src/main/java/net/ypresto/androidtranscoder/engine/MediaTranscoderEngine.java index ad219ca0..9acad5c6 100644 --- a/lib/src/main/java/net/ypresto/androidtranscoder/engine/MediaTranscoderEngine.java +++ b/lib/src/main/java/net/ypresto/androidtranscoder/engine/MediaTranscoderEngine.java @@ -44,6 +44,7 @@ public class MediaTranscoderEngine { private volatile double mProgress; private ProgressCallback mProgressCallback; private long mDurationUs; + private MediaTrimTime mMediaTrimTime; /** * Do not use this constructor unless you know what you are doing. @@ -81,6 +82,21 @@ public double getProgress() { * @throws InterruptedException when cancel to transcode. */ public void transcodeVideo(String outputPath, MediaFormatStrategy formatStrategy) throws IOException, InterruptedException { + transcodeVideo(outputPath, formatStrategy, null); + } + + /** + * Run video transcoding. Blocks current thread. + * Audio data will not be transcoded; original stream will be wrote to output file. + * + * @param outputPath File path to output transcoded video file. + * @param formatStrategy Output format strategy. + * @param mediaTrimTime Media trim time. + * @throws IOException when input or output file could not be opened. + * @throws InvalidOutputFormatException when output format is not supported. + * @throws InterruptedException when cancel to transcode. + */ + public void transcodeVideo(String outputPath, MediaFormatStrategy formatStrategy, MediaTrimTime mediaTrimTime) throws IOException, InterruptedException { if (outputPath == null) { throw new NullPointerException("Output path cannot be null."); } @@ -92,6 +108,7 @@ public void transcodeVideo(String outputPath, MediaFormatStrategy formatStrategy mExtractor = new MediaExtractor(); mExtractor.setDataSource(mInputFileDescriptor); mMuxer = new MediaMuxer(outputPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4); + mMediaTrimTime = mediaTrimTime; setupMetadata(); setupTrackTranscoders(formatStrategy); runPipelines(); @@ -146,6 +163,14 @@ private void setupMetadata() throws IOException { } catch (NumberFormatException e) { mDurationUs = -1; } + + if (mMediaTrimTime != null && mMediaTrimTime.endTimeInUs > 0 && mMediaTrimTime.endTimeInUs < mDurationUs) { + mDurationUs = mMediaTrimTime.endTimeInUs; + } + + if (mMediaTrimTime != null && mMediaTrimTime.startTimeInUs > 0 && mMediaTrimTime.startTimeInUs < mDurationUs ) { + mDurationUs -= mMediaTrimTime.startTimeInUs; + } Log.d(TAG, "Duration (us): " + mDurationUs); } @@ -165,19 +190,23 @@ public void onDetermineOutputFormat() { }); if (videoOutputFormat == null) { - mVideoTrackTranscoder = new PassThroughTrackTranscoder(mExtractor, trackResult.mVideoTrackIndex, queuedMuxer, QueuedMuxer.SampleType.VIDEO); + mVideoTrackTranscoder = new PassThroughTrackTranscoder(mExtractor, trackResult.mVideoTrackIndex, queuedMuxer, QueuedMuxer.SampleType.VIDEO, mMediaTrimTime); } else { - mVideoTrackTranscoder = new VideoTrackTranscoder(mExtractor, trackResult.mVideoTrackIndex, videoOutputFormat, queuedMuxer); + mVideoTrackTranscoder = new VideoTrackTranscoder(mExtractor, trackResult.mVideoTrackIndex, videoOutputFormat, queuedMuxer, mMediaTrimTime); } mVideoTrackTranscoder.setup(); if (audioOutputFormat == null) { - mAudioTrackTranscoder = new PassThroughTrackTranscoder(mExtractor, trackResult.mAudioTrackIndex, queuedMuxer, QueuedMuxer.SampleType.AUDIO); + mAudioTrackTranscoder = new PassThroughTrackTranscoder(mExtractor, trackResult.mAudioTrackIndex, queuedMuxer, QueuedMuxer.SampleType.AUDIO, mMediaTrimTime); } else { throw new UnsupportedOperationException("Transcoding audio tracks currently not supported."); } mAudioTrackTranscoder.setup(); mExtractor.selectTrack(trackResult.mVideoTrackIndex); mExtractor.selectTrack(trackResult.mAudioTrackIndex); + + if (mMediaTrimTime != null && mMediaTrimTime.startTimeInUs > 0) { + mExtractor.seekTo(mMediaTrimTime.startTimeInUs, MediaExtractor.SEEK_TO_CLOSEST_SYNC); + } } private void runPipelines() { diff --git a/lib/src/main/java/net/ypresto/androidtranscoder/engine/MediaTrimTime.java b/lib/src/main/java/net/ypresto/androidtranscoder/engine/MediaTrimTime.java new file mode 100644 index 00000000..4806b3d4 --- /dev/null +++ b/lib/src/main/java/net/ypresto/androidtranscoder/engine/MediaTrimTime.java @@ -0,0 +1,23 @@ +package net.ypresto.androidtranscoder.engine; + +/** + * Created by Taishan Lin on 1/28/16. + */ +public class MediaTrimTime { + public static final long DO_NOT_TRIM_TIME = -1; + public final long startTimeInUs; + public final long endTimeInUs; + + public MediaTrimTime() { + this.startTimeInUs = DO_NOT_TRIM_TIME; + this.endTimeInUs = DO_NOT_TRIM_TIME; + } + + public MediaTrimTime(long startTimeInUs, long endTimeInUs) { + this.startTimeInUs = startTimeInUs; + this.endTimeInUs = endTimeInUs; + if (endTimeInUs != DO_NOT_TRIM_TIME && startTimeInUs >= endTimeInUs) { + throw new IllegalArgumentException("Start time is larger than or equal to end time!"); + } + } +} diff --git a/lib/src/main/java/net/ypresto/androidtranscoder/engine/PassThroughTrackTranscoder.java b/lib/src/main/java/net/ypresto/androidtranscoder/engine/PassThroughTrackTranscoder.java index 7608dac8..6ae77bb0 100644 --- a/lib/src/main/java/net/ypresto/androidtranscoder/engine/PassThroughTrackTranscoder.java +++ b/lib/src/main/java/net/ypresto/androidtranscoder/engine/PassThroughTrackTranscoder.java @@ -29,14 +29,16 @@ public class PassThroughTrackTranscoder implements TrackTranscoder { private final QueuedMuxer mMuxer; private final QueuedMuxer.SampleType mSampleType; private final MediaCodec.BufferInfo mBufferInfo = new MediaCodec.BufferInfo(); + private final MediaTrimTime mMediaTrimTime; private int mBufferSize; private ByteBuffer mBuffer; private boolean mIsEOS; + private boolean mIsTrimEOS; private MediaFormat mActualOutputFormat; private long mWrittenPresentationTimeUs; public PassThroughTrackTranscoder(MediaExtractor extractor, int trackIndex, - QueuedMuxer muxer, QueuedMuxer.SampleType sampleType) { + QueuedMuxer muxer, QueuedMuxer.SampleType sampleType, MediaTrimTime mediaTrimTime) { mExtractor = extractor; mTrackIndex = trackIndex; mMuxer = muxer; @@ -46,6 +48,7 @@ public PassThroughTrackTranscoder(MediaExtractor extractor, int trackIndex, mMuxer.setOutputFormat(mSampleType, mActualOutputFormat); mBufferSize = mActualOutputFormat.getInteger(MediaFormat.KEY_MAX_INPUT_SIZE); mBuffer = ByteBuffer.allocateDirect(mBufferSize).order(ByteOrder.nativeOrder()); + mMediaTrimTime = mediaTrimTime; } @Override @@ -71,9 +74,21 @@ public boolean stepPipeline() { } if (trackIndex != mTrackIndex) return false; + if (mIsTrimEOS) { + mExtractor.advance(); + return true; + } mBuffer.clear(); int sampleSize = mExtractor.readSampleData(mBuffer, 0); assert sampleSize <= mBufferSize; + if (isPastTrimEndTime(mExtractor.getSampleTime())) { + mBuffer.clear(); + mBufferInfo.set(0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM); + mMuxer.writeSampleData(mSampleType, mBuffer, mBufferInfo); + mExtractor.advance(); + mIsTrimEOS = true; + return true; + } boolean isKeyFrame = (mExtractor.getSampleFlags() & MediaExtractor.SAMPLE_FLAG_SYNC) != 0; int flags = isKeyFrame ? MediaCodec.BUFFER_FLAG_SYNC_FRAME : 0; mBufferInfo.set(0, sampleSize, mExtractor.getSampleTime(), flags); @@ -91,7 +106,11 @@ public long getWrittenPresentationTimeUs() { @Override public boolean isFinished() { - return mIsEOS; + return mIsEOS || mIsTrimEOS; + } + + private boolean isPastTrimEndTime(long sampleTime) { + return mMediaTrimTime != null && mMediaTrimTime.endTimeInUs > 0 && sampleTime > mMediaTrimTime.endTimeInUs; } @Override diff --git a/lib/src/main/java/net/ypresto/androidtranscoder/engine/VideoTrackTranscoder.java b/lib/src/main/java/net/ypresto/androidtranscoder/engine/VideoTrackTranscoder.java index a5640dd9..ac76c0cf 100644 --- a/lib/src/main/java/net/ypresto/androidtranscoder/engine/VideoTrackTranscoder.java +++ b/lib/src/main/java/net/ypresto/androidtranscoder/engine/VideoTrackTranscoder.java @@ -32,6 +32,7 @@ public class VideoTrackTranscoder implements TrackTranscoder { private static final int DRAIN_STATE_CONSUMED = 2; private final MediaExtractor mExtractor; + private final MediaTrimTime mMediaTrimTime; private final int mTrackIndex; private final MediaFormat mOutputFormat; private final QueuedMuxer mMuxer; @@ -44,6 +45,7 @@ public class VideoTrackTranscoder implements TrackTranscoder { private OutputSurface mDecoderOutputSurfaceWrapper; private InputSurface mEncoderInputSurfaceWrapper; private boolean mIsExtractorEOS; + private boolean mIsTrimEOS; private boolean mIsDecoderEOS; private boolean mIsEncoderEOS; private boolean mDecoderStarted; @@ -51,11 +53,12 @@ public class VideoTrackTranscoder implements TrackTranscoder { private long mWrittenPresentationTimeUs; public VideoTrackTranscoder(MediaExtractor extractor, int trackIndex, - MediaFormat outputFormat, QueuedMuxer muxer) { + MediaFormat outputFormat, QueuedMuxer muxer, MediaTrimTime mediaTrimTime) { mExtractor = extractor; mTrackIndex = trackIndex; mOutputFormat = outputFormat; mMuxer = muxer; + mMediaTrimTime = mediaTrimTime; } @Override @@ -120,7 +123,7 @@ public long getWrittenPresentationTimeUs() { @Override public boolean isFinished() { - return mIsEncoderEOS; + return mIsEncoderEOS || mIsTrimEOS; } // TODO: CloseGuard @@ -159,7 +162,17 @@ private int drainExtractor(long timeoutUs) { mDecoder.queueInputBuffer(result, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM); return DRAIN_STATE_NONE; } + if (mIsTrimEOS) { + mExtractor.advance(); + return DRAIN_STATE_NONE; + } int sampleSize = mExtractor.readSampleData(mDecoderInputBuffers[result], 0); + if (isPastTrimEndTime(mExtractor.getSampleTime())){ + mIsTrimEOS = true; + mDecoder.queueInputBuffer(result, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM); + mExtractor.advance(); + return DRAIN_STATE_NONE; + } boolean isKeyFrame = (mExtractor.getSampleFlags() & MediaExtractor.SAMPLE_FLAG_SYNC) != 0; mDecoder.queueInputBuffer(result, 0, sampleSize, mExtractor.getSampleTime(), isKeyFrame ? MediaCodec.BUFFER_FLAG_SYNC_FRAME : 0); mExtractor.advance(); @@ -228,4 +241,8 @@ private int drainEncoder(long timeoutUs) { mEncoder.releaseOutputBuffer(result, false); return DRAIN_STATE_CONSUMED; } + + private boolean isPastTrimEndTime(long sampleTime) { + return mMediaTrimTime != null && mMediaTrimTime.endTimeInUs > 0 && sampleTime > mMediaTrimTime.endTimeInUs; + } }