diff options
author | athenian200 <athenian200@outlook.com> | 2019-12-23 09:09:57 -0600 |
---|---|---|
committer | athenian200 <athenian200@outlook.com> | 2019-12-23 09:27:09 -0600 |
commit | 735418fa363d2556d0ce40f0b4e67ff2bfc78b59 (patch) | |
tree | 6a3e7b77783e9c8d7e0f8d18e791f6cdfdbb4e15 | |
parent | 84f4806b4656253faefd534cb59da27241608752 (diff) | |
download | UXP-735418fa363d2556d0ce40f0b4e67ff2bfc78b59.tar UXP-735418fa363d2556d0ce40f0b4e67ff2bfc78b59.tar.gz UXP-735418fa363d2556d0ce40f0b4e67ff2bfc78b59.tar.lz UXP-735418fa363d2556d0ce40f0b4e67ff2bfc78b59.tar.xz UXP-735418fa363d2556d0ce40f0b4e67ff2bfc78b59.zip |
Issue #1332 - Backport promise-based media playback
https://bugzilla.mozilla.org/show_bug.cgi?id=1244768
I happened to find an older version of the promise-based media playback patch in Bugzilla, the one that was originally submitted for review. It had the DocShell changes I already knew how to deal with, and had fewer of the audio wrapper and nsISupports changes that were confusing me in the later patch. I was able to do a better job getting this back into a UXP-appropriate configuration than I could have with the final version.
I'm honestly still a little unsure about some of the minor refactoring done in the patch itself, insisting on already_AddRefed promises and such, but I don't really know how to avoid those completely. Still, I think it's better than it was.
-rw-r--r-- | dom/html/HTMLMediaElement.cpp | 304 | ||||
-rw-r--r-- | dom/html/HTMLMediaElement.h | 38 | ||||
-rw-r--r-- | dom/webidl/HTMLMediaElement.webidl | 2 |
3 files changed, 312 insertions, 32 deletions
diff --git a/dom/html/HTMLMediaElement.cpp b/dom/html/HTMLMediaElement.cpp index 050d1ac69..884f65762 100644 --- a/dom/html/HTMLMediaElement.cpp +++ b/dom/html/HTMLMediaElement.cpp @@ -8,6 +8,7 @@ #include "mozilla/dom/HTMLMediaElementBinding.h" #include "mozilla/dom/HTMLSourceElement.h" #include "mozilla/dom/ElementInlines.h" +#include "mozilla/dom/Promise.h" #include "mozilla/ArrayUtils.h" #include "mozilla/MathAlgorithms.h" #include "mozilla/AsyncEventDispatcher.h" @@ -171,6 +172,22 @@ static const unsigned short MEDIA_ERR_NETWORK = 2; static const unsigned short MEDIA_ERR_DECODE = 3; static const unsigned short MEDIA_ERR_SRC_NOT_SUPPORTED = 4; +static void +ResolvePromisesWithUndefined(const nsTArray<RefPtr<Promise>>& aPromises) +{ + for (auto& promise : aPromises) { + promise->MaybeResolveWithUndefined(); + } +} + +static void +RejectPromises(const nsTArray<RefPtr<Promise>>& aPromises, nsresult aError) +{ + for (auto& promise : aPromises) { + promise->MaybeReject(aError); + } +} + // 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: @@ -261,6 +278,75 @@ public: } }; +/* + * If no error is passed while constructing an instance, the instance will + * resolve the passed promises with undefined; otherwise, the instance will + * reject the passed promises with the passed error. + * + * The constructor appends the constructed instance into the passed media + * element's mPendingPlayPromisesRunners member and once the the runner is run + * (whether fulfilled or canceled), it removes itself from + * mPendingPlayPromisesRunners. + */ +class HTMLMediaElement::nsResolveOrRejectPendingPlayPromisesRunner : public nsMediaEvent +{ + nsTArray<RefPtr<Promise>> mPromises; + nsresult mError; + +public: + nsResolveOrRejectPendingPlayPromisesRunner(HTMLMediaElement* aElement, + nsTArray<RefPtr<Promise>>&& aPromises, + nsresult aError = NS_OK) + : nsMediaEvent(aElement) + , mPromises(Move(aPromises)) + , mError(aError) + { + mElement->mPendingPlayPromisesRunners.AppendElement(this); + } + + void ResolveOrReject() + { + if (NS_SUCCEEDED(mError)) { + ResolvePromisesWithUndefined(mPromises); + } else { + RejectPromises(mPromises, mError); + } + } + + NS_IMETHOD Run() override + { + if (!IsCancelled()) { + ResolveOrReject(); + } + + mElement->mPendingPlayPromisesRunners.RemoveElement(this); + return NS_OK; + } +}; + +class HTMLMediaElement::nsNotifyAboutPlayingRunner : public nsResolveOrRejectPendingPlayPromisesRunner +{ +public: + nsNotifyAboutPlayingRunner(HTMLMediaElement* aElement, + nsTArray<RefPtr<Promise>>&& aPendingPlayPromises) + : nsResolveOrRejectPendingPlayPromisesRunner(aElement, + Move(aPendingPlayPromises)) + { + } + + NS_IMETHOD Run() override + { + if (IsCancelled()) { + mElement->mPendingPlayPromisesRunners.RemoveElement(this); + return NS_OK; + } + + mElement->DispatchEvent(NS_LITERAL_STRING("playing")); + return nsResolveOrRejectPendingPlayPromisesRunner::Run(); + } +}; + + class nsSourceErrorEventRunner : public nsMediaEvent { private: @@ -826,6 +912,7 @@ NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(HTMLMediaElement, nsGenericHTM NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mMediaKeys) #endif NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSelectedVideoStreamTrack) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPendingPlayPromises) NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(HTMLMediaElement, nsGenericHTMLElement) @@ -853,6 +940,7 @@ NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(HTMLMediaElement, nsGenericHTMLE NS_IMPL_CYCLE_COLLECTION_UNLINK(mMediaKeys) #endif NS_IMPL_CYCLE_COLLECTION_UNLINK(mSelectedVideoStreamTrack) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mPendingPlayPromises) NS_IMPL_CYCLE_COLLECTION_UNLINK_END NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(HTMLMediaElement) @@ -1044,6 +1132,14 @@ void HTMLMediaElement::AbortExistingLoads() // with a different load ID to silently be cancelled. mCurrentLoadID++; + // Immediately reject or resolve the already-dispatched + // nsResolveOrRejectPendingPlayPromisesRunners. These runners won't be + // executed again later since the mCurrentLoadID had been changed. + for (auto& runner : mPendingPlayPromisesRunners) { + runner->ResolveOrReject(); + } + mPendingPlayPromisesRunners.Clear(); + if (mChannelLoader) { mChannelLoader->Cancel(); mChannelLoader = nullptr; @@ -1107,7 +1203,10 @@ void HTMLMediaElement::AbortExistingLoads() NS_ASSERTION(!mDecoder && !mSrcStream, "How did someone setup a new stream/decoder already?"); // ChangeNetworkState() will call UpdateAudioChannelPlayingState() // indirectly which depends on mPaused. So we need to update mPaused first. - mPaused = true; + if (!mPaused) { + mPaused = true; + RejectPromises(TakePendingPlayPromises(), NS_ERROR_DOM_MEDIA_ABORT_ERR); + } ChangeNetworkState(nsIDOMHTMLMediaElement::NETWORK_EMPTY); ChangeReadyState(nsIDOMHTMLMediaElement::HAVE_NOTHING); @@ -1149,6 +1248,7 @@ void HTMLMediaElement::NoSupportedMediaSourceError(const nsACString& aErrorDetai mErrorSink->SetError(MEDIA_ERR_SRC_NOT_SUPPORTED, aErrorDetails); ChangeDelayLoadStatus(false); UpdateAudioChannelPlayingState(); + RejectPromises(TakePendingPlayPromises(), NS_ERROR_DOM_MEDIA_NOT_SUPPORTED_ERR); } typedef void (HTMLMediaElement::*SyncSectionFn)(); @@ -1953,14 +2053,7 @@ HTMLMediaElement::Seek(double aTime, // aTime should be non-NaN. MOZ_ASSERT(!mozilla::IsNaN(aTime)); - nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(OwnerDoc()->GetInnerWindow()); - - if (!global) { - aRv.Throw(NS_ERROR_UNEXPECTED); - return nullptr; - } - - RefPtr<Promise> promise = Promise::Create(global, aRv); + RefPtr<Promise> promise = CreateDOMPromise(aRv); if (NS_WARN_IF(aRv.Failed())) { return nullptr; @@ -2208,6 +2301,7 @@ HTMLMediaElement::Pause(ErrorResult& aRv) if (!oldPaused) { FireTimeUpdate(false); DispatchAsyncEvent(NS_LITERAL_STRING("pause")); + AsyncRejectPendingPlayPromises(NS_ERROR_DOM_MEDIA_ABORT_ERR); } } @@ -3200,36 +3294,86 @@ HTMLMediaElement::NotifyXPCOMShutdown() ShutdownDecoder(); } -void +already_AddRefed<Promise> HTMLMediaElement::Play(ErrorResult& aRv) { if (!IsAllowedToPlay()) { MaybeDoLoad(); - return; - } + + // A blocked media element will be resumed later, so we return a pending + // promise which might be resolved/rejected depends on the result of + // resuming the blocked media element. + RefPtr<Promise> promise = CreateDOMPromise(aRv); - nsresult rv = PlayInternal(); - if (NS_FAILED(rv)) { - aRv.Throw(rv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + mPendingPlayPromises.AppendElement(promise); + return promise.forget(); } + RefPtr<Promise> promise = PlayInternal(aRv); + OpenUnsupportedMediaWithExternalAppIfNeeded(); + + return promise.forget(); } -nsresult -HTMLMediaElement::PlayInternal() +already_AddRefed<Promise> +HTMLMediaElement::PlayInternal(ErrorResult& aRv) { + MOZ_ASSERT(!aRv.Failed()); + + // 4.8.12.8 + // When the play() method on a media element is invoked, the user agent must + // run the following steps. + + // 4.8.12.8 - Step 1: + // If the media element is not allowed to play, return a promise rejected + // with a "NotAllowedError" DOMException and abort these steps. + if (!IsAllowedToPlay()) { + // NOTE: for promise-based-play, will return a rejected promise here. + aRv.Throw(NS_ERROR_DOM_MEDIA_NOT_ALLOWED_ERR); + return nullptr; + } + + // 4.8.12.8 - Step 2: + // If the media element's error attribute is not null and its code + // attribute has the value MEDIA_ERR_SRC_NOT_SUPPORTED, return a promise + // rejected with a "NotSupportedError" DOMException and abort these steps. + if (GetError() && GetError()->Code() == MEDIA_ERR_SRC_NOT_SUPPORTED) { + aRv.Throw(NS_ERROR_DOM_MEDIA_NOT_SUPPORTED_ERR); + return nullptr; + } + + // 4.8.12.8 - Step 3: + // Let promise be a new promise and append promise to the list of pending + // play promises. + RefPtr<Promise> promise = CreateDOMPromise(aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + mPendingPlayPromises.AppendElement(promise); + // Play was not blocked so assume user interacted with the element. mHasUserInteraction = true; StopSuspendingAfterFirstFrame(); SetPlayedOrSeeked(true); + // 4.8.12.8 - Step 4: + // If the media element's networkState attribute has the value NETWORK_EMPTY, + // invoke the media element's resource selection algorithm. MaybeDoLoad(); if (mSuspendedForPreloadNone) { ResumeLoad(PRELOAD_ENOUGH); } + // 4.8.12.8 - Step 5: + // If the playback has ended and the direction of playback is forwards, + // seek to the earliest possible position of the media resource. + // Even if we just did Load() or ResumeLoad(), we could already have a decoder // here if we managed to clone an existing decoder. if (mDecoder) { @@ -3239,7 +3383,14 @@ HTMLMediaElement::PlayInternal() if (!mPausedForInactiveDocumentOrChannel) { nsresult rv = mDecoder->Play(); if (NS_FAILED(rv)) { - return rv; + // We don't need to remove the _promise_ from _mPendingPlayPromises_ here. + // If something wrong between |mPendingPlayPromises.AppendElement(promise);| + // and here, the _promise_ should already have been rejected. Otherwise, + // the _promise_ won't be returned to JS at all, so just leave it in the + // _mPendingPlayPromises_ and let it be resolved/rejected with the + // following actions and the promise-resolution won't be observed at all. + aRv.Throw(rv); + return nullptr; } } } @@ -3248,7 +3399,7 @@ HTMLMediaElement::PlayInternal() mCurrentPlayRangeStart = CurrentTime(); } - bool oldPaused = mPaused; + const bool oldPaused = mPaused; mPaused = false; mAutoplaying = false; SetAudioChannelSuspended(nsISuspendedTypes::NONE_SUSPENDED); @@ -3265,8 +3416,27 @@ HTMLMediaElement::PlayInternal() // media, the event would be pending until media is resumed. // TODO: If the playback has ended, then the user agent must set // seek to the effective start. + + // 4.8.12.8 - Step 6: + // If the media element's paused attribute is true, run the following steps: if (oldPaused) { + // 6.1. Change the value of paused to false. (Already done.) + // This step is uplifted because the "block-media-playback" feature needs + // the mPaused to be false before UpdateAudioChannelPlayingState() being + // called. + + // 6.2. If the show poster flag is true, set the element's show poster flag + // to false and run the time marches on steps. + + // 6.3. Queue a task to fire a simple event named play at the element. DispatchAsyncEvent(NS_LITERAL_STRING("play")); + + // 6.4. If the media element's readyState attribute has the value + // HAVE_NOTHING, HAVE_METADATA, or HAVE_CURRENT_DATA, queue a task to + // fire a simple event named waiting at the element. + // Otherwise, the media element's readyState attribute has the value + // HAVE_FUTURE_DATA or HAVE_ENOUGH_DATA: notify about playing for the + // element. switch (mReadyState) { case nsIDOMHTMLMediaElement::HAVE_NOTHING: DispatchAsyncEvent(NS_LITERAL_STRING("waiting")); @@ -3279,12 +3449,20 @@ HTMLMediaElement::PlayInternal() case nsIDOMHTMLMediaElement::HAVE_FUTURE_DATA: case nsIDOMHTMLMediaElement::HAVE_ENOUGH_DATA: FireTimeUpdate(false); - DispatchAsyncEvent(NS_LITERAL_STRING("playing")); + NotifyAboutPlaying(); break; } + } else if (mReadyState >= nsIDOMHTMLMediaElement::HAVE_FUTURE_DATA) { + // 7. Otherwise, if the media element's readyState attribute has the value + // HAVE_FUTURE_DATA or HAVE_ENOUGH_DATA, take pending play promises and + // queue a task to resolve pending play promises with the result. + AsyncResolvePendingPlayPromises(); } - return NS_OK; + // 8. Set the media element's autoplaying flag to false. (Already done.) + + // 9. Return promise. + return promise.forget(); } void @@ -3302,9 +3480,9 @@ NS_IMETHODIMP HTMLMediaElement::Play() return NS_OK; } - nsresult rv = PlayInternal(); - if (NS_FAILED(rv)) { - return rv; + RefPtr<Promise> toBeIgnored = PlayInternal(rv); + if (rv.Failed()) { + return rv.StealNSResult(); } OpenUnsupportedMediaWithExternalAppIfNeeded(); @@ -4463,7 +4641,12 @@ void HTMLMediaElement::PlaybackEnded() return; } - Pause(); + FireTimeUpdate(false); + + if (!mPaused) { + Pause(); + AsyncRejectPendingPlayPromises(NS_ERROR_DOM_MEDIA_ABORT_ERR); + } if (mSrcStream) { // A MediaStream that goes from inactive to active shall be eligible for @@ -4471,7 +4654,6 @@ void HTMLMediaElement::PlaybackEnded() mAutoplaying = true; } - FireTimeUpdate(false); DispatchAsyncEvent(NS_LITERAL_STRING("ended")); } @@ -4865,7 +5047,7 @@ void HTMLMediaElement::ChangeReadyState(nsMediaReadyState aState) DispatchAsyncEvent(NS_LITERAL_STRING("canplay")); if (!mPaused) { mWaitingForKey = NOT_WAITING_FOR_KEY; - DispatchAsyncEvent(NS_LITERAL_STRING("playing")); + NotifyAboutPlaying(); } } @@ -5111,7 +5293,14 @@ nsresult HTMLMediaElement::DispatchAsyncEvent(const nsAString& aName) return NS_OK; } - nsCOMPtr<nsIRunnable> event = new nsAsyncEventRunner(aName, this); + nsCOMPtr<nsIRunnable> event; + + if (aName.EqualsLiteral("playing")) { + event = new nsNotifyAboutPlayingRunner(this, TakePendingPlayPromises()); + } else { + event = new nsAsyncEventRunner(aName, this); + } + NS_DispatchToMainThread(event); return NS_OK; @@ -6601,5 +6790,64 @@ HTMLMediaElement::MarkAsContentSource(CallerAPI aAPI) } } +nsTArray<RefPtr<Promise>> +HTMLMediaElement::TakePendingPlayPromises() +{ + return Move(mPendingPlayPromises); +} + +void +HTMLMediaElement::NotifyAboutPlaying() +{ + // Stick to the DispatchAsyncEvent() call path for now because we want to + // trigger some telemetry-related codes in the DispatchAsyncEvent() method. + DispatchAsyncEvent(NS_LITERAL_STRING("playing")); +} + +already_AddRefed<Promise> +HTMLMediaElement::CreateDOMPromise(ErrorResult& aRv) const +{ + nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(OwnerDoc()->GetInnerWindow()); + + if (!global) { + aRv.Throw(NS_ERROR_UNEXPECTED); + return nullptr; + } + + return Promise::Create(global, aRv); +} + +void +HTMLMediaElement::AsyncResolvePendingPlayPromises() +{ + if (mShuttingDown) { + return; + } + + nsCOMPtr<nsIRunnable> event + = new nsResolveOrRejectPendingPlayPromisesRunner(this, + TakePendingPlayPromises()); + + NS_DispatchToMainThread(event); +} + +void +HTMLMediaElement::AsyncRejectPendingPlayPromises(nsresult aError) +{ + if (mShuttingDown) { + return; + } + + nsCOMPtr<nsIRunnable> event + = new nsResolveOrRejectPendingPlayPromisesRunner(this, + TakePendingPlayPromises(), + aError); + + NS_DispatchToMainThread(event); + +} + + + } // namespace dom } // namespace mozilla diff --git a/dom/html/HTMLMediaElement.h b/dom/html/HTMLMediaElement.h index 899e8449a..44c666f95 100644 --- a/dom/html/HTMLMediaElement.h +++ b/dom/html/HTMLMediaElement.h @@ -16,7 +16,6 @@ #include "DecoderTraits.h" #include "nsIAudioChannelAgent.h" #include "mozilla/Attributes.h" -#include "mozilla/dom/Promise.h" #include "mozilla/dom/TextTrackManager.h" #include "mozilla/WeakPtr.h" #include "MediaDecoder.h" @@ -78,6 +77,7 @@ namespace dom { class MediaError; class MediaSource; +class Promise; class TextTrackList; class AudioTrackList; class VideoTrackList; @@ -549,7 +549,7 @@ public: SetHTMLBoolAttr(nsGkAtoms::loop, aValue, aRv); } - void Play(ErrorResult& aRv); + already_AddRefed<Promise> Play(ErrorResult& aRv); void Pause(ErrorResult& aRv); @@ -835,7 +835,7 @@ protected: nsTArray<Pair<nsString, RefPtr<MediaInputPort>>> mTrackPorts; }; - nsresult PlayInternal(); + already_AddRefed<Promise> PlayInternal(ErrorResult& aRv); /** Use this method to change the mReadyState member, so required * events can be fired. @@ -981,6 +981,7 @@ protected: void AbortExistingLoads(); /** + * These are the dedicated media source failure steps. * Called when all potential resources are exhausted. Changes network * state to NETWORK_NO_SOURCE, and sends error event with code * MEDIA_ERR_SRC_NOT_SUPPORTED. @@ -1285,6 +1286,8 @@ protected: void MaybeNotifyMediaResumed(SuspendTypes aSuspend); class nsAsyncEventRunner; + class nsNotifyAboutPlayingRunner; + class nsResolveOrRejectPendingPlayPromisesRunner; using nsGenericHTMLElement::DispatchEvent; // For nsAsyncEventRunner. nsresult DispatchEvent(const nsAString& aName); @@ -1293,6 +1296,24 @@ protected: // triggers play() after loaded fail. eg. preload the data before start play. void OpenUnsupportedMediaWithExternalAppIfNeeded() const; + // This method moves the mPendingPlayPromises into a temperate object. So the + // mPendingPlayPromises is cleared after this method call. + nsTArray<RefPtr<Promise>> TakePendingPlayPromises(); + + // This method snapshots the mPendingPlayPromises by TakePendingPlayPromises() + // and queues a task to resolve them. + void AsyncResolvePendingPlayPromises(); + + // This method snapshots the mPendingPlayPromises by TakePendingPlayPromises() + // and queues a task to reject them. + void AsyncRejectPendingPlayPromises(nsresult aError); + + // This method snapshots the mPendingPlayPromises by TakePendingPlayPromises() + // and queues a task to resolve them also to dispatch a "playing" event. + void NotifyAboutPlaying(); + + already_AddRefed<Promise> CreateDOMPromise(ErrorResult& aRv) const; + // The current decoder. Load() has been called on this decoder. // At most one of mDecoder and mSrcStream can be non-null. RefPtr<MediaDecoder> mDecoder; @@ -1684,6 +1705,17 @@ private: Visibility mVisibilityState; UniquePtr<ErrorSink> mErrorSink; + + // A list of pending play promises. The elements are pushed during the play() + // method call and are resolved/rejected during further playback steps. + nsTArray<RefPtr<Promise>> mPendingPlayPromises; + + // A list of already-dispatched but not yet run + // nsResolveOrRejectPendingPlayPromisesRunners. + // Runners whose Run() method is called remove themselves from this list. + // We keep track of these because the load algorithm resolves/rejects all + // already-dispatched pending play promises. + nsTArray<nsResolveOrRejectPendingPlayPromisesRunner*> mPendingPlayPromisesRunners; }; } // namespace dom diff --git a/dom/webidl/HTMLMediaElement.webidl b/dom/webidl/HTMLMediaElement.webidl index 313686dac..be79d8679 100644 --- a/dom/webidl/HTMLMediaElement.webidl +++ b/dom/webidl/HTMLMediaElement.webidl @@ -68,7 +68,7 @@ interface HTMLMediaElement : HTMLElement { [SetterThrows] attribute boolean loop; [Throws] - void play(); + Promise<void> play(); [Throws] void pause(); |