| // Copyright 2013 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| package org.chromium.media; |
| |
| import android.annotation.SuppressLint; |
| import android.annotation.TargetApi; |
| import android.media.AudioFormat; |
| import android.media.MediaCodec; |
| import android.media.MediaCodec.CryptoInfo; |
| import android.media.MediaCrypto; |
| import android.media.MediaFormat; |
| import android.os.Build; |
| import android.os.Bundle; |
| import android.os.Handler; |
| import android.os.HandlerThread; |
| import android.os.Looper; |
| import android.view.Surface; |
| |
| import org.chromium.base.Log; |
| import org.chromium.base.annotations.CalledByNative; |
| import org.chromium.base.annotations.JNINamespace; |
| import org.chromium.base.annotations.MainDex; |
| import org.chromium.base.annotations.NativeMethods; |
| |
| import java.nio.ByteBuffer; |
| import java.util.LinkedList; |
| import java.util.Queue; |
| |
| /** |
| * A MediaCodec wrapper for adapting the API and catching exceptions. |
| */ |
| @JNINamespace("media") |
| @MainDex |
| class MediaCodecBridge { |
| private static final String TAG = "MediaCodecBridge"; |
| |
| // After a flush(), dequeueOutputBuffer() can often produce empty presentation timestamps |
| // for several frames. As a result, the player may find that the time does not increase |
| // after decoding a frame. To detect this, we check whether the presentation timestamp from |
| // dequeueOutputBuffer() is larger than input_timestamp - MAX_PRESENTATION_TIMESTAMP_SHIFT_US |
| // after a flush. And we set the presentation timestamp from dequeueOutputBuffer() to be |
| // non-decreasing for the remaining frames. |
| private static final long MAX_PRESENTATION_TIMESTAMP_SHIFT_US = 100000; |
| |
| // We use only one output audio format (PCM16) that has 2 bytes per sample |
| private static final int PCM16_BYTES_PER_SAMPLE = 2; |
| |
| private static final int MEDIA_CODEC_UNKNOWN_CIPHER_MODE = -1; |
| |
| // TODO(qinmin): Use MediaFormat constants when part of the public API. |
| private static final String KEY_CROP_LEFT = "crop-left"; |
| private static final String KEY_CROP_RIGHT = "crop-right"; |
| private static final String KEY_CROP_BOTTOM = "crop-bottom"; |
| private static final String KEY_CROP_TOP = "crop-top"; |
| |
| protected MediaCodec mMediaCodec; |
| private @BitrateAdjuster.Type int mBitrateAdjuster; |
| |
| // The maximum input size this codec was configured with. |
| private int mMaxInputSize; |
| |
| // To support both the synchronous and asynchronous version of MediaCodec |
| // (since we need to work on <M devices), we implement async support as a |
| // layer under synchronous API calls and provide a callback signal for when |
| // work (new input, new output, errors, or format changes) is available. |
| // |
| // Once the callback has been set on MediaCodec, these variables must only |
| // be accessed from synchronized(this) blocks since MediaCodecCallback may |
| // execute on an arbitrary thread. |
| private boolean mUseAsyncApi; |
| private Queue<GetOutputFormatResult> mPendingFormat; |
| private GetOutputFormatResult mCurrentFormat; |
| private boolean mPendingError; |
| private boolean mPendingStart; |
| private long mNativeMediaCodecBridge; |
| private int mSequenceCounter; |
| private Queue<DequeueInputResult> mPendingInputBuffers; |
| private Queue<DequeueOutputResult> mPendingOutputBuffers; |
| |
| // Set by tests which don't have a Java MessagePump to ensure the MediaCodec |
| // callbacks are actually delivered. Always null in production. |
| private static HandlerThread sCallbackHandlerThread; |
| private static Handler sCallbackHandler; |
| |
| private static class DequeueInputResult { |
| private final int mStatus; |
| private final int mIndex; |
| |
| private DequeueInputResult(int status, int index) { |
| mStatus = status; |
| mIndex = index; |
| } |
| |
| @CalledByNative("DequeueInputResult") |
| private int status() { |
| return mStatus; |
| } |
| |
| @CalledByNative("DequeueInputResult") |
| private int index() { |
| return mIndex; |
| } |
| } |
| |
| private static class DequeueOutputResult { |
| private final int mStatus; |
| private final int mIndex; |
| private final int mFlags; |
| private final int mOffset; |
| private final long mPresentationTimeMicroseconds; |
| private final int mNumBytes; |
| |
| private DequeueOutputResult(int status, int index, int flags, int offset, |
| long presentationTimeMicroseconds, int numBytes) { |
| mStatus = status; |
| mIndex = index; |
| mFlags = flags; |
| mOffset = offset; |
| mPresentationTimeMicroseconds = presentationTimeMicroseconds; |
| mNumBytes = numBytes; |
| } |
| |
| @CalledByNative("DequeueOutputResult") |
| private int status() { |
| return mStatus; |
| } |
| |
| @CalledByNative("DequeueOutputResult") |
| private int index() { |
| return mIndex; |
| } |
| |
| @CalledByNative("DequeueOutputResult") |
| private int flags() { |
| return mFlags; |
| } |
| |
| @CalledByNative("DequeueOutputResult") |
| private int offset() { |
| return mOffset; |
| } |
| |
| @CalledByNative("DequeueOutputResult") |
| private long presentationTimeMicroseconds() { |
| return mPresentationTimeMicroseconds; |
| } |
| |
| @CalledByNative("DequeueOutputResult") |
| private int numBytes() { |
| return mNumBytes; |
| } |
| } |
| |
| /** A wrapper around a MediaFormat. */ |
| private static class GetOutputFormatResult { |
| private final int mStatus; |
| // May be null if mStatus is not MediaCodecStatus.OK. |
| private final MediaFormat mFormat; |
| |
| private GetOutputFormatResult(int status, MediaFormat format) { |
| mStatus = status; |
| mFormat = format; |
| } |
| |
| private boolean formatHasCropValues() { |
| return mFormat.containsKey(KEY_CROP_RIGHT) && mFormat.containsKey(KEY_CROP_LEFT) |
| && mFormat.containsKey(KEY_CROP_BOTTOM) && mFormat.containsKey(KEY_CROP_TOP); |
| } |
| |
| @CalledByNative("GetOutputFormatResult") |
| private int status() { |
| return mStatus; |
| } |
| |
| @CalledByNative("GetOutputFormatResult") |
| private int width() { |
| return formatHasCropValues() |
| ? mFormat.getInteger(KEY_CROP_RIGHT) - mFormat.getInteger(KEY_CROP_LEFT) + 1 |
| : mFormat.getInteger(MediaFormat.KEY_WIDTH); |
| } |
| |
| @CalledByNative("GetOutputFormatResult") |
| private int height() { |
| return formatHasCropValues() |
| ? mFormat.getInteger(KEY_CROP_BOTTOM) - mFormat.getInteger(KEY_CROP_TOP) + 1 |
| : mFormat.getInteger(MediaFormat.KEY_HEIGHT); |
| } |
| |
| @CalledByNative("GetOutputFormatResult") |
| private int sampleRate() { |
| return mFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE); |
| } |
| |
| @CalledByNative("GetOutputFormatResult") |
| private int channelCount() { |
| return mFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT); |
| } |
| } |
| |
| // Warning: This class may execute on an arbitrary thread for the lifetime |
| // of the MediaCodec. The MediaCodecBridge methods it calls are synchronized |
| // to avoid race conditions. |
| @TargetApi(Build.VERSION_CODES.M) |
| class MediaCodecCallback extends MediaCodec.Callback { |
| private MediaCodecBridge mMediaCodecBridge; |
| MediaCodecCallback(MediaCodecBridge bridge) { |
| mMediaCodecBridge = bridge; |
| } |
| |
| @Override |
| public void onError(MediaCodec codec, MediaCodec.CodecException e) { |
| // TODO(dalecurtis): We may want to drop transient errors here. |
| Log.e(TAG, "MediaCodec.onError: %s", e.getDiagnosticInfo()); |
| mMediaCodecBridge.onError(e); |
| } |
| |
| @Override |
| public void onInputBufferAvailable(MediaCodec codec, int index) { |
| mMediaCodecBridge.onInputBufferAvailable(index); |
| } |
| |
| @Override |
| public void onOutputBufferAvailable( |
| MediaCodec codec, int index, MediaCodec.BufferInfo info) { |
| mMediaCodecBridge.onOutputBufferAvailable(index, info); |
| } |
| |
| @Override |
| public void onOutputFormatChanged(MediaCodec codec, MediaFormat format) { |
| mMediaCodecBridge.onOutputFormatChanged(format); |
| } |
| }; |
| |
| MediaCodecBridge( |
| MediaCodec mediaCodec, @BitrateAdjuster.Type int bitrateAdjuster, boolean useAsyncApi) { |
| assert mediaCodec != null; |
| mMediaCodec = mediaCodec; |
| mBitrateAdjuster = bitrateAdjuster; |
| mUseAsyncApi = useAsyncApi; |
| |
| if (!mUseAsyncApi) return; |
| |
| enableAsyncApi(); |
| prepareAsyncApiForRestart(); |
| } |
| |
| // There's a Lollipop version of the setCallback() API, so we could enable |
| // it there, but since it's likely to be more stable in later SDK versions |
| // and our tests require their own Handler to pump the callbacks, we limit |
| // support to Marshmallow only. |
| @TargetApi(Build.VERSION_CODES.M) |
| private void enableAsyncApi() { |
| mPendingError = false; |
| mPendingFormat = new LinkedList<GetOutputFormatResult>(); |
| mPendingInputBuffers = new LinkedList<DequeueInputResult>(); |
| mPendingOutputBuffers = new LinkedList<DequeueOutputResult>(); |
| mMediaCodec.setCallback(new MediaCodecCallback(this), sCallbackHandler); |
| } |
| |
| // The methods below are all synchronized because we may receive callbacks |
| // from the MediaCodecCallback on a different thread; especially in the |
| // testing case where we create a separate HandlerThread. |
| |
| private synchronized void prepareAsyncApiForRestart() { |
| mPendingFormat.clear(); |
| mPendingInputBuffers.clear(); |
| mPendingOutputBuffers.clear(); |
| mPendingStart = true; |
| mCurrentFormat = null; |
| ++mSequenceCounter; |
| } |
| |
| @CalledByNative |
| private synchronized void setBuffersAvailableListener(long nativeMediaCodecBridge) { |
| mNativeMediaCodecBridge = nativeMediaCodecBridge; |
| |
| // If any buffers or errors occurred before this, trigger the callback now. |
| if (!mPendingInputBuffers.isEmpty() || !mPendingOutputBuffers.isEmpty() || mPendingError) { |
| notifyBuffersAvailable(); |
| } |
| } |
| |
| private synchronized void notifyBuffersAvailable() { |
| if (mNativeMediaCodecBridge != 0) { |
| MediaCodecBridgeJni.get().onBuffersAvailable( |
| mNativeMediaCodecBridge, MediaCodecBridge.this); |
| } |
| } |
| |
| public synchronized void onError(MediaCodec.CodecException e) { |
| mPendingError = true; |
| mPendingInputBuffers.clear(); |
| mPendingOutputBuffers.clear(); |
| notifyBuffersAvailable(); |
| } |
| |
| public synchronized void onInputBufferAvailable(int index) { |
| if (mPendingStart) return; |
| |
| mPendingInputBuffers.add(new DequeueInputResult(MediaCodecStatus.OK, index)); |
| notifyBuffersAvailable(); |
| } |
| |
| public synchronized void onOutputBufferAvailable(int index, MediaCodec.BufferInfo info) { |
| // Drop buffers that come in during a flush. |
| if (mPendingStart) return; |
| |
| mPendingOutputBuffers.add(new DequeueOutputResult(MediaCodecStatus.OK, index, info.flags, |
| info.offset, info.presentationTimeUs, info.size)); |
| notifyBuffersAvailable(); |
| } |
| |
| public synchronized void onOutputFormatChanged(MediaFormat format) { |
| mPendingOutputBuffers.add( |
| new DequeueOutputResult(MediaCodecStatus.OUTPUT_FORMAT_CHANGED, -1, 0, 0, 0, 0)); |
| mPendingFormat.add(new GetOutputFormatResult(MediaCodecStatus.OK, format)); |
| notifyBuffersAvailable(); |
| } |
| |
| public synchronized void onPendingStartComplete(int sequenceCounter) { |
| // Ignore events from the past. |
| if (mSequenceCounter != sequenceCounter) return; |
| mPendingStart = false; |
| } |
| |
| @CalledByNative |
| void release() { |
| if (mUseAsyncApi) { |
| // Disconnect from the native code to ensure we don't issue calls |
| // into it after its destruction. |
| synchronized (this) { |
| mNativeMediaCodecBridge = 0; |
| } |
| } |
| try { |
| String codecName = mMediaCodec.getName(); |
| // This logging is to help us identify hung MediaCodecs in crash reports. |
| Log.w(TAG, "Releasing: %s", codecName); |
| mMediaCodec.release(); |
| Log.w(TAG, "Codec released"); |
| } catch (IllegalStateException e) { |
| // The MediaCodec is stuck in a bad state, possibly due to losing |
| // the surface. |
| Log.e(TAG, "Cannot release media codec", e); |
| } |
| mMediaCodec = null; |
| } |
| |
| // TODO(sanfin): Move this to constructor or builder. |
| @SuppressWarnings("deprecation") |
| boolean start() { |
| try { |
| if (mUseAsyncApi) { |
| synchronized (this) { |
| if (mPendingError) return false; |
| |
| class CompletePendingStartTask implements Runnable { |
| private int mThisSequence; |
| CompletePendingStartTask(int sequence) { |
| mThisSequence = sequence; |
| } |
| |
| @Override |
| public void run() { |
| onPendingStartComplete(mThisSequence); |
| } |
| }; |
| |
| // Ensure any pending indices are ignored until after start |
| // by trampolining through the handler/looper that the |
| // notifications are coming from. |
| Handler h = sCallbackHandler == null ? new Handler(Looper.getMainLooper()) |
| : sCallbackHandler; |
| h.post(new CompletePendingStartTask(mSequenceCounter)); |
| } |
| } |
| |
| mMediaCodec.start(); |
| } catch (IllegalStateException e) { |
| Log.e(TAG, "Cannot start the media codec", e); |
| return false; |
| } catch (IllegalArgumentException e) { |
| Log.e(TAG, "Cannot start the media codec", e); |
| return false; |
| } |
| return true; |
| } |
| |
| @CalledByNative |
| private DequeueInputResult dequeueInputBuffer(long timeoutUs) { |
| if (mUseAsyncApi) { |
| synchronized (this) { |
| if (mPendingError) return new DequeueInputResult(MediaCodecStatus.ERROR, -1); |
| if (mPendingStart || mPendingInputBuffers.isEmpty()) { |
| return new DequeueInputResult(MediaCodecStatus.TRY_AGAIN_LATER, -1); |
| } |
| return mPendingInputBuffers.remove(); |
| } |
| } |
| |
| int status = MediaCodecStatus.ERROR; |
| int index = -1; |
| try { |
| int indexOrStatus = mMediaCodec.dequeueInputBuffer(timeoutUs); |
| if (indexOrStatus >= 0) { // index! |
| status = MediaCodecStatus.OK; |
| index = indexOrStatus; |
| } else if (indexOrStatus == MediaCodec.INFO_TRY_AGAIN_LATER) { |
| status = MediaCodecStatus.TRY_AGAIN_LATER; |
| } else { |
| Log.e(TAG, "Unexpected index_or_status: %d", indexOrStatus); |
| assert false; |
| } |
| } catch (Exception e) { |
| Log.e(TAG, "Failed to dequeue input buffer", e); |
| } |
| return new DequeueInputResult(status, index); |
| } |
| |
| @CalledByNative |
| private int flush() { |
| try { |
| mMediaCodec.flush(); |
| |
| // MediaCodec.flush() invalidates all returned indices, but there |
| // may be some unhandled callbacks when using the async API. When |
| // we call prepareAsyncApiForRestart() it will set mPendingStart, |
| // start() will then post a task through the callback handler which |
| // clears mPendingStart to start accepting new buffers. |
| if (mUseAsyncApi) { |
| prepareAsyncApiForRestart(); |
| if (!start()) return MediaCodecStatus.ERROR; |
| } |
| } catch (Exception e) { |
| Log.e(TAG, "Failed to flush MediaCodec", e); |
| return MediaCodecStatus.ERROR; |
| } |
| return MediaCodecStatus.OK; |
| } |
| |
| @CalledByNative |
| private void stop() { |
| try { |
| mMediaCodec.stop(); |
| |
| // MediaCodec.stop() invalidates all returned indices. |
| if (mUseAsyncApi) prepareAsyncApiForRestart(); |
| } catch (IllegalStateException e) { |
| Log.e(TAG, "Failed to stop MediaCodec", e); |
| } |
| } |
| |
| @CalledByNative |
| private String getName() { |
| String codecName = "unknown"; |
| try { |
| codecName = mMediaCodec.getName(); |
| } catch (IllegalStateException e) { |
| Log.e(TAG, "Cannot get codec name", e); |
| } |
| return codecName; |
| } |
| |
| @CalledByNative |
| private GetOutputFormatResult getOutputFormat() { |
| if (mUseAsyncApi && mCurrentFormat != null) return mCurrentFormat; |
| |
| MediaFormat format = null; |
| int status = MediaCodecStatus.OK; |
| try { |
| format = mMediaCodec.getOutputFormat(); |
| } catch (IllegalStateException e) { |
| Log.e(TAG, "Failed to get output format", e); |
| status = MediaCodecStatus.ERROR; |
| } |
| return new GetOutputFormatResult(status, format); |
| } |
| |
| /** Returns null if MediaCodec throws IllegalStateException. */ |
| @CalledByNative |
| private ByteBuffer getInputBuffer(int index) { |
| if (mUseAsyncApi) { |
| synchronized (this) { |
| if (mPendingError) return null; |
| } |
| } |
| try { |
| return mMediaCodec.getInputBuffer(index); |
| } catch (IllegalStateException e) { |
| Log.e(TAG, "Failed to get input buffer", e); |
| return null; |
| } |
| } |
| |
| /** Returns null if MediaCodec throws IllegalStateException. */ |
| @CalledByNative |
| protected ByteBuffer getOutputBuffer(int index) { |
| try { |
| return mMediaCodec.getOutputBuffer(index); |
| } catch (IllegalStateException e) { |
| Log.e(TAG, "Failed to get output buffer", e); |
| return null; |
| } |
| } |
| |
| @CalledByNative |
| private int queueInputBuffer( |
| int index, int offset, int size, long presentationTimeUs, int flags) { |
| try { |
| mMediaCodec.queueInputBuffer(index, offset, size, presentationTimeUs, flags); |
| } catch (Exception e) { |
| Log.e(TAG, "Failed to queue input buffer", e); |
| return MediaCodecStatus.ERROR; |
| } |
| return MediaCodecStatus.OK; |
| } |
| |
| @CalledByNative |
| private void setVideoBitrate(int bps, int frameRate) { |
| int targetBps = BitrateAdjuster.getTargetBitrate(mBitrateAdjuster, bps, frameRate); |
| Bundle b = new Bundle(); |
| b.putInt(MediaCodec.PARAMETER_KEY_VIDEO_BITRATE, targetBps); |
| try { |
| mMediaCodec.setParameters(b); |
| } catch (IllegalStateException e) { |
| Log.e(TAG, "Failed to set MediaCodec parameters", e); |
| } |
| Log.v(TAG, "setVideoBitrate: input %dbps@%d, targetBps %d", bps, frameRate, targetBps); |
| } |
| |
| @CalledByNative |
| private void requestKeyFrameSoon() { |
| Bundle b = new Bundle(); |
| b.putInt(MediaCodec.PARAMETER_KEY_REQUEST_SYNC_FRAME, 0); |
| try { |
| mMediaCodec.setParameters(b); |
| } catch (IllegalStateException e) { |
| Log.e(TAG, "Failed to set MediaCodec parameters", e); |
| } |
| } |
| |
| // Incoming |native| values are as defined in media/base/encryption_scheme.h. Translated values |
| // are from MediaCodec. At present, these values are in sync. Returns |
| // MEDIA_CODEC_UNKNOWN_CIPHER_MODE in the case of unknown incoming value. |
| private int translateEncryptionSchemeValue(int nativeValue) { |
| switch (nativeValue) { |
| case EncryptionScheme.UNENCRYPTED: |
| return MediaCodec.CRYPTO_MODE_UNENCRYPTED; |
| case EncryptionScheme.CENC: |
| return MediaCodec.CRYPTO_MODE_AES_CTR; |
| case EncryptionScheme.CBCS: |
| return MediaCodec.CRYPTO_MODE_AES_CBC; |
| default: |
| Log.e(TAG, "Unsupported cipher mode: %d", nativeValue); |
| return MEDIA_CODEC_UNKNOWN_CIPHER_MODE; |
| } |
| } |
| |
| @SuppressLint("WrongConstant") // False positive on logging statement. |
| @CalledByNative |
| private int queueSecureInputBuffer(int index, int offset, byte[] iv, byte[] keyId, |
| int[] numBytesOfClearData, int[] numBytesOfEncryptedData, int numSubSamples, |
| int cipherMode, int patternEncrypt, int patternSkip, long presentationTimeUs) { |
| try { |
| cipherMode = translateEncryptionSchemeValue(cipherMode); |
| if (cipherMode == MEDIA_CODEC_UNKNOWN_CIPHER_MODE) { |
| return MediaCodecStatus.ERROR; |
| } |
| boolean usesCbcs = cipherMode == MediaCodec.CRYPTO_MODE_AES_CBC; |
| if (usesCbcs && !MediaCodecUtil.platformSupportsCbcsEncryption(Build.VERSION.SDK_INT)) { |
| Log.e(TAG, "Encryption scheme 'cbcs' not supported on this platform."); |
| return MediaCodecStatus.ERROR; |
| } |
| CryptoInfo cryptoInfo = new CryptoInfo(); |
| cryptoInfo.set(numSubSamples, numBytesOfClearData, numBytesOfEncryptedData, keyId, iv, |
| cipherMode); |
| if (patternEncrypt != 0 && patternSkip != 0) { |
| if (usesCbcs) { |
| // Above platform check ensured that setting the pattern is indeed supported. |
| MediaCodecUtil.setPatternIfSupported(cryptoInfo, patternEncrypt, patternSkip); |
| } else { |
| Log.e(TAG, "Pattern encryption only supported for 'cbcs' scheme (CBC mode)."); |
| return MediaCodecStatus.ERROR; |
| } |
| } |
| mMediaCodec.queueSecureInputBuffer(index, offset, cryptoInfo, presentationTimeUs, 0); |
| } catch (MediaCodec.CryptoException e) { |
| if (e.getErrorCode() == MediaCodec.CryptoException.ERROR_NO_KEY) { |
| Log.d(TAG, "Failed to queue secure input buffer: CryptoException.ERROR_NO_KEY"); |
| return MediaCodecStatus.NO_KEY; |
| } |
| Log.e(TAG, "Failed to queue secure input buffer. Error code %d", e.getErrorCode(), e); |
| return MediaCodecStatus.ERROR; |
| } catch (IllegalArgumentException e) { |
| // IllegalArgumentException can occur when release() is called on the MediaCrypto |
| // object, but the MediaCodecBridge is unaware of the change. |
| Log.e(TAG, "Failed to queue secure input buffer.", e); |
| return MediaCodecStatus.ERROR; |
| } catch (IllegalStateException e) { |
| Log.e(TAG, "Failed to queue secure input buffer.", e); |
| return MediaCodecStatus.ERROR; |
| } |
| return MediaCodecStatus.OK; |
| } |
| |
| @CalledByNative |
| protected void releaseOutputBuffer(int index, boolean render) { |
| try { |
| mMediaCodec.releaseOutputBuffer(index, render); |
| } catch (IllegalStateException e) { |
| // TODO(qinmin): May need to report the error to the caller. crbug.com/356498. |
| Log.e(TAG, "Failed to release output buffer", e); |
| } |
| } |
| |
| @SuppressWarnings("deprecation") |
| @CalledByNative |
| private DequeueOutputResult dequeueOutputBuffer(long timeoutUs) { |
| if (mUseAsyncApi) { |
| synchronized (this) { |
| if (mPendingError) { |
| return new DequeueOutputResult(MediaCodecStatus.ERROR, -1, 0, 0, 0, 0); |
| } |
| if (mPendingOutputBuffers.isEmpty()) { |
| return new DequeueOutputResult( |
| MediaCodecStatus.TRY_AGAIN_LATER, -1, 0, 0, 0, 0); |
| } |
| if (mPendingOutputBuffers.peek().status() |
| == MediaCodecStatus.OUTPUT_FORMAT_CHANGED) { |
| assert !mPendingFormat.isEmpty(); |
| mCurrentFormat = mPendingFormat.remove(); |
| } |
| return mPendingOutputBuffers.remove(); |
| } |
| } |
| |
| MediaCodec.BufferInfo info = new MediaCodec.BufferInfo(); |
| int status = MediaCodecStatus.ERROR; |
| int index = -1; |
| try { |
| int indexOrStatus = dequeueOutputBufferInternal(info, timeoutUs); |
| |
| if (indexOrStatus >= 0) { // index! |
| status = MediaCodecStatus.OK; |
| index = indexOrStatus; |
| } else if (indexOrStatus == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) { |
| status = MediaCodecStatus.OUTPUT_BUFFERS_CHANGED; |
| } else if (indexOrStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { |
| status = MediaCodecStatus.OUTPUT_FORMAT_CHANGED; |
| MediaFormat newFormat = mMediaCodec.getOutputFormat(); |
| } else if (indexOrStatus == MediaCodec.INFO_TRY_AGAIN_LATER) { |
| status = MediaCodecStatus.TRY_AGAIN_LATER; |
| } else { |
| Log.e(TAG, "Unexpected index_or_status: %d", indexOrStatus); |
| assert false; |
| } |
| } catch (IllegalStateException e) { |
| status = MediaCodecStatus.ERROR; |
| Log.e(TAG, "Failed to dequeue output buffer", e); |
| } |
| |
| return new DequeueOutputResult( |
| status, index, info.flags, info.offset, info.presentationTimeUs, info.size); |
| } |
| |
| protected int dequeueOutputBufferInternal(MediaCodec.BufferInfo info, long timeoutUs) { |
| return mMediaCodec.dequeueOutputBuffer(info, timeoutUs); |
| } |
| |
| // TODO(sanfin): Move this out of MediaCodecBridge. |
| boolean configureVideo(MediaFormat format, Surface surface, MediaCrypto crypto, int flags) { |
| try { |
| mMediaCodec.configure(format, surface, crypto, flags); |
| if (format.containsKey(MediaFormat.KEY_MAX_INPUT_SIZE)) { |
| mMaxInputSize = format.getInteger(MediaFormat.KEY_MAX_INPUT_SIZE); |
| return true; |
| } |
| } catch (IllegalArgumentException e) { |
| Log.e(TAG, "Cannot configure the video codec, wrong format or surface", e); |
| } catch (IllegalStateException e) { |
| Log.e(TAG, "Cannot configure the video codec", e); |
| } catch (MediaCodec.CryptoException e) { |
| Log.e(TAG, "Cannot configure the video codec: DRM error", e); |
| } catch (Exception e) { |
| Log.e(TAG, "Cannot configure the video codec", e); |
| } |
| return false; |
| } |
| |
| @TargetApi(Build.VERSION_CODES.M) |
| @CalledByNative |
| private boolean setSurface(Surface surface) { |
| try { |
| mMediaCodec.setOutputSurface(surface); |
| } catch (IllegalArgumentException | IllegalStateException e) { |
| Log.e(TAG, "Cannot set output surface", e); |
| return false; |
| } |
| return true; |
| } |
| |
| // TODO(sanfin): Move this out of MediaCodecBridge. |
| boolean configureAudio(MediaFormat format, MediaCrypto crypto, int flags) { |
| try { |
| mMediaCodec.configure(format, null, crypto, flags); |
| return true; |
| } catch (IllegalArgumentException e) { |
| Log.e(TAG, "Cannot configure the audio codec", e); |
| } catch (IllegalStateException e) { |
| Log.e(TAG, "Cannot configure the audio codec", e); |
| } catch (MediaCodec.CryptoException e) { |
| Log.e(TAG, "Cannot configure the audio codec: DRM error", e); |
| } catch (Exception e) { |
| Log.e(TAG, "Cannot configure the audio codec", e); |
| } |
| return false; |
| } |
| |
| @SuppressWarnings("deprecation") |
| private int getAudioFormat(int channelCount) { |
| switch (channelCount) { |
| case 1: |
| return AudioFormat.CHANNEL_OUT_MONO; |
| case 2: |
| return AudioFormat.CHANNEL_OUT_STEREO; |
| case 4: |
| return AudioFormat.CHANNEL_OUT_QUAD; |
| case 6: |
| return AudioFormat.CHANNEL_OUT_5POINT1; |
| case 8: |
| if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { |
| return AudioFormat.CHANNEL_OUT_7POINT1_SURROUND; |
| } else { |
| return AudioFormat.CHANNEL_OUT_7POINT1; |
| } |
| default: |
| return AudioFormat.CHANNEL_OUT_DEFAULT; |
| } |
| } |
| |
| @CalledByNative |
| private int getMaxInputSize() { |
| return mMaxInputSize; |
| } |
| |
| @CalledByNative |
| private static void createCallbackHandlerForTesting() { |
| if (sCallbackHandlerThread != null) return; |
| |
| sCallbackHandlerThread = new HandlerThread("TestCallbackThread"); |
| sCallbackHandlerThread.start(); |
| sCallbackHandler = new Handler(sCallbackHandlerThread.getLooper()); |
| } |
| |
| @NativeMethods |
| interface Natives { |
| void onBuffersAvailable(long nativeMediaCodecBridge, MediaCodecBridge caller); |
| } |
| } |