summaryrefslogtreecommitdiffstats
path: root/mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/JellyBeanAsyncCodec.java
diff options
context:
space:
mode:
Diffstat (limited to 'mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/JellyBeanAsyncCodec.java')
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/JellyBeanAsyncCodec.java490
1 files changed, 490 insertions, 0 deletions
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/JellyBeanAsyncCodec.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/JellyBeanAsyncCodec.java
new file mode 100644
index 0000000000..7c5102c63d
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/JellyBeanAsyncCodec.java
@@ -0,0 +1,490 @@
+/* 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.Build;
+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;
+ }
+
+ if (mInputEnded && (what == MSG_POLL_INPUT_BUFFERS)) {
+ return false;
+ }
+
+ return true;
+ }
+
+ 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 android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP
+ && 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) {
+ if (android.os.Build.VERSION.SDK_INT >= 19) {
+ 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 (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT
+ && ((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);
+ }
+ }
+}