Bug 1244768 part 1 - implement utilities which are the foundation of promise-based play operation; r?jwwang
In this patch, the following utilities are implemented:
(1) A list to keep pending promises of a HTMLMediaElement.
(2) A method to take pending promises out from the HTMLMediaElement.
(3) A global function to resolve the passed promises.
(4) A global function to reject the passed promises with an error code.
(5) A method to asynchronously resolve all pending promises of a HTMLMediaElement.
(6) A method to asynchronously reject all pending promises of a HTMLMediaElement.
(7) A method to dispatch a 'playing' event and resolve all the pending play promises.
All the above functionalities are defined at WHATWG 4.8.12.8:
https://html.spec.whatwg.org/multipage/embedded-content.html#list-of-pending-play-promises
This patch also implements two MediaEvent classes, nsResolveOrRejectPendingPlayPromisesRunner and nsNotifyAboutPlayingRunner, which help (5), (6) and (7).
This patch also implements a list of already-dispatched nsResolveOrRejectPendingPlayPromisesRunner; we keep tracing these tasks because the load algorithm resolves/rejects all already-dispatched pending play promises (in patch 2).
MozReview-Commit-ID: EUirNqDfttk
--- a/dom/html/HTMLMediaElement.cpp
+++ b/dom/html/HTMLMediaElement.cpp
@@ -3,16 +3,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/. */
#include "mozilla/dom/HTMLMediaElement.h"
#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"
#include "mozilla/dom/MediaEncryptedEvent.h"
#include "mozilla/EMEUtils.h"
#include "base/basictypes.h"
#include "nsIDOMHTMLMediaElement.h"
@@ -148,16 +149,32 @@ static const double THRESHOLD_HIGH_PLAYB
static const double THRESHOLD_LOW_PLAYBACKRATE_AUDIO = 0.5;
// Media error values. These need to match the ones in MediaError.webidl.
static const unsigned short MEDIA_ERR_ABORTED = 1;
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:
// 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
@@ -238,16 +255,84 @@ public:
// Silently cancel if our load has been cancelled.
if (IsCancelled())
return NS_OK;
return mElement->DispatchEvent(mName);
}
};
+/*
+ * 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:
nsCOMPtr<nsIContent> mSource;
public:
nsSourceErrorEventRunner(HTMLMediaElement* aElement,
nsIContent* aSource)
: nsMediaEvent(aElement),
@@ -1276,16 +1361,17 @@ NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mOutputStreams[i].mStream);
}
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_END
NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(HTMLMediaElement, nsGenericHTMLElement)
if (tmp->mSrcStream) {
// Need to EndMediaStreamPlayback to clear mSrcStream and make sure everything
// gets unhooked correctly.
tmp->EndSrcMediaStreamPlayback();
}
@@ -1304,16 +1390,17 @@ NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_IN
NS_IMPL_CYCLE_COLLECTION_UNLINK(mOutputStreams[i].mStream)
}
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_END
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(HTMLMediaElement)
NS_INTERFACE_MAP_ENTRY(nsIDOMHTMLMediaElement)
NS_INTERFACE_MAP_END_INHERITING(nsGenericHTMLElement)
// nsIDOMHTMLMediaElement
NS_IMPL_URI_ATTR(HTMLMediaElement, Src, src)
@@ -5683,17 +5770,24 @@ nsresult HTMLMediaElement::DispatchAsync
// Save events that occur while in the bfcache. These will be dispatched
// if the page comes out of the bfcache.
if (mEventDeliveryPaused) {
mPendingEvents.AppendElement(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);
+ }
+
OwnerDoc()->Dispatch("HTMLMediaElement::DispatchAsyncEvent",
TaskCategory::Other,
event.forget());
if ((aName.EqualsLiteral("play") || aName.EqualsLiteral("playing"))) {
mPlayTime.Start();
if (IsHidden()) {
HiddenVideoStart();
@@ -6910,10 +7004,58 @@ void
HTMLMediaElement::UpdateCustomPolicyAfterPlayed()
{
OpenUnsupportedMediaWithExternalAppIfNeeded();
if (mAudioChannelWrapper) {
mAudioChannelWrapper->NotifyPlayStarted();
}
}
+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"));
+}
+
+void
+HTMLMediaElement::AsyncResolvePendingPlayPromises()
+{
+ if (mShuttingDown) {
+ return;
+ }
+
+ nsCOMPtr<nsIRunnable> event
+ = new nsResolveOrRejectPendingPlayPromisesRunner(this,
+ TakePendingPlayPromises());
+
+ OwnerDoc()->Dispatch("HTMLMediaElement::AsyncResolvePendingPlayPromises",
+ TaskCategory::Other,
+ event.forget());
+}
+
+void
+HTMLMediaElement::AsyncRejectPendingPlayPromises(nsresult aError)
+{
+ if (mShuttingDown) {
+ return;
+ }
+
+ nsCOMPtr<nsIRunnable> event
+ = new nsResolveOrRejectPendingPlayPromisesRunner(this,
+ TakePendingPlayPromises(),
+ aError);
+
+ OwnerDoc()->Dispatch("HTMLMediaElement::AsyncRejectPendingPlayPromises",
+ TaskCategory::Other,
+ event.forget());
+}
+
+
} // namespace dom
} // namespace mozilla
--- a/dom/html/HTMLMediaElement.h
+++ b/dom/html/HTMLMediaElement.h
@@ -11,17 +11,16 @@
#include "nsGenericHTMLElement.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/Promise.h"
#include "mozilla/dom/TextTrackManager.h"
#include "mozilla/WeakPtr.h"
#include "MediaDecoder.h"
#include "mozilla/dom/MediaKeys.h"
#include "mozilla/StateWatching.h"
#include "nsGkAtoms.h"
#include "PrincipalChangeObserver.h"
@@ -68,16 +67,17 @@ class nsRange;
namespace mozilla {
namespace dom {
// Number of milliseconds between timeupdate events as defined by spec
#define TIMEUPDATE_MS 250
class MediaError;
class MediaSource;
+class Promise;
class TextTrackList;
class AudioTrackList;
class VideoTrackList;
class HTMLMediaElement : public nsGenericHTMLElement,
public nsIDOMHTMLMediaElement,
public MediaDecoderOwner,
public PrincipalChangeObserver<DOMMediaStream>,
@@ -1226,23 +1226,41 @@ protected:
// If the network state is empty and then we would trigger DoLoad().
void MaybeDoLoad();
// Anything we need to check after played success and not related with spec.
void UpdateCustomPolicyAfterPlayed();
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
// 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();
// The current decoder. Load() has been called on this decoder.
// At most one of mDecoder and mSrcStream can be non-null.
RefPtr<MediaDecoder> mDecoder;
// Observers listening to changes to the mDecoder principal.
// Used by streams captured from this element.
nsTArray<DecoderPrincipalChangeObserver*> mDecoderPrincipalChangeObservers;
@@ -1666,14 +1684,25 @@ private:
Visibility mVisibilityState;
UniquePtr<ErrorSink> mErrorSink;
// This wrapper will handle all audio channel related stuffs, eg. the operations
// of tab audio indicator, Fennec's media control.
// Note: mAudioChannelWrapper might be null after GC happened.
RefPtr<AudioChannelAgentCallback> mAudioChannelWrapper;
+
+ // 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
} // namespace mozilla
#endif // mozilla_dom_HTMLMediaElement_h