Bug 1317628-[P2] Handling key-needed/key-added/notifying for encrypted media playback with Fennec out-of-process-decode mode on Android L. draft
authorKilik Kuo <kikuo@mozilla.com>
Sat, 10 Dec 2016 10:29:13 +0800
changeset 448378 7a9ed43005aa4d0f7157257851a543f17c915e53
parent 448323 6e138d67bb08d5af84dd1346965a10eb8fa89156
child 539291 95407983f8878241f9b178d571ad7e803905acbe
push id38344
push userbmo:kikuo@mozilla.com
push dateSat, 10 Dec 2016 02:30:13 +0000
bugs1317628
milestone53.0a1
Bug 1317628-[P2] Handling key-needed/key-added/notifying for encrypted media playback with Fennec out-of-process-decode mode on Android L. MozReview-Commit-ID: EXOng1Blb5M
mobile/android/base/java/org/mozilla/gecko/media/AsyncCodec.java
mobile/android/base/java/org/mozilla/gecko/media/Codec.java
mobile/android/base/java/org/mozilla/gecko/media/GeckoMediaDrm.java
mobile/android/base/java/org/mozilla/gecko/media/GeckoMediaDrmBridgeV21.java
mobile/android/base/java/org/mozilla/gecko/media/GeckoMediaDrmBridgeV23.java
mobile/android/base/java/org/mozilla/gecko/media/JellyBeanAsyncCodec.java
mobile/android/base/java/org/mozilla/gecko/media/LocalMediaDrmBridge.java
mobile/android/base/java/org/mozilla/gecko/media/MediaDrmProxy.java
mobile/android/base/java/org/mozilla/gecko/media/RemoteMediaDrmBridge.java
mobile/android/base/java/org/mozilla/gecko/media/RemoteMediaDrmBridgeStub.java
mobile/android/base/java/org/mozilla/gecko/util/MediaCodecUtil.java
--- a/mobile/android/base/java/org/mozilla/gecko/media/AsyncCodec.java
+++ b/mobile/android/base/java/org/mozilla/gecko/media/AsyncCodec.java
@@ -30,9 +30,12 @@ public interface AsyncCodec {
     public abstract void stop();
     public abstract void flush();
     public abstract void release();
     public abstract ByteBuffer getInputBuffer(int index);
     public abstract ByteBuffer getOutputBuffer(int index);
     public abstract void queueInputBuffer(int index, int offset, int size, long presentationTimeUs, int flags);
     public abstract void queueSecureInputBuffer(int index, int offset, CryptoInfo info, long presentationTimeUs, int flags);
     public abstract void releaseOutputBuffer(int index, boolean render);
+
+    public abstract void setDrmStubId(String drmStubId);
+    public abstract void notifyKeyAdded();
 }
