Bug 1463919 - Have HTMLMediaElement ask for autoplay permission when playback otherwise blocked. r=jya

MozReview-Commit-ID: Ejv0UKBjSVf

--HG--
extra : rebase_source : f999b9a4a1ae7a5a7f1dd31efd3003e40d7fa102
This commit is contained in:
Chris Pearce 2018-06-22 10:14:33 +12:00
parent bbbdebe6e1
commit b309d364b6
3 changed files with 130 additions and 47 deletions

View File

@ -1447,6 +1447,8 @@ 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)

View File

@ -58,6 +58,7 @@
#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"
@ -1844,6 +1845,10 @@ HTMLMediaElement::AbortExistingLoads()
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();
@ -3919,6 +3924,7 @@ HTMLMediaElement::~HTMLMediaElement()
UnregisterActivityObserver();
mSetCDMRequest.DisconnectIfExists();
mAutoplayPermissionRequest.DisconnectIfExists();
if (mDecoder) {
ShutdownDecoder();
}
@ -3991,62 +3997,32 @@ 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
@ -4062,8 +4038,93 @@ HTMLMediaElement::PlayInternal(ErrorResult& aRv)
// 4.8.12.8 - Step 3:
// Let promise be a new promise and append promise to the list of pending
// play promises.
mPendingPlayPromises.AppendElement(promise);
// 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.
@ -4116,7 +4177,7 @@ HTMLMediaElement::PlayInternal(ErrorResult& aRv)
// 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.
@ -4166,7 +4227,7 @@ HTMLMediaElement::PlayInternal(ErrorResult& aRv)
// 8. Set the media element's autoplaying flag to false. (Already done.)
// 9. Return promise.
return promise.forget();
// (Done in caller.)
}
void
@ -6053,6 +6114,7 @@ HTMLMediaElement::ChangeReadyState(nsMediaReadyState aState)
DispatchAsyncEvent(NS_LITERAL_STRING("canplay"));
if (!mPaused) {
if (mDecoder && !mPausedForInactiveDocumentOrChannel) {
MOZ_ASSERT(IsAllowedToPlay());
mDecoder->Play();
}
NotifyAboutPlaying();
@ -7824,6 +7886,13 @@ HTMLMediaElement::AsyncResolvePendingPlayPromises()
void
HTMLMediaElement::AsyncRejectPendingPlayPromises(nsresult aError)
{
mAutoplayPermissionRequest.DisconnectIfExists();
if (!mPaused) {
mPaused = true;
DispatchAsyncEvent(NS_LITERAL_STRING("pause"));
}
if (mShuttingDown) {
return;
}

View File

@ -45,6 +45,7 @@ typedef uint8_t AudibleState;
namespace mozilla {
class AbstractThread;
class AutoplayRequest;
class ChannelMediaDecoder;
class DecoderDoctorDiagnostics;
class DOMMediaStream;
@ -863,7 +864,7 @@ protected:
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.
@ -1356,6 +1357,14 @@ protected:
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;
@ -1563,6 +1572,9 @@ protected:
// 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;