Bug 1395922 [Part1]- Handle HTMLMediaElement::SetMediaKeys asynchronously. draft
authorKilik Kuo <kikuo@mozilla.com>
Wed, 13 Sep 2017 15:48:10 +0800
changeset 668135 feabc4ca7cc9156b2272c3eae2d3393d4871834a
parent 662980 b0e945eed81db8bf076daf64e381c514f70144f0
child 668136 abecc5f9d182d5dedaf31e9435317bca2097b302
push id80935
push userkikuo@mozilla.com
push dateThu, 21 Sep 2017 04:40:11 +0000
bugs1395922
milestone57.0a1
Bug 1395922 [Part1]- Handle HTMLMediaElement::SetMediaKeys asynchronously. MozReview-Commit-ID: BmAl6ufvQnf
dom/html/HTMLMediaElement.cpp
dom/html/HTMLMediaElement.h
dom/media/MediaDecoder.cpp
dom/media/MediaDecoder.h
dom/media/MediaDecoderOwner.h
dom/media/MediaFormatReader.cpp
dom/media/MediaFormatReader.h
--- a/dom/html/HTMLMediaElement.cpp
+++ b/dom/html/HTMLMediaElement.cpp
@@ -180,16 +180,200 @@ ResolvePromisesWithUndefined(const nsTAr
 static void
 RejectPromises(const nsTArray<RefPtr<Promise>>& aPromises, nsresult aError)
 {
   for (auto& promise : aPromises) {
     promise->MaybeReject(aError);
   }
 }
 
+class HTMLMediaElement::SetMediaKeysRunner : public Runnable
+{
+public:
+  explicit SetMediaKeysRunner(const char* aName, HTMLMediaElement* aElement)
+    : Runnable(aName)
+    , mElement(aElement)
+  {
+    MOZ_ASSERT(mElement);
+  }
+  ~SetMediaKeysRunner() {}
+
+  void RemoveAssociation()
+  {
+    LOG(LogLevel::Debug, ("%s", __func__));
+    // 5.2.3 Stop using the CDM instance represented by the mediaKeys attribute
+    // to decrypt media data and remove the association with the media element.
+    mElement->mMediaKeys->Unbind();
+    mElement->mMediaKeys = nullptr;
+  }
+
+  bool TryAsyncRemoveCDMAssociation()
+  {
+    LOG(LogLevel::Debug, ("%s", __func__));
+    // 5.2.1 If the user agent or CDM do not support removing the association,
+    // let this object's attaching media keys value be false and reject promise
+    // with a new DOMException whose name is NotSupportedError.
+    // 5.2.2 If the association cannot currently be removed, let this object's
+    // attaching media keys value be false and reject promise with a new
+    // DOMException whose name is InvalidStateError.
+    if (mElement->mDecoder) {
+      // Log the playback time when MediaKeys is going to be set to another.
+      // Once the media pipeline has done the operation, FF may resume the
+      // playback if the new MediaKeys is appropriate.
+      mElement->mResumeEMEPlaybackTime = mElement->CurrentTime();
+      RefPtr<HTMLMediaElement::SetMediaKeysRunner> self = this;
+      mElement->mDecoder->SetCDMProxy(nullptr)
+        ->Then(mElement->mAbstractMainThread,
+               __func__,
+               [self](const MediaResult& aResult) {
+                 self->mElement->mSetCDMRequest.Complete();
+                 self->RemoveAssociation();
+                 if (self->AttachNewMediaKeysCDMProxy()) {
+                   // No incoming MediaKeys object or MediaDecoder is not
+                   // created yet.
+                   self->mElement->mResumeEMEPlaybackTime = 0.f;
+                   self->SetCDMProxySuccess();
+                 }
+               },
+               [self](const MediaResult& aResult) {
+                 self->mElement->mSetCDMRequest.Complete();
+                 // 5.2.4 If the preceding step failed, let this object's attaching media
+                 // keys value be false and reject promise with a new DOMException whose
+                 // name is the appropriate error name.
+                 self->SetCDMProxyFailure(aResult);
+               })
+        ->Track(mElement->mSetCDMRequest);
+      return false;
+    }
+    RemoveAssociation();
+    return true;
+  }
+
+  void MakeAssociationWithCDMResolved()
+  {
+    LOG(LogLevel::Debug, ("%s", __func__));
+    mElement->mSetCDMRequest.Complete();
+    SetCDMProxySuccess();
+  }
+
+  void MakeAssociationWithCDMRejected(const MediaResult& aResult)
+  {
+    LOG(LogLevel::Debug, ("%s", __func__));
+    mElement->mSetCDMRequest.Complete();
+    SetCDMProxyFailure(aResult);
+  }
+
+  bool TryAsyncMakeAssociationWithCDM(CDMProxy* aProxy)
+  {
+    MOZ_ASSERT(aProxy);
+    LOG(LogLevel::Debug, ("%s", __func__));
+    // 5.3.3 Queue a task to run the "Attempt to Resume Playback If Necessary"
+    // algorithm on the media element.
+    // Note: Setting the CDMProxy on the MediaDecoder will unblock playback.
+    if (mElement->mDecoder) {
+      // CDMProxy is set asynchronously in MediaFormatReader, once it's done,
+      // HTMLMediaElement::AsyncResolveOrRejectSetCDMPromise will be called to
+      // resolve or reject the DOM promise.
+      RefPtr<HTMLMediaElement::SetMediaKeysRunner> self = this;
+      mElement->mDecoder->SetCDMProxy(aProxy)
+        ->Then(mElement->mAbstractMainThread,
+               __func__,
+               [self](const MediaResult& aResult) {
+                 self->MakeAssociationWithCDMResolved();
+               },
+               [self](const MediaResult& aResult) {
+                 self->MakeAssociationWithCDMRejected(aResult);
+               })
+        ->Track(mElement->mSetCDMRequest);
+      return false;
+    }
+    return true;
+  }
+
+  void SetCDMProxySuccess()
+  {
+    mElement->AsyncResolveOrRejectSetCDMPromise(MediaResult(NS_OK));
+  }
+
+  void SetCDMProxyFailure(const MediaResult& aError)
+  {
+    mElement->mResumeEMEPlaybackTime = 0.f;
+    mElement->AsyncResolveOrRejectSetCDMPromise(aError);
+  }
+
+  bool DetachExistingMediaKeys()
+  {
+    LOG(LogLevel::Debug, ("%s", __func__));
+    // 5.1 If mediaKeys is not null, CDM instance represented by mediaKeys is
+    // already in use by another media element, and the user agent is unable
+    // to use it with this element, let this object's attaching media keys
+    // value be false and reject promise with a new DOMException whose name
+    // is QuotaExceededError.
+    if (mElement->mIncomingMediaKeys &&
+        mElement->mIncomingMediaKeys->IsBoundToMediaElement()) {
+      SetCDMProxyFailure(MediaResult(
+        NS_ERROR_DOM_QUOTA_EXCEEDED_ERR,
+        "MediaKeys object is already bound to another HTMLMediaElement"));
+      return false;
+    }
+
+    // 5.2 If the mediaKeys attribute is not null, run the following steps:
+    if (mElement->mMediaKeys) {
+      return TryAsyncRemoveCDMAssociation();
+    }
+    return true;
+  }
+
+  bool AttachNewMediaKeysCDMProxy()
+  {
+    LOG(LogLevel::Debug,
+        ("%s incoming MediaKeys(%p)", __func__, mElement->mIncomingMediaKeys.get()));
+    // 5.3. If mediaKeys is not null, run the following steps:
+    if (mElement->mIncomingMediaKeys) {
+      auto cdmProxy = mElement->mIncomingMediaKeys->GetCDMProxy();
+      if (!cdmProxy) {
+        SetCDMProxyFailure(MediaResult(
+          NS_ERROR_DOM_INVALID_STATE_ERR,
+          "CDM crashed before binding MediaKeys object to HTMLMediaElement"));
+        return false;
+      }
+
+      // 5.3.1 Associate the CDM instance represented by mediaKeys with the
+      // media element for decrypting media data.
+      if (NS_FAILED(mElement->mIncomingMediaKeys->Bind(mElement))) {
+        // 5.3.2 If the preceding step failed, run the following steps:
+
+        // 5.3.2.1 Set the mediaKeys attribute to null.
+        mElement->mMediaKeys = nullptr;
+        // 5.3.2.2 Let this object's attaching media keys value be false.
+        // 5.3.2.3 Reject promise with a new DOMException whose name is
+        // the appropriate error name.
+        SetCDMProxyFailure(
+          MediaResult(NS_ERROR_DOM_INVALID_STATE_ERR,
+                      "Failed to bind MediaKeys object to HTMLMediaElement"));
+        return false;
+      }
+
+      return TryAsyncMakeAssociationWithCDM(cdmProxy);
+    }
+    return true;
+  }
+
+  NS_IMETHOD Run()
+  {
+    if (!DetachExistingMediaKeys() || !AttachNewMediaKeysCDMProxy()) {
+      return NS_OK;
+    }
+
+    SetCDMProxySuccess();
+  }
+
+protected:
+  RefPtr<HTMLMediaElement> mElement;
+};
 // Under certain conditions there may be no-one holding references to
 // a media element from script, DOM parent, etc, but the element may still
 // fire meaningful events in the future so we can't destroy it yet:
 // 1) If the element is delaying the load event (or would be, if it were
 // in a document), then events up to loadeddata or error could be fired,
 // so we need to stay alive.
 // 2) If the element is not paused and playback has not ended, then
 // we will (or might) play, sending timeupdate and ended events and possibly
@@ -1457,16 +1641,18 @@ NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPlayed);
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTextTrackManager)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mAudioTrackList)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mVideoTrackList)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mMediaKeys)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSelectedVideoStreamTrack)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPendingPlayPromises)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSeekDOMPromise)
+  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSetMediaKeysDOMPromise)
+  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mIncomingMediaKeys)
 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
 
 NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(HTMLMediaElement, nsGenericHTMLElement)
   tmp->RemoveMutationObserver(tmp);
   if (tmp->mSrcStream) {
     // Need to EndMediaStreamPlayback to clear mSrcStream and make sure everything
     // gets unhooked correctly.
     tmp->EndSrcMediaStreamPlayback();
@@ -1488,16 +1674,18 @@ NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_IN
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mPlayed)
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mTextTrackManager)
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mAudioTrackList)
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mVideoTrackList)
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mMediaKeys)
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mSelectedVideoStreamTrack)
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mPendingPlayPromises)
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mSeekDOMPromise)
+  NS_IMPL_CYCLE_COLLECTION_UNLINK(mSetMediaKeysDOMPromise)
+  NS_IMPL_CYCLE_COLLECTION_UNLINK(mIncomingMediaKeys)
 NS_IMPL_CYCLE_COLLECTION_UNLINK_END
 
 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(HTMLMediaElement)
   NS_INTERFACE_MAP_ENTRY(nsIDOMHTMLMediaElement)
 NS_INTERFACE_MAP_END_INHERITING(nsGenericHTMLElement)
 
 // nsIDOMHTMLMediaElement
 NS_IMPL_URI_ATTR(HTMLMediaElement, Src, src)
