Implement promise-based media playback.

This commit is contained in:
Fedor 2020-03-12 20:39:56 +03:00
parent 7fd3f667aa
commit 159202da62
4 changed files with 313 additions and 38 deletions

View File

@ -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);
if (NS_WARN_IF(aRv.Failed())) {
return nullptr;
}
mPendingPlayPromises.AppendElement(promise);
return promise.forget();
}
nsresult rv = PlayInternal();
if (NS_FAILED(rv)) {
aRv.Throw(rv);
}
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,10 @@ NS_IMETHODIMP HTMLMediaElement::Play()
return NS_OK;
}
nsresult rv = PlayInternal();
if (NS_FAILED(rv)) {
return rv;
ErrorResult rv;
RefPtr<Promise> toBeIgnored = PlayInternal(rv);
if (rv.Failed()) {
return rv.StealNSResult();
}
OpenUnsupportedMediaWithExternalAppIfNeeded();
@ -4463,7 +4642,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 +4655,6 @@ void HTMLMediaElement::PlaybackEnded()
mAutoplaying = true;
}
FireTimeUpdate(false);
DispatchAsyncEvent(NS_LITERAL_STRING("ended"));
}
@ -4865,7 +5048,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 +5294,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 +6791,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

View File

@ -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

View File

@ -68,7 +68,7 @@ interface HTMLMediaElement : HTMLElement {
[SetterThrows]
attribute boolean loop;
[Throws]
void play();
Promise<void> play();
[Throws]
void pause();

View File

@ -5427,13 +5427,7 @@ pref("dom.node.rootNode.enabled", true);
// Default media volume
pref("media.default_volume", "1.0");
// Once bug 1276272 is resolved, we will trun this preference to default ON in
// non-release channels.
#ifdef RELEASE_OR_BETA
pref("media.seekToNextFrame.enabled", false);
#else
pref("media.seekToNextFrame.enabled", true);
#endif
// return the maximum number of cores that navigator.hardwareCurrency returns
pref("dom.maxHardwareConcurrency", 16);