summaryrefslogtreecommitdiffstats
path: root/dom/html
diff options
context:
space:
mode:
authorathenian200 <athenian200@outlook.com>2019-12-23 09:09:57 -0600
committerathenian200 <athenian200@outlook.com>2019-12-23 09:27:09 -0600
commit735418fa363d2556d0ce40f0b4e67ff2bfc78b59 (patch)
tree6a3e7b77783e9c8d7e0f8d18e791f6cdfdbb4e15 /dom/html
parent84f4806b4656253faefd534cb59da27241608752 (diff)
downloadUXP-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.
Diffstat (limited to 'dom/html')
-rw-r--r--dom/html/HTMLMediaElement.cpp304
-rw-r--r--dom/html/HTMLMediaElement.h38
2 files changed, 311 insertions, 31 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