@@ -3803,80 +3991,85 @@ private:
   }
   // Guaranteed to be valid by HTMLMediaElement.
   HTMLMediaElement* mWeak = nullptr;
   Phase mPhase = Phase::Init;
 };
 
 NS_IMPL_ISUPPORTS(HTMLMediaElement::ShutdownObserver, nsIObserver)
 
-HTMLMediaElement::HTMLMediaElement(already_AddRefed<mozilla::dom::NodeInfo>& aNodeInfo)
-  : nsGenericHTMLElement(aNodeInfo),
-    mMainThreadEventTarget(OwnerDoc()->EventTargetFor(TaskCategory::Other)),
-    mAbstractMainThread(OwnerDoc()->AbstractMainThreadFor(TaskCategory::Other)),
-    mWatchManager(this, mAbstractMainThread),
-    mSrcStreamTracksAvailable(false),
-    mSrcStreamPausedCurrentTime(-1),
-    mShutdownObserver(new ShutdownObserver),
-    mSourcePointer(nullptr),
-    mNetworkState(nsIDOMHTMLMediaElement::NETWORK_EMPTY),
-    mReadyState(nsIDOMHTMLMediaElement::HAVE_NOTHING, "HTMLMediaElement::mReadyState"),
-    mCurrentLoadID(0),
-    mLoadWaitStatus(NOT_WAITING),
-    mVolume(1.0),
-    mIsAudioTrackAudible(false),
-    mMuted(0),
-    mPreloadAction(PRELOAD_UNDEFINED),
-    mLastCurrentTime(0.0),
-    mFragmentStart(-1.0),
-    mFragmentEnd(-1.0),
-    mDefaultPlaybackRate(1.0),
-    mPlaybackRate(1.0),
-    mPreservesPitch(true),
-    mPlayed(new TimeRanges(ToSupports(OwnerDoc()))),
-    mCurrentPlayRangeStart(-1.0),
-    mBegun(false),
-    mLoadedDataFired(false),
-    mAutoplaying(true),
-    mAutoplayEnabled(true),
-    mPaused(true, *this),
-    mStatsShowing(false),
-    mAllowCasting(false),
-    mIsCasting(false),
-    mAudioCaptured(false),
-    mPlayingBeforeSeek(false),
-    mPausedForInactiveDocumentOrChannel(false),
-    mEventDeliveryPaused(false),
-    mIsRunningLoadMethod(false),
-    mIsDoingExplicitLoad(false),
-    mIsLoadingFromSourceChildren(false),
-    mDelayingLoadEvent(false),
-    mIsRunningSelectResource(false),
-    mHaveQueuedSelectResource(false),
-    mSuspendedAfterFirstFrame(false),
-    mAllowSuspendAfterFirstFrame(true),
-    mHasPlayedOrSeeked(false),
-    mHasSelfReference(false),
-    mShuttingDown(false),
-    mSuspendedForPreloadNone(false),
-    mSrcStreamIsPlaying(false),
-    mMediaSecurityVerified(false),
-    mCORSMode(CORS_NONE),
-    mIsEncrypted(false),
-    mWaitingForKey(NOT_WAITING_FOR_KEY),
-    mDownloadSuspendedByCache(false, "HTMLMediaElement::mDownloadSuspendedByCache"),
-    mDisableVideo(false),
-    mHasUserInteraction(false),
-    mFirstFrameLoaded(false),
-    mDefaultPlaybackStartPosition(0.0),
-    mHasSuspendTaint(false),
-    mMediaTracksConstructed(false),
-    mVisibilityState(Visibility::UNTRACKED),
-    mErrorSink(new ErrorSink(this)),
-    mAudioChannelWrapper(new AudioChannelAgentCallback(this))
+HTMLMediaElement::HTMLMediaElement(
+  already_AddRefed<mozilla::dom::NodeInfo>& aNodeInfo)
+  : nsGenericHTMLElement(aNodeInfo)
+  , mMainThreadEventTarget(OwnerDoc()->EventTargetFor(TaskCategory::Other))
+  , mAbstractMainThread(OwnerDoc()->AbstractMainThreadFor(TaskCategory::Other))
+  , mWatchManager(this, mAbstractMainThread)
+  , mSrcStreamTracksAvailable(false)
+  , mSrcStreamPausedCurrentTime(-1)
+  , mShutdownObserver(new ShutdownObserver)
+  , mSourcePointer(nullptr)
+  , mNetworkState(nsIDOMHTMLMediaElement::NETWORK_EMPTY)
+  , mReadyState(nsIDOMHTMLMediaElement::HAVE_NOTHING,
+                "HTMLMediaElement::mReadyState")
+  , mCurrentLoadID(0)
+  , mLoadWaitStatus(NOT_WAITING)
+  , mVolume(1.0)
+  , mIsAudioTrackAudible(false)
+  , mMuted(0)
+  , mPreloadAction(PRELOAD_UNDEFINED)
+  , mLastCurrentTime(0.0)
+  , mFragmentStart(-1.0)
+  , mFragmentEnd(-1.0)
+  , mDefaultPlaybackRate(1.0)
+  , mPlaybackRate(1.0)
+  , mPreservesPitch(true)
+  , mPlayed(new TimeRanges(ToSupports(OwnerDoc())))
+  , mAttachingMediaKey(false)
+  , mCurrentPlayRangeStart(-1.0)
+  , mBegun(false)
+  , mLoadedDataFired(false)
+  , mAutoplaying(true)
+  , mAutoplayEnabled(true)
+  , mPaused(true, *this)
+  , mStatsShowing(false)
+  , mAllowCasting(false)
+  , mIsCasting(false)
+  , mAudioCaptured(false)
+  , mPlayingBeforeSeek(false)
+  , mPausedForInactiveDocumentOrChannel(false)
+  , mEventDeliveryPaused(false)
+  , mIsRunningLoadMethod(false)
+  , mIsDoingExplicitLoad(false)
+  , mIsLoadingFromSourceChildren(false)
+  , mDelayingLoadEvent(false)
+  , mIsRunningSelectResource(false)
+  , mHaveQueuedSelectResource(false)
+  , mSuspendedAfterFirstFrame(false)
+  , mAllowSuspendAfterFirstFrame(true)
+  , mHasPlayedOrSeeked(false)
+  , mHasSelfReference(false)
+  , mShuttingDown(false)
+  , mSuspendedForPreloadNone(false)
+  , mSrcStreamIsPlaying(false)
+  , mMediaSecurityVerified(false)
+  , mCORSMode(CORS_NONE)
+  , mIsEncrypted(false)
+  , mWaitingForKey(NOT_WAITING_FOR_KEY)
+  , mDownloadSuspendedByCache(false,
+                              "HTMLMediaElement::mDownloadSuspendedByCache")
+  , mDisableVideo(false)
+  , mHasUserInteraction(false)
+  , mFirstFrameLoaded(false)
+  , mDefaultPlaybackStartPosition(0.0)
+  , mHasSuspendTaint(false)
+  , mMediaTracksConstructed(false)
+  , mVisibilityState(Visibility::UNTRACKED)
+  , mErrorSink(new ErrorSink(this))
+  , mAudioChannelWrapper(new AudioChannelAgentCallback(this))
+  , mResumeEMEPlaybackTime(0.f)
 {
   MOZ_ASSERT(mMainThreadEventTarget);
   MOZ_ASSERT(mAbstractMainThread);
 
   ErrorResult rv;
 
   double defaultVolume = Preferences::GetFloat("media.default_volume", 1.0);
   SetVolume(defaultVolume, rv);
@@ -6862,99 +7055,34 @@ HTMLMediaElement::SetMediaKeys(mozilla::
 
   // 1. If mediaKeys and the mediaKeys attribute are the same object,
   // return a resolved promise.
   if (mMediaKeys == aMediaKeys) {
     promise->MaybeResolveWithUndefined();
     return promise.forget();
   }
 
-  // Note: Our attaching code is synchronous, so we can skip the following steps.
-
   // 2. If this object's attaching media keys value is true, return a
   // promise rejected with a new DOMException whose name is InvalidStateError.
+  if (mAttachingMediaKey) {
+    promise->MaybeReject(
+      NS_ERROR_DOM_INVALID_STATE_ERR,
+      NS_LITERAL_CSTRING("A MediaKeys object is in attaching operation."));
+    return promise.forget();
+  }
+
   // 3. Let this object's attaching media keys value be true.
+  mAttachingMediaKey = true;
+  mIncomingMediaKeys = aMediaKeys;
+  mSetMediaKeysDOMPromise = promise;
   // 4. Let promise be a new promise.
   // 5. Run the following steps in parallel:
-
-  // 5.1 If mediaKeys is not null, CDM instance represented by mediaKeys is
-  // already in use by another media element, and the user agent is unable
-  // to use it with this element, let this object's attaching media keys
-  // value be false and reject promise with a new DOMException whose name
-  // is QuotaExceededError.
-  if (aMediaKeys && aMediaKeys->IsBoundToMediaElement()) {
-    promise->MaybeReject(NS_ERROR_DOM_QUOTA_EXCEEDED_ERR,
-      NS_LITERAL_CSTRING("MediaKeys object is already bound to another HTMLMediaElement"));
-    return promise.forget();
-  }
-
-  // 5.2 If the mediaKeys attribute is not null, run the following steps:
-  if (mMediaKeys) {
-    // 5.2.1 If the user agent or CDM do not support removing the association,
-    // let this object's attaching media keys value be false and reject promise
-    // with a new DOMException whose name is NotSupportedError.
-
-    // 5.2.2 If the association cannot currently be removed, let this object's
-    // attaching media keys value be false and reject promise with a new
-    // DOMException whose name is InvalidStateError.
-    if (mDecoder) {
-      // We don't support swapping out the MediaKeys once we've started to
-      // setup the playback pipeline. Note this also means we don't need to worry
-      // about handling disassociating the MediaKeys from the MediaDecoder.
-      promise->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR,
-        NS_LITERAL_CSTRING("Can't change MediaKeys on HTMLMediaElement after load has started"));
-      return promise.forget();
-    }
-
-    // 5.2.3 Stop using the CDM instance represented by the mediaKeys attribute
-    // to decrypt media data and remove the association with the media element.
-    mMediaKeys->Unbind();
-    mMediaKeys = nullptr;
-
-    // 5.2.4 If the preceding step failed, let this object's attaching media
-    // keys value be false and reject promise with a new DOMException whose
-    // name is the appropriate error name.
-  }
-
-  // 5.3. If mediaKeys is not null, run the following steps:
-  if (aMediaKeys) {
-    if (!aMediaKeys->GetCDMProxy()) {
-      promise->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR,
-        NS_LITERAL_CSTRING("CDM crashed before binding MediaKeys object to HTMLMediaElement"));
-      return promise.forget();
-    }
-
-    // 5.3.1 Associate the CDM instance represented by mediaKeys with the
-    // media element for decrypting media data.
-    if (NS_FAILED(aMediaKeys->Bind(this))) {
-      // 5.3.2 If the preceding step failed, run the following steps:
-      // 5.3.2.1 Set the mediaKeys attribute to null.
-      mMediaKeys = nullptr;
-      // 5.3.2.2 Let this object's attaching media keys value be false.
-      // 5.3.2.3 Reject promise with a new DOMException whose name is
-      // the appropriate error name.
-      promise->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR,
-                           NS_LITERAL_CSTRING("Failed to bind MediaKeys object to HTMLMediaElement"));
-      return promise.forget();
-    }
-    // 5.3.3 Queue a task to run the "Attempt to Resume Playback If Necessary"
-    // algorithm on the media element.
-    // Note: Setting the CDMProxy on the MediaDecoder will unblock playback.
-    if (mDecoder) {
-      mDecoder->SetCDMProxy(aMediaKeys->GetCDMProxy());
-    }
-  }
-
-  // 5.4 Set the mediaKeys attribute to mediaKeys.
-  mMediaKeys = aMediaKeys;
-
-  // 5.5 Let this object's attaching media keys value be false.
-
-  // 5.6 Resolve promise.
-  promise->MaybeResolveWithUndefined();
+  RefPtr<SetMediaKeysRunner> r =
+    new SetMediaKeysRunner("SetMediaKeysRunner", this);
+  mAbstractMainThread->Dispatch(r.forget());
 
   // 6. Return promise.
   return promise.forget();
 }
 
 EventHandlerNonNull*
 HTMLMediaElement::GetOnencrypted()
 {
@@ -7586,16 +7714,56 @@ HTMLMediaElement::AsyncRejectSeekDOMProm
       "dom::HTMLMediaElement::AsyncRejectSeekDOMPromiseIfExists",
       [=]() { promise->MaybeReject(NS_ERROR_DOM_ABORT_ERR); });
     mAbstractMainThread->Dispatch(r.forget());
     mSeekDOMPromise = nullptr;
   }
 }
 
 void
