Bug 1257777 - Part 3: Implement remote codec, manager service, and parcelables. r=jchen
authorJohn Lin <jolin@mozilla.com>
Fri, 05 Aug 2016 15:18:52 +0800
changeset 351000 8c1fffb47bc81cd1363cffdd5e9efa1aa0aea856
parent 350999 1d86193c7b9a2e268fbb70f4b92461f1b5c7283c
child 351001 ec903c5415a243a8a6cbef2b528e4569d80d2ceb
push id6570
push userraliiev@mozilla.com
push dateMon, 14 Nov 2016 12:26:13 +0000
treeherdermozilla-beta@f455459b2ae5 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjchen
bugs1257777
milestone51.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1257777 - Part 3: Implement remote codec, manager service, and parcelables. r=jchen MozReview-Commit-ID: L0bc0wUaQKQ
mobile/android/base/AndroidManifest.xml.in
mobile/android/base/java/org/mozilla/gecko/media/Codec.java
mobile/android/base/java/org/mozilla/gecko/media/CodecManager.java
mobile/android/base/java/org/mozilla/gecko/media/FormatParam.java
mobile/android/base/java/org/mozilla/gecko/media/Sample.java
mobile/android/base/moz.build
--- 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,292 @@
+/* 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 LOGTAG = "GeckoRemoteCodec";
+    private static final boolean DEBUG = false;
+
+    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(LOGTAG, "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 (DEBUG && eos) {
+                Log.d(LOGTAG, "output EOS");
+            }
+        }
+
+        private void outputDummy(MediaCodec.BufferInfo info) {
+            try {
+                if (DEBUG) Log.d(LOGTAG, "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(LOGTAG, "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(LOGTAG, "FAIL: callbacks must be set before calling configure()");
+            return false;
+        }
+
+        if (mCodec != null) {
+            if (DEBUG) Log.d(LOGTAG, "release existing codec: " + mCodec);
+            releaseCodec();
+        }
+
+        if (DEBUG) Log.d(LOGTAG, "configure " + this);
+
+        MediaFormat fmt = format.asFormat();
+        String codecName = getDecoderForFormat(fmt);
+        if (codecName == null) {
+            Log.e(LOGTAG, "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();
+            if (DEBUG) Log.d(LOGTAG, codec.toString() + " created");
+            return true;
+        } catch (Exception e) {
+            if (DEBUG) Log.d(LOGTAG, "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 {
+        if (DEBUG) Log.d(LOGTAG, "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 {
+        if (DEBUG) Log.d(LOGTAG, "stop " + this);
+        try {
+            mCodec.stop();
+        } catch (Exception e) {
+            reportError(Error.FATAL, e);
+        }
+    }
+
+    @Override
+    public synchronized void flush() throws RemoteException {
+        mFlushing = true;
+        if (DEBUG) Log.d(LOGTAG, "flush " + this);
+        mInputProcessor.reset();
+        try {
+            mCodec.flush();
+        } catch (Exception e) {
+            reportError(Error.FATAL, e);
+        }
+
+        mFlushing = false;
+        if (DEBUG) Log.d(LOGTAG, "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 {
+        if (DEBUG) Log.d(LOGTAG, "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 = mFormat.getByteBuffer(KEY_CONFIG_0);
+            bundle.putByteArray(KEY_CONFIG_0,
+                Sample.byteArrayFromBuffer(bytes, 0, bytes.capacity()));
+        }
+        if (mFormat.containsKey(KEY_CONFIG_1)) {
+            ByteBuffer bytes = mFormat.getByteBuffer(KEY_CONFIG_1);
+            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,118 @@
+/* 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.position(offset);
+        buffer.get(bytes);
+        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',