Bug 1257777 - Part 3: Implement remote codec, manager service, and parcelables. r?jchen
MozReview-Commit-ID: L0bc0wUaQKQ
--- a/mobile/android/base/AndroidManifest.xml.in
+++ b/mobile/android/base/AndroidManifest.xml.in
@@ -415,10 +415,18 @@
#ifdef MOZ_ANDROID_MLS_STUMBLER
#include ../stumbler/manifests/StumblerManifest_services.xml.in
#endif
#ifdef MOZ_ANDROID_GCM
#include GcmAndroidManifest_services.xml.in
#endif
+ <service
+ android:name="org.mozilla.gecko.media.CodecManager"
+ android:enabled="true"
+ android:exported="false"
+ android:process=":media"
+ android:isolatedProcess="false">
+ </service>
+
</application>
</manifest>
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/media/Codec.java
@@ -0,0 +1,291 @@
+/* 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;
+import android.media.MediaCodecList;
+import android.media.MediaFormat;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.os.TransactionTooLargeException;
+import android.util.Log;
+import android.view.Surface;
+
+import java.nio.ByteBuffer;
+import java.util.LinkedList;
+import java.util.Queue;
+
+/* package */ final class Codec extends ICodec.Stub implements IBinder.DeathRecipient {
+ private static final String LOG_TAG = Codec.class.getSimpleName();
+
+ public enum Error {
+ DECODE, FATAL
+ };
+
+ private final class Callbacks implements AsyncCodec.Callbacks {
+ private ICodecCallbacks mRemote;
+
+ public Callbacks(ICodecCallbacks remote) {
+ mRemote = remote;
+ }
+
+ @Override
+ public void onInputBufferAvailable(AsyncCodec codec, int index) {
+ if (mFlushing) {
+ // Flush invalidates all buffers.
+ return;
+ }
+ if (!mInputProcessor.onBuffer(index)) {
+ reportError(Error.FATAL, new Exception("FAIL: input buffer queue is full"));
+ }
+ }
+
+ @Override
+ public void onOutputBufferAvailable(AsyncCodec codec, int index, MediaCodec.BufferInfo info) {
+ if (mFlushing) {
+ // Flush invalidates all buffers.
+ return;
+ }
+ ByteBuffer buffer = codec.getOutputBuffer(index);
+ try {
+ mRemote.onOutput(new Sample(buffer, info));
+ } catch (TransactionTooLargeException ttle) {
+ Log.e(LOG_TAG, "Output is too large:" + ttle.getMessage());
+ outputDummy(info);
+ } catch (RemoteException e) {
+ // Dead recipient.
+ e.printStackTrace();
+ }
+ mCodec.releaseOutputBuffer(index, true);
+ boolean eos = (info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0;
+ if (eos) {
+ Log.d(LOG_TAG, "output EOS");
+ }
+ }
+
+ private void outputDummy(MediaCodec.BufferInfo info) {
+ try {
+ Log.d(LOG_TAG, "return dummy sample");
+ mRemote.onOutput(Sample.createDummyWithInfo(info));
+ } catch (RemoteException e) {
+ // Dead recipient.
+ e.printStackTrace();
+ }
+ }
+
+ @Override
+ public void onError(AsyncCodec codec, int error) {
+ reportError(Error.FATAL, new Exception("codec error:" + error));
+ }
+
+ @Override
+ public void onOutputFormatChanged(AsyncCodec codec, MediaFormat format) {
+ try {
+ mRemote.onOutputFormatChanged(new FormatParam(format));
+ } catch (RemoteException re) {
+ // Dead recipient.
+ re.printStackTrace();
+ }
+ }
+ }
+
+ private final class InputProcessor {
+ private Queue<Sample> mInputSamples = new LinkedList<Sample>();
+ private Queue<Integer> mAvailableInputBuffers = new LinkedList<Integer>();
+
+ private synchronized boolean onSample(Sample sample) {
+ if (!mInputSamples.offer(sample)) {
+ return false;
+ }
+ feedSampleToBuffer();
+ return true;
+ }
+
+ private synchronized boolean onBuffer(int index) {
+ if (!mAvailableInputBuffers.offer(index)) {
+ return false;
+ }
+ feedSampleToBuffer();
+ return true;
+ }
+
+ private void feedSampleToBuffer() {
+ while (!mAvailableInputBuffers.isEmpty() && !mInputSamples.isEmpty()) {
+ int index = mAvailableInputBuffers.poll();
+ Sample sample = mInputSamples.poll();
+ int len = 0;
+ if (!sample.isEOS() && sample.bytes != null) {
+ len = sample.info.size;
+ ByteBuffer buf = mCodec.getInputBuffer(index);
+ buf.put(sample.bytes);
+ try {
+ mCallbacks.onInputExhausted();
+ } catch (RemoteException e) {
+ e.printStackTrace();
+ }
+ }
+ mCodec.queueInputBuffer(index, 0, len, sample.info.presentationTimeUs, sample.info.flags);
+ }
+ }
+
+ private synchronized void reset() {
+ mInputSamples.clear();
+ mAvailableInputBuffers.clear();
+ }
+ }
+ private volatile ICodecCallbacks mCallbacks;
+ private AsyncCodec mCodec;
+ private InputProcessor mInputProcessor;
+ private volatile boolean mFlushing = false;
+
+ public synchronized void setCallbacks(ICodecCallbacks callbacks) throws RemoteException {
+ mCallbacks = callbacks;
+ callbacks.asBinder().linkToDeath(this, 0);
+ }
+
+ // IBinder.DeathRecipient
+ @Override
+ public synchronized void binderDied() {
+ Log.e(LOG_TAG, "Callbacks is dead");
+ try {
+ release();
+ } catch (RemoteException e) {
+ // Nowhere to report the error.
+ }
+ }
+
+ @Override
+ public synchronized boolean configure(FormatParam format, Surface surface, int flags) throws RemoteException {
+ if (mCallbacks == null) {
+ Log.e(LOG_TAG, "FAIL: callbacks must be set before calling configure()");
+ return false;
+ }
+
+ if (mCodec != null) {
+ Log.d(LOG_TAG, "release existing codec: " + mCodec);
+ releaseCodec();
+ }
+
+ Log.d(LOG_TAG, "configure " + this);
+
+ MediaFormat fmt = format.asFormat();
+ String codecName = getDecoderForFormat(fmt);
+ if (codecName == null) {
+ Log.e(LOG_TAG, "FAIL: cannot find codec");
+ return false;
+ }
+
+ try {
+ AsyncCodec codec = AsyncCodecFactory.create(codecName);
+ codec.setCallbacks(new Callbacks(mCallbacks), null);
+ codec.configure(fmt, surface, flags);
+ mCodec = codec;
+ mInputProcessor = new InputProcessor();
+ Log.d(LOG_TAG, codec.toString() + " created");
+ return true;
+ } catch (Exception e) {
+ Log.d(LOG_TAG, "FAIL: cannot create codec -- " + codecName);
+ e.printStackTrace();
+ return false;
+ }
+ }
+
+ private void releaseCodec() {
+ mInputProcessor.reset();
+ try {
+ mCodec.release();
+ } catch (Exception e) {
+ reportError(Error.FATAL, e);
+ }
+ mCodec = null;
+ }
+
+ private String getDecoderForFormat(MediaFormat format) {
+ String mime = format.getString(MediaFormat.KEY_MIME);
+ if (mime == null) {
+ return null;
+ }
+ int numCodecs = MediaCodecList.getCodecCount();
+ for (int i = 0; i < numCodecs; i++) {
+ MediaCodecInfo info = MediaCodecList.getCodecInfoAt(i);
+ if (info.isEncoder()) {
+ continue;
+ }
+ String[] types = info.getSupportedTypes();
+ for (String t : types) {
+ if (t.equalsIgnoreCase(mime)) {
+ return info.getName();
+ }
+ }
+ }
+ return null;
+ // TODO: API 21+ is simpler.
+ //static MediaCodecList sCodecList = new MediaCodecList(MediaCodecList.ALL_CODECS);
+ //return sCodecList.findDecoderForFormat(format);
+ }
+
+ @Override
+ public synchronized void start() throws RemoteException {
+ Log.d(LOG_TAG, "start " + this);
+ mFlushing = false;
+ try {
+ mCodec.start();
+ } catch (Exception e) {
+ reportError(Error.FATAL, e);
+ }
+ }
+
+ private void reportError(Error error, Exception e) {
+ if (e != null) {
+ e.printStackTrace();
+ }
+ try {
+ mCallbacks.onError(error == Error.FATAL);
+ } catch (RemoteException re) {
+ re.printStackTrace();
+ }
+ }
+
+ @Override
+ public synchronized void stop() throws RemoteException {
+ Log.d(LOG_TAG, "stop " + this);
+ try {
+ mCodec.stop();
+ } catch (Exception e) {
+ reportError(Error.FATAL, e);
+ }
+ }
+
+ @Override
+ public synchronized void flush() throws RemoteException {
+ mFlushing = true;
+ Log.d(LOG_TAG, "flush " + this);
+ mInputProcessor.reset();
+ try {
+ mCodec.flush();
+ } catch (Exception e) {
+ reportError(Error.FATAL, e);
+ }
+
+ mFlushing = false;
+ Log.d(LOG_TAG, "flushed " + this);
+ }
+
+ @Override
+ public synchronized void queueInput(Sample sample) throws RemoteException {
+ if (!mInputProcessor.onSample(sample)) {
+ reportError(Error.FATAL, new Exception("FAIL: input sample queue is full"));
+ }
+ }
+
+ @Override
+ public synchronized void release() throws RemoteException {
+ Log.d(LOG_TAG, "release " + this);
+ releaseCodec();
+ mCallbacks.asBinder().unlinkToDeath(this, 0);
+ mCallbacks = null;
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/media/CodecManager.java
@@ -0,0 +1,25 @@
+/* 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.app.Service;
+import android.content.Intent;
+import android.os.Binder;
+import android.os.IBinder;
+import android.os.RemoteException;
+
+public final class CodecManager extends Service {
+ private Binder mBinder = new ICodecManager.Stub() {
+ @Override
+ public ICodec createCodec() throws RemoteException {
+ return new Codec();
+ }
+ };
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return mBinder;
+ }
+}
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/media/FormatParam.java
@@ -0,0 +1,133 @@
+/* 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.MediaFormat;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.nio.ByteBuffer;
+
+/** A wrapper to make {@link MediaFormat} parcelable.
+ * Supports following keys:
+ * <ul>
+ * <li>{@link MediaFormat#KEY_MIME}</li>
+ * <li>{@link MediaFormat#KEY_WIDTH}</li>
+ * <li>{@link MediaFormat#KEY_HEIGHT}</li>
+ * <li>{@link MediaFormat#KEY_CHANNEL_COUNT}</li>
+ * <li>{@link MediaFormat#KEY_SAMPLE_RATE}</li>
+ * <li>"csd-0"</li>
+ * <li>"csd-1"</li>
+ * </ul>
+ */
+public final class FormatParam implements Parcelable {
+ // Keys for codec specific config bits not exposed in {@link MediaFormat}.
+ private static final String KEY_CONFIG_0 = "csd-0";
+ private static final String KEY_CONFIG_1 = "csd-1";
+
+ private MediaFormat mFormat;
+
+ public MediaFormat asFormat() {
+ return mFormat;
+ }
+
+ public FormatParam(MediaFormat format) {
+ mFormat = format;
+ }
+
+ protected FormatParam(Parcel in) {
+ mFormat = new MediaFormat();
+ readFromParcel(in);
+ }
+
+ public static final Creator<FormatParam> CREATOR = new Creator<FormatParam>() {
+ @Override
+ public FormatParam createFromParcel(Parcel in) {
+ return new FormatParam(in);
+ }
+
+ @Override
+ public FormatParam[] newArray(int size) {
+ return new FormatParam[size];
+ }
+ };
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ public void readFromParcel(Parcel in) {
+ Bundle bundle = in.readBundle();
+ fromBundle(bundle);
+ }
+
+ private void fromBundle(Bundle bundle) {
+ if (bundle.containsKey(MediaFormat.KEY_MIME)) {
+ mFormat.setString(MediaFormat.KEY_MIME,
+ bundle.getString(MediaFormat.KEY_MIME));
+ }
+ if (bundle.containsKey(MediaFormat.KEY_WIDTH)) {
+ mFormat.setInteger(MediaFormat.KEY_WIDTH,
+ bundle.getInt(MediaFormat.KEY_WIDTH));
+ }
+ if (bundle.containsKey(MediaFormat.KEY_HEIGHT)) {
+ mFormat.setInteger(MediaFormat.KEY_HEIGHT,
+ bundle.getInt(MediaFormat.KEY_HEIGHT));
+ }
+ if (bundle.containsKey(MediaFormat.KEY_CHANNEL_COUNT)) {
+ mFormat.setInteger(MediaFormat.KEY_CHANNEL_COUNT,
+ bundle.getInt(MediaFormat.KEY_CHANNEL_COUNT));
+ }
+ if (bundle.containsKey(MediaFormat.KEY_SAMPLE_RATE)) {
+ mFormat.setInteger(MediaFormat.KEY_SAMPLE_RATE,
+ bundle.getInt(MediaFormat.KEY_SAMPLE_RATE));
+ }
+ if (bundle.containsKey(KEY_CONFIG_0)) {
+ mFormat.setByteBuffer(KEY_CONFIG_0,
+ ByteBuffer.wrap(bundle.getByteArray(KEY_CONFIG_0)));
+ }
+ if (bundle.containsKey(KEY_CONFIG_1)) {
+ mFormat.setByteBuffer(KEY_CONFIG_1,
+ ByteBuffer.wrap(bundle.getByteArray((KEY_CONFIG_1))));
+ }
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeBundle(toBundle());
+ }
+
+ private Bundle toBundle() {
+ Bundle bundle = new Bundle();
+ if (mFormat.containsKey(MediaFormat.KEY_MIME)) {
+ bundle.putString(MediaFormat.KEY_MIME, mFormat.getString(MediaFormat.KEY_MIME));
+ }
+ if (mFormat.containsKey(MediaFormat.KEY_WIDTH)) {
+ bundle.putInt(MediaFormat.KEY_WIDTH, mFormat.getInteger(MediaFormat.KEY_WIDTH));
+ }
+ if (mFormat.containsKey(MediaFormat.KEY_HEIGHT)) {
+ bundle.putInt(MediaFormat.KEY_HEIGHT, mFormat.getInteger(MediaFormat.KEY_HEIGHT));
+ }
+ if (mFormat.containsKey(MediaFormat.KEY_CHANNEL_COUNT)) {
+ bundle.putInt(MediaFormat.KEY_CHANNEL_COUNT, mFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT));
+ }
+ if (mFormat.containsKey(MediaFormat.KEY_SAMPLE_RATE)) {
+ bundle.putInt(MediaFormat.KEY_SAMPLE_RATE, mFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE));
+ }
+ if (mFormat.containsKey(KEY_CONFIG_0)) {
+ ByteBuffer bytes = (ByteBuffer)mFormat.getByteBuffer(KEY_CONFIG_0).rewind();
+ bundle.putByteArray(KEY_CONFIG_0,
+ Sample.byteArrayFromBuffer(bytes, 0, bytes.capacity()));
+ }
+ if (mFormat.containsKey(KEY_CONFIG_1)) {
+ ByteBuffer bytes = (ByteBuffer)mFormat.getByteBuffer(KEY_CONFIG_1).rewind();
+ bundle.putByteArray(KEY_CONFIG_1,
+ Sample.byteArrayFromBuffer(bytes, 0, bytes.capacity()));
+ }
+ return bundle;
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/media/Sample.java
@@ -0,0 +1,117 @@
+/* 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.MediaCodec.BufferInfo;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.nio.ByteBuffer;
+
+// POD carrying input sample data and info cross process.
+public final class Sample implements Parcelable {
+ public static final Sample EOS;
+ static {
+ BufferInfo eosInfo = new BufferInfo();
+ eosInfo.set(0, 0, Long.MIN_VALUE, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
+ EOS = new Sample(null, eosInfo);
+ }
+
+ public BufferInfo info;
+ public ByteBuffer bytes;
+
+ public Sample(ByteBuffer bytes, BufferInfo info) {
+ this.info = info;
+ this.bytes = bytes;
+ }
+
+ protected Sample(Parcel in) {
+ readFromParcel(in);
+ }
+
+ public static Sample createDummyWithInfo(BufferInfo info) {
+ BufferInfo dummyInfo = new BufferInfo();
+ dummyInfo.set(0, 0, info.presentationTimeUs, info.flags);
+ return new Sample(null, dummyInfo);
+ }
+
+ public boolean isDummy() {
+ return bytes == null && info.size == 0;
+ }
+
+ public boolean isEOS() {
+ return (info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0;
+ }
+
+ public static final Creator<Sample> CREATOR = new Creator<Sample>() {
+ @Override
+ public Sample createFromParcel(Parcel in) {
+ return new Sample(in);
+ }
+
+ @Override
+ public Sample[] newArray(int size) {
+ return new Sample[size];
+ }
+ };
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ public void readFromParcel(Parcel in) {
+ long pts = in.readLong();
+ int flags = in.readInt();
+ int size = 0;
+ byte[] buf = in.createByteArray();
+ if (buf != null) {
+ bytes = ByteBuffer.wrap(buf);
+ size = buf.length;
+ }
+ info = new BufferInfo();
+ info.set(0, size, pts, flags);
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int parcelableFlags) {
+ dest.writeLong(info.presentationTimeUs);
+ dest.writeInt(info.flags);
+ dest.writeByteArray(byteArrayFromBuffer(bytes, info.offset, info.size));
+ }
+
+ public static byte[] byteArrayFromBuffer(ByteBuffer buffer, int offset, int size) {
+ if (buffer == null || buffer.capacity() == 0 || size == 0) {
+ return null;
+ }
+ if (buffer.hasArray() && offset == 0 && buffer.array().length == size) {
+ return buffer.array();
+ }
+ int length = Math.min(offset + size, buffer.capacity()) - offset;
+ byte[] bytes = new byte[length];
+ buffer.get(bytes, offset, length);
+ return bytes;
+ }
+
+ public byte[] getBytes() {
+ return byteArrayFromBuffer(bytes, info.offset, info.size);
+ }
+
+ @Override
+ public String toString() {
+ if (isEOS()) {
+ return "EOS sample";
+ } else {
+ StringBuilder str = new StringBuilder();
+ str.append("{ pts=").append(info.presentationTimeUs);
+ if (bytes != null) {
+ str.append(", size=").append(info.size);
+ }
+ str.append(", flags=").append(Integer.toHexString(info.flags)).append(" }");
+ return str.toString();
+ }
+ }
+}
\ No newline at end of file
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -502,18 +502,22 @@ gbjar.sources += ['java/org/mozilla/geck
'javaaddons/JavaAddonManagerV1.java',
'LauncherActivity.java',
'lwt/LightweightTheme.java',
'lwt/LightweightThemeDrawable.java',
'mdns/MulticastDNSManager.java',
'media/AsyncCodec.java',
'media/AsyncCodecFactory.java',
'media/AudioFocusAgent.java',
+ 'media/Codec.java',
+ 'media/CodecManager.java',
+ 'media/FormatParam.java',
'media/JellyBeanAsyncCodec.java',
'media/MediaControlService.java',
+ 'media/Sample.java',
'MediaCastingBar.java',
'MemoryMonitor.java',
'menu/GeckoMenu.java',
'menu/GeckoMenuInflater.java',
'menu/GeckoMenuItem.java',
'menu/GeckoSubMenu.java',
'menu/MenuItemActionBar.java',
'menu/MenuItemDefault.java',