+HTMLMediaElement::AsyncResolveOrRejectSetCDMPromise(const MediaResult& aResult)
+{
+  LOG(LogLevel::Debug, ("%s", __func__));
+  MOZ_ASSERT(NS_IsMainThread());
+  if (mSetMediaKeysDOMPromise) {
+    MediaResult result = aResult;
+    RefPtr<DetailedPromise> promise = mSetMediaKeysDOMPromise.forget();
+    RefPtr<HTMLMediaElement> self = this;
+    nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction(
+      "dom::HTMLMediaElement::AsyncResolveOrRejectSetCDMPromise", [=]() {
+        if (NS_FAILED(aResult)) {
+          // 5.2.2 If the association cannot currently be removed, let this object's
+          // attaching media keys value be false and reject promise with a new
+          // DOMException whose name is InvalidStateError.
+          self->mAttachingMediaKey = false;
+          promise->MaybeReject(result.Code(), result.Message());
+        } else {
+          // 5.4 Set the mediaKeys attribute to mediaKeys.
+          self->mMediaKeys = self->mIncomingMediaKeys;
+          // 5.5 Let this object's attaching media keys value be false.
+          self->mAttachingMediaKey = false;
+          // 5.6 Resolve promise.
+          promise->MaybeResolveWithUndefined();
+
+          if (self->mResumeEMEPlaybackTime != 0.f) {
+            // Since setting CDMProxy will unblock playback now, seeking to the
+            // position where last playback stopped when MediaKeys was removing
+            // should provide a better experience.
+            IgnoredErrorResult rv;
+            self->FastSeek(self->mResumeEMEPlaybackTime, rv);
+          }
+        }
+        self->mResumeEMEPlaybackTime = 0.f;
+        self->mIncomingMediaKeys = nullptr;
+      });
+    mAbstractMainThread->Dispatch(r.forget());
+  }
+}
+
+void
 HTMLMediaElement::ReportCanPlayTelemetry()
 {
   LOG(LogLevel::Debug, ("%s", __func__));
 
   RefPtr<nsIThread> thread;
   nsresult rv = NS_NewNamedThread("MediaTelemetry", getter_AddRefs(thread));
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return;
--- a/dom/html/HTMLMediaElement.h
+++ b/dom/html/HTMLMediaElement.h
@@ -5,25 +5,27 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 #ifndef mozilla_dom_HTMLMediaElement_h
 #define mozilla_dom_HTMLMediaElement_h
 
 #include "nsAutoPtr.h"
 #include "nsIDOMHTMLMediaElement.h"
 #include "nsGenericHTMLElement.h"
 #include "MediaEventSource.h"
+#include "MediaResult.h"
 #include "SeekTarget.h"
 #include "MediaDecoderOwner.h"
 #include "nsCycleCollectionParticipant.h"
 #include "nsIObserver.h"
 #include "mozilla/CORSMode.h"
 #include "DecoderTraits.h"
 #include "nsIAudioChannelAgent.h"
 #include "mozilla/Attributes.h"
 #include "mozilla/dom/TextTrackManager.h"
+#include "mozilla/MozPromise.h"
 #include "mozilla/WeakPtr.h"
 #include "mozilla/dom/MediaKeys.h"
 #include "mozilla/StateWatching.h"
 #include "nsGkAtoms.h"
 #include "PrincipalChangeObserver.h"
 #include "nsStubMutationObserver.h"
 #include "MediaSegment.h" // for PrincipalHandle
 
@@ -1287,16 +1289,17 @@ protected:
 
   // Returns a StreamCaptureType populated with the right bits, depending on the
   // tracks this HTMLMediaElement has.
   StreamCaptureType CaptureTypeForElement();
 
   // True if this element can be captured, false otherwise.
   bool CanBeCaptured(StreamCaptureType aCaptureType);
 
+  class SetMediaKeysRunner;
   class nsAsyncEventRunner;
   class nsNotifyAboutPlayingRunner;
   class nsResolveOrRejectPendingPlayPromisesRunner;
   using nsGenericHTMLElement::DispatchEvent;
   // For nsAsyncEventRunner.
   nsresult DispatchEvent(const nsAString& aName);
 
   // Open unsupported types media with the external app when the media element
@@ -1529,16 +1532,21 @@ protected:
   // Timer used for updating progress events.
   nsCOMPtr<nsITimer> mProgressTimer;
 
   // Timer used to simulate video-suspend.
   nsCOMPtr<nsITimer> mVideoDecodeSuspendTimer;
 
   // Encrypted Media Extension media keys.
   RefPtr<MediaKeys> mMediaKeys;
+  RefPtr<MediaKeys> mIncomingMediaKeys;
+  // The dom promise is used for HTMLMediaElement::SetMediaKeys.
+  RefPtr<DetailedPromise> mSetMediaKeysDOMPromise;
+  // Used to indicate if the MediaKeys attaching operation is on-going or not.
+  bool mAttachingMediaKey;
 
   // Stores the time at the start of the current 'played' range.
   double mCurrentPlayRangeStart;
 
   // If true then we have begun downloading the media content.
   // Set to false when completed, or not yet started.
   bool mBegun;
 
@@ -1758,16 +1766,19 @@ private:
    * It will not be called if the value is being unset.
    *
    * @param aNamespaceID the namespace of the attr being set
    * @param aName the localname of the attribute being set
    * @param aNotify Whether we plan to notify document observers.
    */
   void AfterMaybeChangeAttr(int32_t aNamespaceID, nsIAtom* aName, bool aNotify);
 
+  // Used to resolve/reject the HTMLMediaElement::SetMediaKeys dom promise.
+  void AsyncResolveOrRejectSetCDMPromise(const MediaResult& aResult);
+
   // Total time a video has spent playing.
   TimeDurationAccumulator mPlayTime;
 
   // Total time a video has spent playing while hidden.
   TimeDurationAccumulator mHiddenPlayTime;
 
   // Total time a video has (or would have) spent in video-decode-suspend mode.
   TimeDurationAccumulator mVideoDecodeSuspendTime;
@@ -1811,16 +1822,26 @@ private:
   // We keep track of these because the load algorithm resolves/rejects all
   // already-dispatched pending play promises.
   nsTArray<nsResolveOrRejectPendingPlayPromisesRunner*> mPendingPlayPromisesRunners;
 
   // A pending seek promise which is created at Seek() method call and is
   // resolved/rejected at AsyncResolveSeekDOMPromiseIfExists()/
   // AsyncRejectSeekDOMPromiseIfExists() methods.
   RefPtr<dom::Promise> mSeekDOMPromise;
+
+  // A promise request holder to deal with the on-going request.
+  // Used to track whether CDM proxy is set to MediaFormatReader successfully
+  // or not.
+  MozPromiseRequestHolder<MediaDecoderOwner::SetCDMPromise> mSetCDMRequest;
+
+  // The playback position where CDMProxy is detached during playback.
+  // It could be used to seek to when CDMProxy is re-attached and keys are
+  // usable for decryption.
+  double mResumeEMEPlaybackTime;
 };
 
 // Check if the context is chrome or has the debugger or tabs permission
 bool
 HasDebuggerOrTabsPrivilege(JSContext* aCx, JSObject* aObj);
 
 } // namespace dom
 } // namespace mozilla