--- a/mobile/android/base/java/org/mozilla/gecko/media/Codec.java
+++ b/mobile/android/base/java/org/mozilla/gecko/media/Codec.java
@@ -1,14 +1,17 @@
 /* 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 org.mozilla.gecko.AppConstants;
+import org.mozilla.gecko.util.MediaCodecUtil;
+
 import android.media.MediaCodec;
 import android.media.MediaCodecInfo;
 import android.media.MediaCodecList;
 import android.media.MediaCrypto;
 import android.media.MediaFormat;
 import android.os.IBinder;
 import android.os.RemoteException;
 import android.os.TransactionTooLargeException;
@@ -25,16 +28,29 @@ import java.util.concurrent.ConcurrentLi
 /* 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
     };
 
+    public final class CodecKeyWaiter implements MediaCodecUtil.KeyWaiter{
+        private AsyncCodec mCodecImpl;
+
+        CodecKeyWaiter(AsyncCodec codec) {
+            mCodecImpl = codec;
+        }
+
+        @Override
+        public void onKeyAdded() {
+            mCodecImpl.notifyKeyAdded();
+        }
+    }
+
     private final class Callbacks implements AsyncCodec.Callbacks {
         private ICodecCallbacks mRemote;
         private boolean mHasInputCapacitySet;
         private boolean mHasOutputCapacitySet;
 
         public Callbacks(ICodecCallbacks remote) {
             mRemote = remote;
         }
@@ -158,16 +174,20 @@ import java.util.concurrent.ConcurrentLi
                 return false;
             }
             feedSampleToBuffer();
             return true;
         }
 
         private void feedSampleToBuffer() {
             while (!mAvailableInputBuffers.isEmpty() && !mInputSamples.isEmpty()) {
+                if (AppConstants.Versions.preMarshmallow &&
+                    RemoteMediaDrmBridgeStub.waitForKey(mDrmStubId)) {
+                    return;
+                }
                 int index = mAvailableInputBuffers.poll();
                 int len = 0;
                 Sample sample = mInputSamples.poll();
                 long pts = sample.info.presentationTimeUs;
                 int flags = sample.info.flags;
                 MediaCodec.CryptoInfo cryptoInfo = sample.cryptoInfo;
                 if (!sample.isEOS() && sample.buffer != null) {
                     len = sample.info.size;
@@ -200,16 +220,17 @@ import java.util.concurrent.ConcurrentLi
     private volatile ICodecCallbacks mCallbacks;
     private AsyncCodec mCodec;
     private InputProcessor mInputProcessor;
     private volatile boolean mFlushing = false;
     private SamplePool mSamplePool;
     private Queue<Sample> mSentOutputs = new ConcurrentLinkedQueue<>();
     // Value will be updated after configure called.
     private volatile boolean mIsAdaptivePlaybackSupported = false;
+    private String mDrmStubId;
 
     public synchronized void setCallbacks(ICodecCallbacks callbacks) throws RemoteException {
         mCallbacks = callbacks;
         callbacks.asBinder().linkToDeath(this, 0);
     }
 
     // IBinder.DeathRecipient
     @Override
@@ -251,33 +272,39 @@ import java.util.concurrent.ConcurrentLi
 
             MediaCrypto crypto = RemoteMediaDrmBridgeStub.getMediaCrypto(drmStubId);
             if (DEBUG) {
                 boolean hasCrypto = crypto != null;
                 Log.d(LOGTAG, "configure mediacodec with crypto(" + hasCrypto + ") / Id :" + drmStubId);
             }
 
             codec.setCallbacks(new Callbacks(mCallbacks), null);
-
             // Video decoder should config with adaptive playback capability.
             if (surface != null) {
                 mIsAdaptivePlaybackSupported = codec.isAdaptivePlaybackSupported(
                                                    fmt.getString(MediaFormat.KEY_MIME));
                 if (mIsAdaptivePlaybackSupported) {
                     if (DEBUG) Log.d(LOGTAG, "codec supports adaptive playback  = " + mIsAdaptivePlaybackSupported);
                     // TODO: may need to find a way to not use hard code to decide the max w/h.
                     fmt.setInteger(MediaFormat.KEY_MAX_WIDTH, 1920);
                     fmt.setInteger(MediaFormat.KEY_MAX_HEIGHT, 1080);
                 }
             }
 
             codec.configure(fmt, surface, crypto, flags);
             mCodec = codec;
             mInputProcessor = new InputProcessor();
             mSamplePool = new SamplePool(codecName);
+
+            if (crypto != null && AppConstants.Versions.preMarshmallow) {
+                mDrmStubId = drmStubId;
+                mCodec.setDrmStubId(drmStubId);
+                RemoteMediaDrmBridgeStub.setupKeyAddedCallback(drmStubId,
+                                                               new CodecKeyWaiter(mCodec));
+            }
             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;
         }
     }
--- a/mobile/android/base/java/org/mozilla/gecko/media/GeckoMediaDrm.java
+++ b/mobile/android/base/java/org/mozilla/gecko/media/GeckoMediaDrm.java
@@ -1,15 +1,16 @@
 /* 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.MediaCrypto;
+import org.mozilla.gecko.util.MediaCodecUtil;
 
 public interface GeckoMediaDrm {
     public interface Callbacks {
         void onSessionCreated(int createSessionToken,
                               int promiseId,
                               byte[] sessionId,
                               byte[] request);
         void onSessionUpdated(int promiseId, byte[] sessionId);
@@ -28,11 +29,12 @@ public interface GeckoMediaDrm {
                        int promiseId,
                        String initDataType,
                        byte[] initData);
     void updateSession(int promiseId, String sessionId, byte[] response);
     void closeSession(int promiseId, String sessionId);
     void release();
     MediaCrypto getMediaCrypto();
 
+    void setupKeyAddedCallback(MediaCodecUtil.KeyWaiter waiter);
     void notifyKeyNeeded();
     boolean waitForKey();
 }
--- a/mobile/android/base/java/org/mozilla/gecko/media/GeckoMediaDrmBridgeV21.java
+++ b/mobile/android/base/java/org/mozilla/gecko/media/GeckoMediaDrmBridgeV21.java
@@ -1,26 +1,28 @@
 /* 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 org.mozilla.gecko.util.MediaCodecUtil;
 
 import java.lang.*;
 import java.io.BufferedReader;
 import java.io.IOException;
 import java.io.InputStreamReader;
 import java.net.HttpURLConnection;
 import java.net.URL;
 import java.net.URLEncoder;
 import java.nio.ByteBuffer;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.UUID;
 import java.util.ArrayDeque;
+import java.util.ArrayList;
 
 import android.annotation.SuppressLint;
 import android.os.AsyncTask;
 import android.os.Handler;
 import android.os.HandlerThread;
 import android.media.DeniedByServerException;
 import android.media.MediaCrypto;
 import android.media.MediaCryptoException;
@@ -45,16 +47,18 @@ public class GeckoMediaDrmBridgeV21 impl
     private HandlerThread mHandlerThread;
     private ByteBuffer mCryptoSessionId;
 
     // mProvisioningPromiseId is great than 0 only during provisioning.
     private int mProvisioningPromiseId;
     private HashSet<ByteBuffer> mSessionIds;
     private HashMap<ByteBuffer, String> mSessionMIMETypes;
     private ArrayDeque<PendingCreateSessionData> mPendingCreateSessionDataQueue;
+    private ArrayList<MediaCodecUtil.KeyWaiter> mKeyWaiters =
+        new ArrayList<MediaCodecUtil.KeyWaiter>();
 
     private boolean keyNeeded = false;
     private HashSet<String> mOngoingSessionIds = new HashSet<String>();
 
     private MediaCrypto mCrypto;
 
     protected MediaDrm mDrm;
     protected GeckoMediaDrm.Callbacks mCallbacks;
@@ -92,16 +96,19 @@ public class GeckoMediaDrmBridgeV21 impl
       }
     }
 
     private void notifyKeyAdded(String sessionId) {
         if (DEBUG) Log.d(LOGTAG, "notifyKeyAdded() ");
         boolean deleted = mOngoingSessionIds.remove(sessionId);
         if (deleted && mOngoingSessionIds.isEmpty()) {
             keyNeeded = false;
+            for (MediaCodecUtil.KeyWaiter waiter : mKeyWaiters) {
+                waiter.onKeyAdded();
+            }
         }
     }
 
     @SuppressLint("WrongConstant")
     private void configureVendorSpecificProperty() {
         assertTrue(mDrm != null);
         // Support L3 for now
         mDrm.setPropertyString("securityLevel", "L3");
@@ -290,16 +297,23 @@ public class GeckoMediaDrmBridgeV21 impl
 
     @Override
     public MediaCrypto getMediaCrypto() {
         if (DEBUG) Log.d(LOGTAG, "getMediaCrypto()");
         return mCrypto;
     }
 
     @Override
+    public void setupKeyAddedCallback(MediaCodecUtil.KeyWaiter callback) {
+        assertTrue(callback != null);
+        if (DEBUG) Log.d(LOGTAG, "setupKeyAddedCallback()");
+        mKeyWaiters.add(callback);
+    }
+
+    @Override
     public synchronized void notifyKeyNeeded() {
         if (DEBUG) Log.d(LOGTAG, "notifyKeyNeeded()");
         keyNeeded = true;
     }
 
     @Override
     public synchronized boolean waitForKey() {
         if (DEBUG) Log.d(LOGTAG, "waitForKey : " + keyNeeded);
--- a/mobile/android/base/java/org/mozilla/gecko/media/GeckoMediaDrmBridgeV23.java
+++ b/mobile/android/base/java/org/mozilla/gecko/media/GeckoMediaDrmBridgeV23.java
@@ -1,13 +1,14 @@
 /* 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 org.mozilla.gecko.util.MediaCodecUtil;
 
 import android.annotation.TargetApi;
 import android.media.DeniedByServerException;
 import android.media.NotProvisionedException;
 
 import static android.os.Build.VERSION_CODES.M;
 import android.media.MediaDrm;
 import android.util.Log;
@@ -64,20 +65,26 @@ public class GeckoMediaDrmBridgeV23 exte
     protected void onSessionMessage(byte[] sessionId,
                                     int sessionMessageType,
                                     byte[] request) {
         assertTrue(mCallbacks != null);
         mCallbacks.onSessionMessage(sessionId, sessionMessageType, request);
     }
 
     @Override
+    public void setupKeyAddedCallback(MediaCodecUtil.KeyWaiter callback) {
+        // Should not be used starting from Android M.
+        assertTrue(false);
+    }
+
+    @Override
     public synchronized void notifyKeyNeeded() {
-        // Should not be used after Android M.
+        // Should not be used starting from Android M.
         assertTrue(false);
     }
 
     @Override
     public synchronized boolean waitForKey() {
-        // Should not be used after Android M.
+        // Should not be used starting from Android M.
         assertTrue(false);
         return false;
     }
 }
--- a/mobile/android/base/java/org/mozilla/gecko/media/JellyBeanAsyncCodec.java
+++ b/mobile/android/base/java/org/mozilla/gecko/media/JellyBeanAsyncCodec.java
@@ -1,15 +1,16 @@
 /* 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 org.mozilla.gecko.util.HardwareCodecCapabilityUtils;
+import org.mozilla.gecko.util.MediaCodecUtil;
 
 import android.media.MediaCodec;
 import android.media.MediaCrypto;
 import android.media.MediaFormat;
 import android.os.Handler;
 import android.os.HandlerThread;
 import android.os.Looper;
 import android.os.Message;
@@ -21,18 +22,16 @@ import java.nio.ByteBuffer;
 
 // 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(Looper looper) {
             super(looper);
         }
 
         protected void cancel() {
@@ -199,17 +198,17 @@ final class JellyBeanAsyncCodec implemen
                     case MSG_POLL_OUTPUT_BUFFERS:
                         pollOutputBuffer();
                         break;
                     default:
                         return false;
                 }
             } catch (IllegalStateException e) {
                 e.printStackTrace();
-                mCallbackSender.notifyError(ERROR_CODEC);
+                mCallbackSender.notifyError(MediaCodecUtil.CODEC_STATUS_ERROR);
             }
 
             return true;
         }
 
         private void pollInputBuffer() {
             int result = mCodec.dequeueInputBuffer(DEQUEUE_TIMEOUT_US);
             if (result >= 0) {
@@ -253,16 +252,21 @@ final class JellyBeanAsyncCodec implemen
     private ByteBuffer[] mOutputBuffers;
     private AsyncCodec.Callbacks mCallbacks;
     private CallbackSender mCallbackSender;
 
     private BufferPoller mBufferPoller;
     private volatile boolean mInputEnded;
     private volatile boolean mOutputEnded;
 
+    // A lock to block the input procedure when encountering no-key error.
+    final Object waitForKeyLock = new Object();
+    private boolean waitForKey = false;
+    private String mDrmStubId;
+
     // Must be called on a thread with looper.
     /* package */ JellyBeanAsyncCodec(String name) throws IOException {
         mCodec = MediaCodec.createByCodecName(name);
         initBufferPoller(name + " buffer poller");
     }
 
     private void initBufferPoller(String name) {
         if (mBufferPoller != null) {
@@ -331,17 +335,17 @@ final class JellyBeanAsyncCodec implemen
         assertCallbacks();
 
         mInputEnded = (flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0;
 
         try {
             mCodec.queueInputBuffer(index, offset, size, presentationTimeUs, flags);
         } catch (IllegalStateException e) {
             e.printStackTrace();
-            mCallbackSender.notifyError(ERROR_CODEC);
+            mCallbackSender.notifyError(MediaCodecUtil.CODEC_STATUS_ERROR);
             return;
         }
 
         mBufferPoller.schedulePolling(BufferPoller.MSG_POLL_OUTPUT_BUFFERS);
         mBufferPoller.schedulePolling(BufferPoller.MSG_POLL_INPUT_BUFFERS);
     }
 
     @Override
@@ -349,26 +353,38 @@ final class JellyBeanAsyncCodec implemen
                                              int offset,
                                              MediaCodec.CryptoInfo cryptoInfo,
                                              long presentationTimeUs,
                                              int flags) {
         assertCallbacks();
 
         mInputEnded = (flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0;
 
-        try {
-            mCodec.queueSecureInputBuffer(index, offset, cryptoInfo, presentationTimeUs, flags);
-        } catch (IllegalStateException e) {
-            e.printStackTrace();
-            mCallbackSender.notifyError(ERROR_CODEC);
+        int res = MediaCodecUtil.queueSecureInputBuffer(mCodec, index, offset, cryptoInfo, presentationTimeUs, flags);
+        if (res != MediaCodecUtil.CODEC_STATUS_OK) {
+            if (DEBUG) Log.d(LOGTAG, "Got MediaCodecUtil Error : " + res);
+            if (res == MediaCodecUtil.CODEC_STATUS_NO_KEY) {
+                synchronized (waitForKeyLock) {
+                    try {
+                        waitForKey = true;
+                        RemoteMediaDrmBridgeStub.notifyKeyNeeded(mDrmStubId);
+                        waitForKeyLock.wait();
+                        if (DEBUG) Log.d(LOGTAG, "going to queueSecureInputBuffer 2nd time.");
+                        MediaCodecUtil.queueSecureInputBuffer(mCodec, index, offset, cryptoInfo, presentationTimeUs, flags);
+                    } catch (Exception e) {
+                        Log.e(LOGTAG, "Got exception when queueSecureInputBuffer 2nd time.", e);
+                    }
+                }
+            } else {
+                mCallbackSender.notifyError(res);
+            }
             return;
         }
-
+        mBufferPoller.schedulePolling(BufferPoller.MSG_POLL_OUTPUT_BUFFERS);
         mBufferPoller.schedulePolling(BufferPoller.MSG_POLL_INPUT_BUFFERS);
-        mBufferPoller.schedulePolling(BufferPoller.MSG_POLL_OUTPUT_BUFFERS);
     }
 
     @Override
     public final void releaseOutputBuffer(int index, boolean render) {
         assertCallbacks();
 
         mCodec.releaseOutputBuffer(index, render);
     }
@@ -418,16 +434,36 @@ final class JellyBeanAsyncCodec implemen
         assertCallbacks();
 
         cancelPendingTasks();
         mCallbackSender = null;
         mCodec.release();
         stopBufferPoller();
     }
 
+    @Override
+    public void setDrmStubId(String drmStubId) {
+        mDrmStubId = drmStubId;
+    }
+
+    @Override
+    public void notifyKeyAdded() {
+        if (DEBUG) Log.d(LOGTAG, "notifyKeyAdded()");
+        synchronized (waitForKeyLock) {
+            try {
+                if (waitForKey) {
+                    waitForKey = false;
+                    waitForKeyLock.notify();
+                }
+            } catch (Exception e) {
+                Log.e(LOGTAG, "Got exception : ", e);
+            }
+        }
+    }
+
     private void stopBufferPoller() {
         if (mBufferPoller == null) {
             Log.e(LOGTAG, "no initialized poller.");
             return;
         }
 
         mBufferPoller.getLooper().quit();
         mBufferPoller = null;
--- a/mobile/android/base/java/org/mozilla/gecko/media/LocalMediaDrmBridge.java
+++ b/mobile/android/base/java/org/mozilla/gecko/media/LocalMediaDrmBridge.java
@@ -1,14 +1,15 @@
 /* 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 org.mozilla.gecko.AppConstants;
+import org.mozilla.gecko.util.MediaCodecUtil;
 
 import android.media.MediaCrypto;
 import android.util.Log;
 
 final class LocalMediaDrmBridge implements GeckoMediaDrm {
     private static final String LOGTAG = "GeckoLocalMediaDrmBridge";
     private static final boolean DEBUG = false;
     private GeckoMediaDrm mBridge = null;
@@ -156,16 +157,21 @@ final class LocalMediaDrmBridge implemen
 
     @Override
     public synchronized MediaCrypto getMediaCrypto() {
         if (DEBUG) Log.d(LOGTAG, "getMediaCrypto()");
         return mBridge != null ? mBridge.getMediaCrypto() : null;
     }
 
     @Override
+    public void setupKeyAddedCallback(MediaCodecUtil.KeyWaiter callback) {
+        assertTrue(false);
+    }
+
+    @Override
     public void notifyKeyNeeded() {
         assertTrue(mBridge != null);
         mBridge.notifyKeyNeeded();
     }
 
     @Override
     public boolean waitForKey() {
         assertTrue(mBridge != null);
--- a/mobile/android/base/java/org/mozilla/gecko/media/MediaDrmProxy.java
+++ b/mobile/android/base/java/org/mozilla/gecko/media/MediaDrmProxy.java
@@ -369,17 +369,16 @@ public final class MediaDrmProxy {
         assertTrue(mImpl != null);
         if (DEBUG) Log.d(LOGTAG, "waitForKey()");
         return mImpl.waitForKey();
     }
 
     @WrapForJNI
     public static synchronized boolean waitForKey(String stubId) {
         assertTrue(AppConstants.Versions.preMarshmallow);
-
         for (MediaDrmProxy proxy : sProxyList) {
             if (proxy.getStubId().equals(stubId)) {
                 return proxy.waitForKey();
             }
         }
         if (DEBUG) Log.d(LOGTAG, "ERROR : Cannot find targe DRM stub - " + stubId);
         return false;
     }
--- a/mobile/android/base/java/org/mozilla/gecko/media/RemoteMediaDrmBridge.java
+++ b/mobile/android/base/java/org/mozilla/gecko/media/RemoteMediaDrmBridge.java
@@ -1,14 +1,16 @@
 /* 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 org.mozilla.gecko.util.MediaCodecUtil;
+
 import android.media.MediaCrypto;
 import android.util.Log;
 
 final class RemoteMediaDrmBridge implements GeckoMediaDrm {
     private static final String LOGTAG = "GeckoRemoteMediaDrmBridge";
     private static final boolean DEBUG = false;
     private CallbacksForwarder mCallbacksFwd;
     private IMediaDrmBridge mRemote;
@@ -146,16 +148,22 @@ final class RemoteMediaDrmBridge impleme
     @Override
     public synchronized MediaCrypto getMediaCrypto() {
         if (DEBUG) Log.d(LOGTAG, "getMediaCrypto(), should not enter here!");
         assertTrue(false);
         return null;
     }
 
     @Override
+    public void setupKeyAddedCallback(MediaCodecUtil.KeyWaiter callback) {
+        // Should not be used in this case.
+        assertTrue(false);
+    }
+
+    @Override
     public void notifyKeyNeeded() {
         // Should not be used in this case.
         assertTrue(false);
     }
 
     @Override
     public boolean waitForKey() {
         // Should not be used in this case.
--- a/mobile/android/base/java/org/mozilla/gecko/media/RemoteMediaDrmBridgeStub.java
+++ b/mobile/android/base/java/org/mozilla/gecko/media/RemoteMediaDrmBridgeStub.java
@@ -1,17 +1,19 @@
 /* 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 org.mozilla.gecko.AppConstants;
+import org.mozilla.gecko.util.MediaCodecUtil;
 
 import java.util.ArrayList;
+import java.util.HashSet;
 
 import android.media.MediaCrypto;
 import android.os.IBinder;
 import android.os.RemoteException;
 import android.util.Log;
 
 final class RemoteMediaDrmBridgeStub extends IMediaDrmBridge.Stub implements IBinder.DeathRecipient {
     private static final String LOGTAG = "GeckoRemoteMediaDrmBridgeStub";
@@ -47,16 +49,34 @@ final class RemoteMediaDrmBridgeStub ext
             if (sBridgeStubs.get(i) != null &&
                 sBridgeStubs.get(i).getId().equals(stubId)) {
                 return sBridgeStubs.get(i).getMediaCryptoFromBridge();
             }
         }
         return null;
     }
 
+    public static synchronized void setupKeyAddedCallback(String stubId,
+                                                          MediaCodecUtil.KeyWaiter callback) {
+        assertTrue(AppConstants.Versions.preMarshmallow);
+        if (DEBUG) Log.d(LOGTAG, "setupKeyAddedCallback()");
+
+        for (RemoteMediaDrmBridgeStub bridgeStub : sBridgeStubs) {
+            if (bridgeStub.getId().equals(stubId)) {
+                bridgeStub.setupKeyAddedCallback(callback);
+                return;
+            }
+        }
+    }
+
+    private void setupKeyAddedCallback(MediaCodecUtil.KeyWaiter callback) {
+        assertTrue(mBridge != null);
+        mBridge.setupKeyAddedCallback(callback);
+    }
+
     public static synchronized void notifyKeyNeeded(String stubId) {
         assertTrue(AppConstants.Versions.preMarshmallow);
         if (DEBUG) Log.d(LOGTAG, "notifyKeyNeeded()");
 
         for (RemoteMediaDrmBridgeStub bridgeStub : sBridgeStubs) {
             if (bridgeStub.getId().equals(stubId)) {
                 bridgeStub.notifyKeyNeeded();
                 return;
@@ -74,16 +94,17 @@ final class RemoteMediaDrmBridgeStub ext
         assertTrue(AppConstants.Versions.preMarshmallow);
         if (DEBUG) Log.d(LOGTAG, "waitForKey()");
 
         for (RemoteMediaDrmBridgeStub bridgeStub : sBridgeStubs) {
             if (bridgeStub.getId().equals(stubId)) {
                 return bridgeStub.waitForKey();
             }
         }
+        if (DEBUG) Log.d(LOGTAG, "waitForKey() - find no stub error !");
         return false;
     }
 
     private boolean waitForKey() {
         assertTrue(mBridge != null);
         return mBridge.waitForKey();
     }
 
--- a/mobile/android/base/java/org/mozilla/gecko/util/MediaCodecUtil.java
+++ b/mobile/android/base/java/org/mozilla/gecko/util/MediaCodecUtil.java
@@ -38,9 +38,13 @@ public final class MediaCodecUtil {
                 return CODEC_STATUS_NO_KEY;
             }
             return CODEC_STATUS_ERROR;
         } catch (Exception e) {
             return CODEC_STATUS_ERROR;
         }
         return CODEC_STATUS_OK;
     }
+
+    public interface KeyWaiter {
+        public void onKeyAdded();
+    }
 }