Bug 1463919 - Have HTMLMediaElement ask for autoplay permission when playback otherwise blocked. r=jya
MozReview-Commit-ID: Ejv0UKBjSVf
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1442,16 +1442,18 @@ pref("media.gmp.trial-create.enabled", t
// to enable the CDM if its disabled; it's as if the keysystem is completely
// unsupported.
#ifdef MOZ_WIDEVINE_EME
pref("media.gmp-widevinecdm.visible", true);
pref("media.gmp-widevinecdm.enabled", true);
#endif
+pref("media.autoplay.ask-permission", false);
+
// Play with different values of the decay time and get telemetry,
// 0 means to randomize (and persist) the experiment value in users' profiles,
// -1 means no experiment is run and we use the preferred value for frecency (6h)
pref("browser.cache.frecency_experiment", 0);
pref("browser.translation.detectLanguage", false);
pref("browser.translation.neverForLanguages", "");
// Show the translation UI bits, like the info bar, notification icon and preferences.
--- a/dom/html/HTMLMediaElement.cpp
+++ b/dom/html/HTMLMediaElement.cpp
@@ -53,16 +53,17 @@
#include "mozilla/MathAlgorithms.h"
#include "mozilla/NotNull.h"
#include "mozilla/Preferences.h"
#include "mozilla/Sprintf.h"
#include "mozilla/StaticPrefs.h"
#include "mozilla/Telemetry.h"
#include "mozilla/dom/AudioTrack.h"
#include "mozilla/dom/AudioTrackList.h"
+#include "mozilla/dom/AutoplayRequest.h"
#include "mozilla/dom/BlobURLProtocolHandler.h"
#include "mozilla/dom/ElementInlines.h"
#include "mozilla/dom/HTMLAudioElement.h"
#include "mozilla/dom/HTMLInputElement.h"
#include "mozilla/dom/HTMLMediaElementBinding.h"
#include "mozilla/dom/HTMLSourceElement.h"
#include "mozilla/dom/HTMLVideoElement.h"
#include "mozilla/dom/MediaEncryptedEvent.h"
@@ -1839,16 +1840,20 @@ HTMLMediaElement::AbortExistingLoads()
// we destroyed the decoder, so fire a timeupdate event so that the
// change will be reflected in the controls.
FireTimeUpdate(false);
}
DispatchAsyncEvent(NS_LITERAL_STRING("emptied"));
UpdateAudioChannelPlayingState();
}
+ // Disconnect requests for permission to play. We'll make a new request
+ // if required should the new media resource try to play.
+ mAutoplayPermissionRequest.DisconnectIfExists();
+
// We may have changed mPaused, mAutoplaying, and other
// things which can affect AddRemoveSelfReference
AddRemoveSelfReference();
mIsRunningSelectResource = false;
if (mTextTrackManager) {
mTextTrackManager->NotifyReset();
@@ -3914,16 +3919,17 @@ HTMLMediaElement::~HTMLMediaElement()
mShutdownObserver->Unsubscribe();
if (mVideoFrameContainer) {
mVideoFrameContainer->ForgetElement();
}
UnregisterActivityObserver();
mSetCDMRequest.DisconnectIfExists();
+ mAutoplayPermissionRequest.DisconnectIfExists();
if (mDecoder) {
ShutdownDecoder();
}
if (mProgressTimer) {
StopProgress();
}
if (mVideoDecodeSuspendTimer) {
mVideoDecodeSuspendTimer->Cancel();
@@ -3986,89 +3992,144 @@ HTMLMediaElement::SetPlayedOrSeeked(bool
}
void
HTMLMediaElement::NotifyXPCOMShutdown()
{
ShutdownDecoder();
}
+bool
+HTMLMediaElement::AudioChannelAgentDelayingPlayback()
+{
+ return mAudioChannelWrapper && mAudioChannelWrapper->IsPlaybackBlocked();
+}
+
already_AddRefed<Promise>
HTMLMediaElement::Play(ErrorResult& aRv)
{
LOG(LogLevel::Debug,
("%p Play() called by JS readyState=%d", this, mReadyState));
- if (mAudioChannelWrapper && mAudioChannelWrapper->IsPlaybackBlocked()) {
- MaybeDoLoad();
-
- // 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<PlayPromise> promise = CreatePlayPromise(aRv);
-
- if (NS_WARN_IF(aRv.Failed())) {
- return nullptr;
- }
-
- LOG(LogLevel::Debug, ("%p Play() call delayed by AudioChannelAgent", this));
-
- mPendingPlayPromises.AppendElement(promise);
- return promise.forget();
- }
-
- RefPtr<Promise> promise = PlayInternal(aRv);
-
- UpdateCustomPolicyAfterPlayed();
-
- return promise.forget();
-}
-
-already_AddRefed<Promise>
-HTMLMediaElement::PlayInternal(ErrorResult& aRv)
-{
- MOZ_ASSERT(!aRv.Failed());
-
- RefPtr<PlayPromise> promise = CreatePlayPromise(aRv);
-
- if (NS_WARN_IF(aRv.Failed())) {
- return nullptr;
- }
-
// 4.8.12.8
// When the play() method on a media element is invoked, the user agent must
// run the following steps.
+ RefPtr<PlayPromise> promise = CreatePlayPromise(aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+
// 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.
- LOG(LogLevel::Debug,
- ("%p Play() promise rejected because not allowed to play.", this));
- promise->MaybeReject(NS_ERROR_DOM_MEDIA_NOT_ALLOWED_ERR);
- return promise.forget();
- }
+ // NOTE: we may require requesting permission from the user, so we do the
+ // "not allowed" check below.
// 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) {
LOG(LogLevel::Debug,
("%p Play() promise rejected because source not supported.", this));
promise->MaybeReject(NS_ERROR_DOM_MEDIA_NOT_SUPPORTED_ERR);
return promise.forget();
}
// 4.8.12.8 - Step 3:
// Let promise be a new promise and append promise to the list of pending
// play promises.
+ // Note: Promise appended to list of pending promises as needed below.
+
+ if (AudioChannelAgentDelayingPlayback()) {
+ // The audio channel agent may delay starting playback of a media resource
+ // until the tab the media element is in has been in the foreground.
+ // Save a reference to the promise, and return it. The AudioChannelAgent
+ // will call Play() again if the tab is brought to the foreground, or the
+ // audio tab indicator is clicked, which will resolve the promise if we end
+ // up playing.
+ LOG(LogLevel::Debug, ("%p Play() call delayed by AudioChannelAgent", this));
+ MaybeDoLoad();
+ mPendingPlayPromises.AppendElement(promise);
+ return promise.forget();
+ }
+
+ const bool handlingUserInput = EventStateManager::IsHandlingUserInput();
+ if (IsAllowedToPlay()) {
+ mPendingPlayPromises.AppendElement(promise);
+ PlayInternal(handlingUserInput);
+ UpdateCustomPolicyAfterPlayed();
+ return promise.forget();
+ }
+
+ // Otherwise, not allowed to play. We may still be allowed to play if we
+ // ask for and are granted permission by the user.
+
+ if (!Preferences::GetBool("media.autoplay.ask-permission", false)) {
+ LOG(LogLevel::Debug, ("%p play not allowed and prompting disabled.", this));
+ promise->MaybeReject(NS_ERROR_DOM_MEDIA_NOT_ALLOWED_ERR);
+ return promise.forget();
+ }
+
+ // Prompt the user for permission to play.
mPendingPlayPromises.AppendElement(promise);
-
+ EnsureAutoplayRequested(handlingUserInput);
+ return promise.forget();
+}
+
+void
+HTMLMediaElement::EnsureAutoplayRequested(bool aHandlingUserInput)
+{
+ if (mAutoplayPermissionRequest.Exists()) {
+ // Autoplay has already been requested in a previous play() call.
+ // Await for the previous request to be approved or denied. This
+ // play request's promise will be fulfilled with all other pending
+ // promises when the permission prompt is resolved.
+ LOG(LogLevel::Debug,
+ ("%p EnsureAutoplayRequested() existing request, bailing.", this));
+ return;
+ }
+
+ RefPtr<AutoplayRequest> request =
+ AutoplayPolicy::RequestFor(WrapNotNull(OwnerDoc()));
+ if (!request) {
+ AsyncRejectPendingPlayPromises(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return;
+ }
+ RefPtr<HTMLMediaElement> self = this;
+ request->RequestWithPrompt()
+ ->Then(mAbstractMainThread,
+ __func__,
+ [ self, handlingUserInput = aHandlingUserInput, request ](
+ bool aApproved) {
+ self->mAutoplayPermissionRequest.Complete();
+ LOG(LogLevel::Debug,
+ ("%p Autoplay request approved request=%p",
+ self.get(),
+ request.get()));
+ self->PlayInternal(handlingUserInput);
+ self->UpdateCustomPolicyAfterPlayed();
+ },
+ [self, request](nsresult aError) {
+ self->mAutoplayPermissionRequest.Complete();
+ LOG(LogLevel::Debug,
+ ("%p Autoplay request denied request=%p",
+ self.get(),
+ request.get()));
+ LOG(LogLevel::Debug, ("%s rejecting play promimses", __func__));
+ self->AsyncRejectPendingPlayPromises(
+ NS_ERROR_DOM_MEDIA_NOT_ALLOWED_ERR);
+ })
+ ->Track(mAutoplayPermissionRequest);
+}
+
+void
+HTMLMediaElement::PlayInternal(bool aHandlingUserInput)
+{
if (mPreloadAction == HTMLMediaElement::PRELOAD_NONE) {
// The media load algorithm will be initiated by a user interaction.
// We want to boost the channel priority for better responsiveness.
// Note this must be done before UpdatePreloadAction() which will
// update |mPreloadAction|.
mUseUrgentStartForChannel = true;
}
@@ -4111,17 +4172,17 @@ HTMLMediaElement::PlayInternal(ErrorResu
AddRemoveSelfReference();
UpdatePreloadAction();
UpdateSrcMediaStreamPlaying();
// Once play() has been called in a user generated event handler,
// it is allowed to autoplay. Note: we can reach here when not in
// a user generated event handler if our readyState has not yet
// reached HAVE_METADATA.
- mIsBlessed |= EventStateManager::IsHandlingUserInput();
+ mIsBlessed |= aHandlingUserInput;
// 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.)
@@ -4161,17 +4222,17 @@ HTMLMediaElement::PlayInternal(ErrorResu
// HAVE_FUTURE_DATA or HAVE_ENOUGH_DATA, take pending play promises and
// queue a task to resolve pending play promises with the result.
AsyncResolvePendingPlayPromises();
}
// 8. Set the media element's autoplaying flag to false. (Already done.)
// 9. Return promise.
- return promise.forget();
+ // (Done in caller.)
}
void
HTMLMediaElement::MaybeDoLoad()
{
if (mNetworkState == NETWORK_EMPTY) {
DoLoad();
}
@@ -6048,16 +6109,17 @@ HTMLMediaElement::ChangeReadyState(nsMed
DispatchAsyncEvent(NS_LITERAL_STRING("loadeddata"));
mLoadedDataFired = true;
}
if (oldState < HAVE_FUTURE_DATA && mReadyState >= HAVE_FUTURE_DATA) {
DispatchAsyncEvent(NS_LITERAL_STRING("canplay"));
if (!mPaused) {
if (mDecoder && !mPausedForInactiveDocumentOrChannel) {
+ MOZ_ASSERT(IsAllowedToPlay());
mDecoder->Play();
}
NotifyAboutPlaying();
}
}
CheckAutoplayDataReady();
@@ -7819,16 +7881,23 @@ HTMLMediaElement::AsyncResolvePendingPla
this, TakePendingPlayPromises());
mMainThreadEventTarget->Dispatch(event.forget());
}
void
HTMLMediaElement::AsyncRejectPendingPlayPromises(nsresult aError)
{
+ mAutoplayPermissionRequest.DisconnectIfExists();
+
+ if (!mPaused) {
+ mPaused = true;
+ DispatchAsyncEvent(NS_LITERAL_STRING("pause"));
+ }
+
if (mShuttingDown) {
return;
}
nsCOMPtr<nsIRunnable> event = new nsResolveOrRejectPendingPlayPromisesRunner(
this, TakePendingPlayPromises(), aError);
mMainThreadEventTarget->Dispatch(event.forget());
--- a/dom/html/HTMLMediaElement.h
+++ b/dom/html/HTMLMediaElement.h
@@ -40,16 +40,17 @@
typedef uint16_t nsMediaNetworkState;
typedef uint16_t nsMediaReadyState;
typedef uint32_t SuspendTypes;
typedef uint32_t AudibleChangedReasons;
typedef uint8_t AudibleState;
namespace mozilla {
class AbstractThread;
+class AutoplayRequest;
class ChannelMediaDecoder;
class DecoderDoctorDiagnostics;
class DOMMediaStream;
class ErrorResult;
class MediaResource;
class MediaDecoder;
class MediaInputPort;
class MediaStream;
@@ -858,17 +859,17 @@ protected:
// Tracks that were created on main thread before MediaDecoder fed them
// to the MediaStreamGraph.
nsTArray<RefPtr<MediaStreamTrack>> mPreCreatedTracks;
// The following members are keeping state for a captured MediaStream.
nsTArray<Pair<nsString, RefPtr<MediaInputPort>>> mTrackPorts;
};
- already_AddRefed<Promise> PlayInternal(ErrorResult& aRv);
+ void PlayInternal(bool aHandlingUserInput);
/** Use this method to change the mReadyState member, so required
* events can be fired.
*/
void ChangeReadyState(nsMediaReadyState aState);
/**
* Use this method to change the mNetworkState member, so required
@@ -1351,16 +1352,24 @@ protected:
void MakeAssociationWithCDMResolved();
void SetCDMProxyFailure(const MediaResult& aResult);
void ResetSetMediaKeysTempVariables();
void PauseIfShouldNotBePlaying();
WatchManager<HTMLMediaElement> mWatchManager;
+ // If the media element's tab has never been in the foreground, this
+ // registers as with the AudioChannelAgent to notify us when the tab
+ // is put in the foreground, whereupon we will begin playback.
+ bool AudioChannelAgentDelayingPlayback();
+
+ // Ensures we're prompting the user for permission to autoplay.
+ void EnsureAutoplayRequested(bool aHandlingUserInput);
+
// The current decoder. Load() has been called on this decoder.
// At most one of mDecoder and mSrcStream can be non-null.
RefPtr<MediaDecoder> mDecoder;
// The DocGroup-specific nsISerialEventTarget of this HTML element on the main
// thread.
nsCOMPtr<nsISerialEventTarget> mMainThreadEventTarget;
@@ -1558,16 +1567,19 @@ protected:
// 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 = false;
MozPromiseRequestHolder<SetCDMPromise> mSetCDMRequest;
+ // Request holder for permission prompt to autoplay. Non-null if we're
+ // currently showing a prompt for permission to autoplay.
+ MozPromiseRequestHolder<GenericPromise> mAutoplayPermissionRequest;
// Stores the time at the start of the current 'played' range.
double mCurrentPlayRangeStart = 1.0;
// True if loadeddata has been fired.
bool mLoadedDataFired = false;
// Indicates whether current playback is a result of user action