--- a/dom/media/MediaDecoder.cpp
+++ b/dom/media/MediaDecoder.cpp
@@ -1374,28 +1374,21 @@ MediaDecoder::CanPlayThrough()
   bool val = CanPlayThroughImpl();
   if (val != mCanPlayThrough) {
     mCanPlayThrough = val;
     mDecoderStateMachine->DispatchCanPlayThrough(val);
   }
   return val;
 }
 
-void
+RefPtr<MediaDecoderOwner::SetCDMPromise>
 MediaDecoder::SetCDMProxy(CDMProxy* aProxy)
 {
   MOZ_ASSERT(NS_IsMainThread());
-  RefPtr<CDMProxy> proxy = aProxy;
-  RefPtr<MediaFormatReader> reader = mReader;
-  nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction(
-    "MediaFormatReader::SetCDMProxy",
-    [reader, proxy]() {
-    reader->SetCDMProxy(proxy);
-    });
-  mReader->OwnerThread()->Dispatch(r.forget());
+  return mReader->SetCDMProxy(aProxy);
 }
 
 bool
 MediaDecoder::IsOpusEnabled()
 {
   return Preferences::GetBool("media.opus.enabled");
 }
 
--- a/dom/media/MediaDecoder.h
+++ b/dom/media/MediaDecoder.h
@@ -353,17 +353,17 @@ private:
 
   MediaDecoderOwner* GetOwner() const;
 
   AbstractThread* AbstractMainThread() const
   {
     return mAbstractMainThread;
   }
 
