Bug 1257777 - Part 3: Implement remote codec, manager service, and parcelables. r?jchen draft
authorJohn Lin <jolin@mozilla.com>
Fri, 05 Aug 2016 15:18:52 +0800
changeset 397136 22dd4f1e65b3782134a99d609291abcc4c05299f
parent 397135 0f571c42d1485f897fc5f2da119a19af46bec851
child 397137 c870db18f77b121f89ea4739c193a68cf58db752
push id25213
push userbmo:jolin@mozilla.com
push dateFri, 05 Aug 2016 08:21:05 +0000
reviewersjchen
bugs1257777
milestone51.0a1
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,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',