/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ package org.mozilla.gecko.media; import android.media.MediaCodec; import android.media.MediaCodecInfo.CodecCapabilities; import android.media.MediaCrypto; import android.media.MediaFormat; import android.os.Bundle; import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; import android.os.Message; import android.util.Log; import android.view.Surface; import java.io.IOException; import java.nio.ByteBuffer; import org.mozilla.gecko.util.HardwareCodecCapabilityUtils; // Implement async API using MediaCodec sync mode (API v16). // This class uses internal worker thread/handler (mBufferPoller) to poll // input and output buffer and notifies the client through callbacks. final class JellyBeanAsyncCodec implements AsyncCodec { private static final String LOGTAG = "GeckoAsyncCodecAPIv16"; private static final boolean DEBUG = false; private static final int ERROR_CODEC = -10000; private abstract class CancelableHandler extends Handler { private static final int MSG_CANCELLATION = 0x434E434C; // 'CNCL' protected CancelableHandler(final Looper looper) { super(looper); } protected void cancel() { removeCallbacksAndMessages(null); sendEmptyMessage(MSG_CANCELLATION); // Wait until handleMessageLocked() is done. synchronized (this) { } } protected boolean isCanceled() { return hasMessages(MSG_CANCELLATION); } // Subclass should implement this and return true if it handles msg. // Warning: Never, ever call super.handleMessage() in this method! protected abstract boolean handleMessageLocked(Message msg); public final void handleMessage(final Message msg) { // Block cancel() during handleMessageLocked(). synchronized (this) { if (isCanceled() || handleMessageLocked(msg)) { return; } } switch (msg.what) { case MSG_CANCELLATION: // Just a marker. Nothing to do here. if (DEBUG) { Log.d( LOGTAG, "handler " + this + " done cancellation, codec=" + JellyBeanAsyncCodec.this); } break; default: super.handleMessage(msg); break; } } } // A handler to invoke AsyncCodec.Callbacks methods. private final class CallbackSender extends CancelableHandler { private static final int MSG_INPUT_BUFFER_AVAILABLE = 1; private static final int MSG_OUTPUT_BUFFER_AVAILABLE = 2; private static final int MSG_OUTPUT_FORMAT_CHANGE = 3; private static final int MSG_ERROR = 4; private Callbacks mCallbacks; private CallbackSender(final Looper looper, final Callbacks callbacks) { super(looper); mCallbacks = callbacks; } public void notifyInputBuffer(final int index) { if (isCanceled()) { return; } final Message msg = obtainMessage(MSG_INPUT_BUFFER_AVAILABLE); msg.arg1 = index; processMessage(msg); } private void processMessage(final Message msg) { if (Looper.myLooper() == getLooper()) { handleMessage(msg); } else { sendMessage(msg); } } public void notifyOutputBuffer(final int index, final MediaCodec.BufferInfo info) { if (isCanceled()) { return; } final Message msg = obtainMessage(MSG_OUTPUT_BUFFER_AVAILABLE, info); msg.arg1 = index; processMessage(msg); } public void notifyOutputFormat(final MediaFormat format) { if (isCanceled()) { return; } processMessage(obtainMessage(MSG_OUTPUT_FORMAT_CHANGE, format)); } public void notifyError(final int result) { Log.e(LOGTAG, "codec error:" + result); processMessage(obtainMessage(MSG_ERROR, result, 0)); } protected boolean handleMessageLocked(final Message msg) { switch (msg.what) { case MSG_INPUT_BUFFER_AVAILABLE: // arg1: buffer index. mCallbacks.onInputBufferAvailable(JellyBeanAsyncCodec.this, msg.arg1); break; case MSG_OUTPUT_BUFFER_AVAILABLE: // arg1: buffer index, obj: info. mCallbacks.onOutputBufferAvailable( JellyBeanAsyncCodec.this, msg.arg1, (MediaCodec.BufferInfo) msg.obj); break; case MSG_OUTPUT_FORMAT_CHANGE: // obj: output format. mCallbacks.onOutputFormatChanged(JellyBeanAsyncCodec.this, (MediaFormat) msg.obj); break; case MSG_ERROR: // arg1: error code. mCallbacks.onError(JellyBeanAsyncCodec.this, msg.arg1); break; default: return false; } return true; } } // Handler to poll input and output buffers using dequeue(Input|Output)Buffer(), // with 10ms time-out. Once triggered and successfully gets a buffer, it // will schedule next polling until EOS or failure. To prevent it from // automatically polling more buffer, use cancel() it inherits from // CancelableHandler. private final class BufferPoller extends CancelableHandler { private static final int MSG_POLL_INPUT_BUFFERS = 1; private static final int MSG_POLL_OUTPUT_BUFFERS = 2; private static final long DEQUEUE_TIMEOUT_US = 10000; public BufferPoller(final Looper looper) { super(looper); } private void schedulePollingIfNotCanceled(final int what) { if (isCanceled()) { return; } schedulePolling(what); } private void schedulePolling(final int what) { if (needsBuffer(what)) { sendEmptyMessage(what); } } private boolean needsBuffer(final int what) { if (mOutputEnded && (what == MSG_POLL_OUTPUT_BUFFERS)) { return false; } return !(mInputEnded && (what == MSG_POLL_INPUT_BUFFERS)); } protected boolean handleMessageLocked(final Message msg) { try { switch (msg.what) { case MSG_POLL_INPUT_BUFFERS: pollInputBuffer(); break; case MSG_POLL_OUTPUT_BUFFERS: pollOutputBuffer(); break; default: return false; } } catch (final IllegalStateException e) { e.printStackTrace(); mCallbackSender.notifyError(ERROR_CODEC); } return true; } private void pollInputBuffer() { final int result = mCodec.dequeueInputBuffer(DEQUEUE_TIMEOUT_US); if (result >= 0) { mCallbackSender.notifyInputBuffer(result); } else if (result == MediaCodec.INFO_TRY_AGAIN_LATER) { mBufferPoller.schedulePollingIfNotCanceled(BufferPoller.MSG_POLL_INPUT_BUFFERS); } else { mCallbackSender.notifyError(result); } } private void pollOutputBuffer() { boolean dequeueMoreBuffer = true; final MediaCodec.BufferInfo info = new MediaCodec.BufferInfo(); final int result = mCodec.dequeueOutputBuffer(info, DEQUEUE_TIMEOUT_US); if (result >= 0) { if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) { mOutputEnded = true; } mCallbackSender.notifyOutputBuffer(result, info); } else if (result == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) { mOutputBuffers = mCodec.getOutputBuffers(); } else if (result == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { mOutputBuffers = mCodec.getOutputBuffers(); mCallbackSender.notifyOutputFormat(mCodec.getOutputFormat()); } else if (result == MediaCodec.INFO_TRY_AGAIN_LATER) { // When input ended, keep polling remaining output buffer until EOS. dequeueMoreBuffer = mInputEnded; } else { mCallbackSender.notifyError(result); dequeueMoreBuffer = false; } if (dequeueMoreBuffer) { schedulePollingIfNotCanceled(MSG_POLL_OUTPUT_BUFFERS); } } } private MediaCodec mCodec; private ByteBuffer[] mInputBuffers; private ByteBuffer[] mOutputBuffers; private AsyncCodec.Callbacks mCallbacks; private CallbackSender mCallbackSender; private BufferPoller mBufferPoller; private volatile boolean mInputEnded; private volatile boolean mOutputEnded; // Must be called on a thread with looper. /* package */ JellyBeanAsyncCodec(final String name) throws IOException { mCodec = MediaCodec.createByCodecName(name); initBufferPoller(name + " buffer poller"); } private void initBufferPoller(final String name) { if (mBufferPoller != null) { Log.e(LOGTAG, "poller already initialized"); return; } final HandlerThread thread = new HandlerThread(name); thread.start(); mBufferPoller = new BufferPoller(thread.getLooper()); if (DEBUG) { Log.d(LOGTAG, "start poller for codec:" + this + ", thread=" + thread.getThreadId()); } } @Override public void setCallbacks(final AsyncCodec.Callbacks callbacks, final Handler handler) { if (callbacks == null) { return; } Looper looper = (handler == null) ? null : handler.getLooper(); if (looper == null) { // Use this thread if no handler supplied. looper = Looper.myLooper(); } if (looper == null) { // This thread has no looper. Use poller thread. looper = mBufferPoller.getLooper(); } mCallbackSender = new CallbackSender(looper, callbacks); if (DEBUG) { Log.d(LOGTAG, "setCallbacks(): sender=" + mCallbackSender); } } @Override public void configure( final MediaFormat format, final Surface surface, final MediaCrypto crypto, final int flags) { assertCallbacks(); mCodec.configure(format, surface, crypto, flags); } @Override public boolean isAdaptivePlaybackSupported(final String mimeType) { return HardwareCodecCapabilityUtils.checkSupportsAdaptivePlayback(mCodec, mimeType); } @Override public boolean isTunneledPlaybackSupported(final String mimeType) { try { return mCodec .getCodecInfo() .getCapabilitiesForType(mimeType) .isFeatureSupported(CodecCapabilities.FEATURE_TunneledPlayback); } catch (final Exception e) { return false; } } private void assertCallbacks() { if (mCallbackSender == null) { throw new IllegalStateException(LOGTAG + ": callback must be supplied with setCallbacks()."); } } @Override public void start() { assertCallbacks(); mCodec.start(); mInputEnded = false; mOutputEnded = false; mInputBuffers = mCodec.getInputBuffers(); resumeReceivingInputs(); mOutputBuffers = mCodec.getOutputBuffers(); } @Override public void resumeReceivingInputs() { for (int i = 0; i < mInputBuffers.length; i++) { mBufferPoller.schedulePolling(BufferPoller.MSG_POLL_INPUT_BUFFERS); } } @Override public final void setBitrate(final int bps) { final Bundle params = new Bundle(); params.putInt(MediaCodec.PARAMETER_KEY_VIDEO_BITRATE, bps); mCodec.setParameters(params); } @Override public final void queueInputBuffer( final int index, final int offset, final int size, final long presentationTimeUs, final int flags) { assertCallbacks(); mInputEnded = (flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0; if (((flags & MediaCodec.BUFFER_FLAG_KEY_FRAME) != 0)) { final Bundle params = new Bundle(); params.putInt(MediaCodec.PARAMETER_KEY_REQUEST_SYNC_FRAME, 0); mCodec.setParameters(params); } try { mCodec.queueInputBuffer(index, offset, size, presentationTimeUs, flags); } catch (final IllegalStateException e) { e.printStackTrace(); mCallbackSender.notifyError(ERROR_CODEC); return; } mBufferPoller.schedulePolling(BufferPoller.MSG_POLL_OUTPUT_BUFFERS); mBufferPoller.schedulePolling(BufferPoller.MSG_POLL_INPUT_BUFFERS); } @Override public final void queueSecureInputBuffer( final int index, final int offset, final MediaCodec.CryptoInfo cryptoInfo, final long presentationTimeUs, final int flags) { assertCallbacks(); mInputEnded = (flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0; try { mCodec.queueSecureInputBuffer(index, offset, cryptoInfo, presentationTimeUs, flags); } catch (final IllegalStateException e) { e.printStackTrace(); mCallbackSender.notifyError(ERROR_CODEC); return; } mBufferPoller.schedulePolling(BufferPoller.MSG_POLL_INPUT_BUFFERS); mBufferPoller.schedulePolling(BufferPoller.MSG_POLL_OUTPUT_BUFFERS); } @Override public final void releaseOutputBuffer(final int index, final boolean render) { assertCallbacks(); mCodec.releaseOutputBuffer(index, render); } @Override public final ByteBuffer getInputBuffer(final int index) { assertCallbacks(); return mInputBuffers[index]; } @Override public final ByteBuffer getOutputBuffer(final int index) { assertCallbacks(); return mOutputBuffers[index]; } @Override public MediaFormat getInputFormat() { return null; } @Override public void flush() { assertCallbacks(); mInputEnded = false; mOutputEnded = false; cancelPendingTasks(); mCodec.flush(); } private void cancelPendingTasks() { mBufferPoller.cancel(); mCallbackSender.cancel(); } @Override public void stop() { assertCallbacks(); cancelPendingTasks(); mCodec.stop(); } @Override public void release() { assertCallbacks(); cancelPendingTasks(); mCallbackSender = null; mCodec.release(); stopBufferPoller(); } private void stopBufferPoller() { if (mBufferPoller == null) { Log.e(LOGTAG, "no initialized poller."); return; } mBufferPoller.getLooper().quit(); mBufferPoller = null; if (DEBUG) { Log.d(LOGTAG, "stop poller " + this); } } }