-  void SetCDMProxy(CDMProxy* aProxy);
+  RefPtr<MediaDecoderOwner::SetCDMPromise> SetCDMProxy(CDMProxy* aProxy);
 
   void EnsureTelemetryReported();
 
   static bool IsOggEnabled();
   static bool IsOpusEnabled();
   static bool IsWaveEnabled();
   static bool IsWebMEnabled();
 
--- a/dom/media/MediaDecoderOwner.h
+++ b/dom/media/MediaDecoderOwner.h
@@ -1,34 +1,39 @@
 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* vim:set ts=2 sw=2 sts=2 et cindent: */
 /* 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/. */
 #ifndef MediaDecoderOwner_h_
 #define MediaDecoderOwner_h_
 
+#include "mozilla/MozPromise.h"
 #include "mozilla/UniquePtr.h"
 #include "MediaInfo.h"
+#include "nsIDocument.h"
 
 namespace mozilla {
 
 class AbstractThread;
 class GMPCrashHelper;
 class VideoFrameContainer;
 class MediaInfo;
 class MediaResult;
 
 namespace dom {
 class HTMLMediaElement;
 } // namespace dom
 
 class MediaDecoderOwner
 {
 public:
+  using SetCDMPromise =
+    MozPromise<MediaResult, MediaResult, /* IsExclusive */ true>;
+
   // Called by the media decoder to indicate that the download is progressing.
   virtual void DownloadProgressed() = 0;
 
   // Dispatch an asynchronous event to the decoder owner
   virtual nsresult DispatchAsyncEvent(const nsAString& aName) = 0;
 
   // Triggers a recomputation of readyState.
   virtual void UpdateReadyState() = 0;
--- a/dom/media/MediaFormatReader.cpp
+++ b/dom/media/MediaFormatReader.cpp
@@ -38,19 +38,21 @@ using mozilla::layers::LayersBackend;
 static mozilla::LazyLogModule sFormatDecoderLog("MediaFormatReader");
 mozilla::LazyLogModule gMediaDemuxerLog("MediaDemuxer");
 
 #define LOG(arg, ...) MOZ_LOG(sFormatDecoderLog, mozilla::LogLevel::Debug, ("MediaFormatReader(%p)::%s: " arg, this, __func__, ##__VA_ARGS__))
 #define LOGV(arg, ...) MOZ_LOG(sFormatDecoderLog, mozilla::LogLevel::Verbose, ("MediaFormatReader(%p)::%s: " arg, this, __func__, ##__VA_ARGS__))
 
 #define NS_DispatchToMainThread(...) CompileError_UseAbstractMainThreadInstead
 
+#define ATTACHING_CDMPROXY_VIDEO 0x01
+#define ATTACHING_CDMPROXY_AUDIO 0x02
+
 namespace mozilla {
 
-
 typedef void* MediaDataDecoderID;
 
 /**
  * This helper class is used to report telemetry of the time used to recover a
  * decoder from GPU crash.
  * It uses MediaDecoderOwnerID to identify which video we're dealing with.
  * It uses MediaDataDecoderID to make sure that the old MediaDataDecoder has
  * been deleted and we're already recovered.
@@ -1192,16 +1194,17 @@ MediaFormatReader::MediaFormatReader(Med
   , mCrashHelper(aInit.mCrashHelper)
   , mDecoderFactory(new DecoderFactory(this))
   , mShutdownPromisePool(new ShutdownPromisePool())
   , mBuffered(mTaskQueue,
               TimeIntervals(),
               "MediaFormatReader::mBuffered (Canonical)")
   , mFrameStats(aInit.mFrameStats)
   , mMediaDecoderOwnerID(aInit.mMediaDecoderOwnerID)
+  , mAttachingCDMProxy(0)
 {
   MOZ_ASSERT(aDemuxer);
   MOZ_COUNT_CTOR(MediaFormatReader);
   mOnTrackWaitingForKeyListener = OnTrackWaitingForKey().Connect(
     mTaskQueue, this, &MediaFormatReader::NotifyWaitingForKey);
 }
 
 MediaFormatReader::~MediaFormatReader()
@@ -1216,16 +1219,20 @@ MediaFormatReader::Shutdown()
   MOZ_ASSERT(OnTaskQueue());
   LOG("");
 
   mDemuxerInitRequest.DisconnectIfExists();
   mNotifyDataArrivedPromise.DisconnectIfExists();
   mMetadataPromise.RejectIfExists(NS_ERROR_DOM_MEDIA_CANCELED, __func__);
   mSeekPromise.RejectIfExists(NS_ERROR_DOM_MEDIA_CANCELED, __func__);
   mSkipRequest.DisconnectIfExists();
+  mSetCDMPromise.RejectIfExists(
+    MediaResult(NS_ERROR_DOM_INVALID_STATE_ERR,
+                "MediaFormatReader is shutting down"),
+    __func__);
 
   if (mAudio.HasPromise()) {
     mAudio.RejectPromise(NS_ERROR_DOM_MEDIA_CANCELED, __func__);
   }
   if (mVideo.HasPromise()) {
     mVideo.RejectPromise(NS_ERROR_DOM_MEDIA_CANCELED, __func__);
   }
 
@@ -1306,33 +1313,46 @@ MediaFormatReader::Init()
 
   mVideo.mTaskQueue = new TaskQueue(
     GetMediaThreadPool(MediaThreadType::PLATFORM_DECODER),
     "MFR::mVideo::mTaskQueue");
 
   return NS_OK;
 }
 
-void
+RefPtr<MediaDecoderOwner::SetCDMPromise>
 MediaFormatReader::SetCDMProxy(CDMProxy* aProxy)
 {
+  if (!mSetCDMPromise.IsEmpty()) {
+    return MediaDecoderOwner::SetCDMPromise::CreateAndReject(
+      MediaResult(NS_ERROR_DOM_INVALID_STATE_ERR,
+                  "A CDMProxy attaching operation is in process."),
+      __func__);
+  }
+
+  RefPtr<MediaDecoderOwner::SetCDMPromise> p = mSetCDMPromise.Ensure(__func__);
   RefPtr<CDMProxy> proxy = aProxy;
   RefPtr<MediaFormatReader> self = this;
   nsCOMPtr<nsIRunnable> r =
     NS_NewRunnableFunction("MediaFormatReader::SetCDMProxy", [=]() {
       MOZ_ASSERT(self->OnTaskQueue());
-      self->mCDMProxy = proxy;
-      if (HasAudio()) {
-        self->ScheduleUpdate(TrackInfo::kAudioTrack);
+      LOGV("SetCDMProxy (%x)", proxy.get());
+
+      self->mAttachingCDMProxy |= (ATTACHING_CDMPROXY_AUDIO | ATTACHING_CDMPROXY_VIDEO);
+      if (!proxy) {
+        // Decoders should be shut down when CDMProxy is going to be detached as
+        // MFR should not use the old decoders if any new CDMProxy is set.
+        self->ReleaseResources();
       }
-      if (HasVideo()) {
-        self->ScheduleUpdate(TrackInfo::kVideoTrack);
-      }
+      self->mCDMProxy = proxy;
+      self->ScheduleUpdate(TrackInfo::kVideoTrack);
+      self->ScheduleUpdate(TrackInfo::kAudioTrack);
     });
   OwnerThread()->Dispatch(r.forget());
+  return p;
 }
 
 bool
 MediaFormatReader::IsWaitingOnCDMResource()
 {
   MOZ_ASSERT(OnTaskQueue());
   return IsEncrypted() && !mCDMProxy;
 }
@@ -2214,16 +2234,35 @@ MediaFormatReader::DrainDecoder(TrackTyp
              decoder.mDrainRequest.Complete();
              NotifyError(aTrack, aError);
            })
     ->Track(decoder.mDrainRequest);
   LOG("Requesting %s decoder to drain", TrackTypeToStr(aTrack));
 }
 
 void
+MediaFormatReader::CheckCDMProxyAttachingStatus(TrackType aTrack)
+{
+  MOZ_ASSERT(OnTaskQueue());
+  auto& decoder = GetDecoderData(aTrack);
+  if (decoder.mFlushed && mAttachingCDMProxy != 0) {
+    MOZ_ASSERT(!mSetCDMPromise.IsEmpty());
+    uint8_t flag = aTrack == TrackType::kVideoTrack
+      ? ATTACHING_CDMPROXY_VIDEO
+      : ATTACHING_CDMPROXY_AUDIO;
+    mAttachingCDMProxy ^= flag;
+
+    if (mAttachingCDMProxy == 0) {
+      mSetCDMPromise.Resolve(MediaResult(NS_OK), __func__);
+    }
+    ScheduleUpdate(aTrack);
+  }
+}
+
+void
 MediaFormatReader::Update(TrackType aTrack)
 {
   MOZ_ASSERT(OnTaskQueue());
 
   if (mShutdown) {
     return;
   }
 
@@ -2232,16 +2271,18 @@ MediaFormatReader::Update(TrackType aTra
   bool needOutput = false;
   auto& decoder = GetDecoderData(aTrack);
   decoder.mUpdateScheduled = false;
 
   if (!mInitDone) {
     return;
   }
 
+  CheckCDMProxyAttachingStatus(aTrack);
+
   if (aTrack == TrackType::kVideoTrack && mSkipRequest.Exists()) {
     LOGV("Skipping in progress, nothing more to do");
     return;
   }
 
   if (UpdateReceivedNewData(aTrack)) {
     LOGV("Nothing more to do");
     return;
--- a/dom/media/MediaFormatReader.h
+++ b/dom/media/MediaFormatReader.h
@@ -11,16 +11,17 @@
 #include "mozilla/Maybe.h"
 #include "mozilla/StateMirroring.h"
 #include "mozilla/TaskQueue.h"
 #include "mozilla/Mutex.h"
 
 #include "FrameStatistics.h"
 #include "MediaEventSource.h"
 #include "MediaDataDemuxer.h"
+#include "MediaDecoderOwner.h"
 #include "MediaMetadataManager.h"
 #include "MediaPrefs.h"
 #include "nsAutoPtr.h"
 #include "PDMFactory.h"
 #include "SeekTarget.h"
 
 namespace mozilla {
 
@@ -189,17 +190,18 @@ public:
   RefPtr<WaitForDataPromise> WaitForData(MediaData::Type aType);
 
   // The MediaDecoderStateMachine uses various heuristics that assume that
   // raw media data is arriving sequentially from a network channel. This
   // makes sense in the <video src="foo"> case, but not for more advanced use
   // cases like MSE.
   bool UseBufferingHeuristics() const { return mTrackDemuxersMayBlock; }
 
-  void SetCDMProxy(CDMProxy* aProxy);
+  RefPtr<MediaDecoderOwner::SetCDMPromise> SetCDMProxy(CDMProxy* aProxy);
+  MozPromiseHolder<MediaDecoderOwner::SetCDMPromise> mSetCDMPromise;
 
   // Returns a string describing the state of the decoder data.
   // Used for debugging purposes.
   void GetMozDebugReaderData(nsACString& aString);
 
   // Switch the video decoder to NullDecoderModule. It might takes effective
   // since a few samples later depends on how much demuxed samples are already
   // queued in the original video decoder.
@@ -764,13 +766,20 @@ private:
   MediaEventProducer<void> mOnWaitingForKey;
 
   MediaEventProducer<MediaResult> mOnDecodeWarning;
 
   RefPtr<FrameStatistics> mFrameStats;
 
   // Used in bug 1393399 for telemetry.
   const MediaDecoderOwnerID mMediaDecoderOwnerID;
+
+  // Check if the CDMProxy attaching procedure is done and resolve the promise
+  // which is requested from MediaElement.
+  void CheckCDMProxyAttachingStatus(TrackType aTrack);
+
+  // Used to identify if it's still in CDMProxy attaching procedure.
+  uint8_t mAttachingCDMProxy;
 };
 
 } // namespace mozilla
 
 #endif