aboutsummaryrefslogtreecommitdiffstats
path: root/libs/android-transcoder/src/main/java/net/ypresto/androidtranscoder/engine/VideoTrackTranscoder.java
diff options
context:
space:
mode:
Diffstat (limited to 'libs/android-transcoder/src/main/java/net/ypresto/androidtranscoder/engine/VideoTrackTranscoder.java')
-rw-r--r--libs/android-transcoder/src/main/java/net/ypresto/androidtranscoder/engine/VideoTrackTranscoder.java231
1 files changed, 231 insertions, 0 deletions
diff --git a/libs/android-transcoder/src/main/java/net/ypresto/androidtranscoder/engine/VideoTrackTranscoder.java b/libs/android-transcoder/src/main/java/net/ypresto/androidtranscoder/engine/VideoTrackTranscoder.java
new file mode 100644
index 000000000..a5640dd99
--- /dev/null
+++ b/libs/android-transcoder/src/main/java/net/ypresto/androidtranscoder/engine/VideoTrackTranscoder.java
@@ -0,0 +1,231 @@
+/*
+ * Copyright (C) 2014 Yuya Tanaka
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package net.ypresto.androidtranscoder.engine;
+
+import android.media.MediaCodec;
+import android.media.MediaExtractor;
+import android.media.MediaFormat;
+
+import net.ypresto.androidtranscoder.format.MediaFormatExtraConstants;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+
+// Refer: https://android.googlesource.com/platform/cts/+/lollipop-release/tests/tests/media/src/android/media/cts/ExtractDecodeEditEncodeMuxTest.java
+public class VideoTrackTranscoder implements TrackTranscoder {
+ private static final String TAG = "VideoTrackTranscoder";
+ private static final int DRAIN_STATE_NONE = 0;
+ private static final int DRAIN_STATE_SHOULD_RETRY_IMMEDIATELY = 1;
+ private static final int DRAIN_STATE_CONSUMED = 2;
+
+ private final MediaExtractor mExtractor;
+ private final int mTrackIndex;
+ private final MediaFormat mOutputFormat;
+ private final QueuedMuxer mMuxer;
+ private final MediaCodec.BufferInfo mBufferInfo = new MediaCodec.BufferInfo();
+ private MediaCodec mDecoder;
+ private MediaCodec mEncoder;
+ private ByteBuffer[] mDecoderInputBuffers;
+ private ByteBuffer[] mEncoderOutputBuffers;
+ private MediaFormat mActualOutputFormat;
+ private OutputSurface mDecoderOutputSurfaceWrapper;
+ private InputSurface mEncoderInputSurfaceWrapper;
+ private boolean mIsExtractorEOS;
+ private boolean mIsDecoderEOS;
+ private boolean mIsEncoderEOS;
+ private boolean mDecoderStarted;
+ private boolean mEncoderStarted;
+ private long mWrittenPresentationTimeUs;
+
+ public VideoTrackTranscoder(MediaExtractor extractor, int trackIndex,
+ MediaFormat outputFormat, QueuedMuxer muxer) {
+ mExtractor = extractor;
+ mTrackIndex = trackIndex;
+ mOutputFormat = outputFormat;
+ mMuxer = muxer;
+ }
+
+ @Override
+ public void setup() {
+ mExtractor.selectTrack(mTrackIndex);
+ try {
+ mEncoder = MediaCodec.createEncoderByType(mOutputFormat.getString(MediaFormat.KEY_MIME));
+ } catch (IOException e) {
+ throw new IllegalStateException(e);
+ }
+ mEncoder.configure(mOutputFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
+ mEncoderInputSurfaceWrapper = new InputSurface(mEncoder.createInputSurface());
+ mEncoderInputSurfaceWrapper.makeCurrent();
+ mEncoder.start();
+ mEncoderStarted = true;
+ mEncoderOutputBuffers = mEncoder.getOutputBuffers();
+
+ MediaFormat inputFormat = mExtractor.getTrackFormat(mTrackIndex);
+ if (inputFormat.containsKey(MediaFormatExtraConstants.KEY_ROTATION_DEGREES)) {
+ // Decoded video is rotated automatically in Android 5.0 lollipop.
+ // Turn off here because we don't want to encode rotated one.
+ // refer: https://android.googlesource.com/platform/frameworks/av/+blame/lollipop-release/media/libstagefright/Utils.cpp
+ inputFormat.setInteger(MediaFormatExtraConstants.KEY_ROTATION_DEGREES, 0);
+ }
+ mDecoderOutputSurfaceWrapper = new OutputSurface();
+ try {
+ mDecoder = MediaCodec.createDecoderByType(inputFormat.getString(MediaFormat.KEY_MIME));
+ } catch (IOException e) {
+ throw new IllegalStateException(e);
+ }
+ mDecoder.configure(inputFormat, mDecoderOutputSurfaceWrapper.getSurface(), null, 0);
+ mDecoder.start();
+ mDecoderStarted = true;
+ mDecoderInputBuffers = mDecoder.getInputBuffers();
+ }
+
+ @Override
+ public MediaFormat getDeterminedFormat() {
+ return mActualOutputFormat;
+ }
+
+ @Override
+ public boolean stepPipeline() {
+ boolean busy = false;
+
+ int status;
+ while (drainEncoder(0) != DRAIN_STATE_NONE) busy = true;
+ do {
+ status = drainDecoder(0);
+ if (status != DRAIN_STATE_NONE) busy = true;
+ // NOTE: not repeating to keep from deadlock when encoder is full.
+ } while (status == DRAIN_STATE_SHOULD_RETRY_IMMEDIATELY);
+ while (drainExtractor(0) != DRAIN_STATE_NONE) busy = true;
+
+ return busy;
+ }
+
+ @Override
+ public long getWrittenPresentationTimeUs() {
+ return mWrittenPresentationTimeUs;
+ }
+
+ @Override
+ public boolean isFinished() {
+ return mIsEncoderEOS;
+ }
+
+ // TODO: CloseGuard
+ @Override
+ public void release() {
+ if (mDecoderOutputSurfaceWrapper != null) {
+ mDecoderOutputSurfaceWrapper.release();
+ mDecoderOutputSurfaceWrapper = null;
+ }
+ if (mEncoderInputSurfaceWrapper != null) {
+ mEncoderInputSurfaceWrapper.release();
+ mEncoderInputSurfaceWrapper = null;
+ }
+ if (mDecoder != null) {
+ if (mDecoderStarted) mDecoder.stop();
+ mDecoder.release();
+ mDecoder = null;
+ }
+ if (mEncoder != null) {
+ if (mEncoderStarted) mEncoder.stop();
+ mEncoder.release();
+ mEncoder = null;
+ }
+ }
+
+ private int drainExtractor(long timeoutUs) {
+ if (mIsExtractorEOS) return DRAIN_STATE_NONE;
+ int trackIndex = mExtractor.getSampleTrackIndex();
+ if (trackIndex >= 0 && trackIndex != mTrackIndex) {
+ return DRAIN_STATE_NONE;
+ }
+ int result = mDecoder.dequeueInputBuffer(timeoutUs);
+ if (result < 0) return DRAIN_STATE_NONE;
+ if (trackIndex < 0) {
+ mIsExtractorEOS = true;
+ mDecoder.queueInputBuffer(result, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
+ return DRAIN_STATE_NONE;
+ }
+ int sampleSize = mExtractor.readSampleData(mDecoderInputBuffers[result], 0);
+ 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();
+ return DRAIN_STATE_CONSUMED;
+ }
+
+ private int drainDecoder(long timeoutUs) {
+ if (mIsDecoderEOS) return DRAIN_STATE_NONE;
+ int result = mDecoder.dequeueOutputBuffer(mBufferInfo, timeoutUs);
+ switch (result) {
+ case MediaCodec.INFO_TRY_AGAIN_LATER:
+ return DRAIN_STATE_NONE;
+ case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
+ case MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED:
+ return DRAIN_STATE_SHOULD_RETRY_IMMEDIATELY;
+ }
+ if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
+ mEncoder.signalEndOfInputStream();
+ mIsDecoderEOS = true;
+ mBufferInfo.size = 0;
+ }
+ boolean doRender = (mBufferInfo.size > 0);
+ // NOTE: doRender will block if buffer (of encoder) is full.
+ // Refer: http://bigflake.com/mediacodec/CameraToMpegTest.java.txt
+ mDecoder.releaseOutputBuffer(result, doRender);
+ if (doRender) {
+ mDecoderOutputSurfaceWrapper.awaitNewImage();
+ mDecoderOutputSurfaceWrapper.drawImage();
+ mEncoderInputSurfaceWrapper.setPresentationTime(mBufferInfo.presentationTimeUs * 1000);
+ mEncoderInputSurfaceWrapper.swapBuffers();
+ }
+ return DRAIN_STATE_CONSUMED;
+ }
+
+ private int drainEncoder(long timeoutUs) {
+ if (mIsEncoderEOS) return DRAIN_STATE_NONE;
+ int result = mEncoder.dequeueOutputBuffer(mBufferInfo, timeoutUs);
+ switch (result) {
+ case MediaCodec.INFO_TRY_AGAIN_LATER:
+ return DRAIN_STATE_NONE;
+ case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
+ if (mActualOutputFormat != null)
+ throw new RuntimeException("Video output format changed twice.");
+ mActualOutputFormat = mEncoder.getOutputFormat();
+ mMuxer.setOutputFormat(QueuedMuxer.SampleType.VIDEO, mActualOutputFormat);
+ return DRAIN_STATE_SHOULD_RETRY_IMMEDIATELY;
+ case MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED:
+ mEncoderOutputBuffers = mEncoder.getOutputBuffers();
+ return DRAIN_STATE_SHOULD_RETRY_IMMEDIATELY;
+ }
+ if (mActualOutputFormat == null) {
+ throw new RuntimeException("Could not determine actual output format.");
+ }
+
+ if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
+ mIsEncoderEOS = true;
+ mBufferInfo.set(0, 0, 0, mBufferInfo.flags);
+ }
+ if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
+ // SPS or PPS, which should be passed by MediaFormat.
+ mEncoder.releaseOutputBuffer(result, false);
+ return DRAIN_STATE_SHOULD_RETRY_IMMEDIATELY;
+ }
+ mMuxer.writeSampleData(QueuedMuxer.SampleType.VIDEO, mEncoderOutputBuffers[result], mBufferInfo);
+ mWrittenPresentationTimeUs = mBufferInfo.presentationTimeUs;
+ mEncoder.releaseOutputBuffer(result, false);
+ return DRAIN_STATE_CONSUMED;
+ }
+}