0

[Blink>Media] Moving autoplay logic to AutoplayPolicy

This CL moves autoplay code from HTMLMediaElement to the
AutoplayPolicy class, which helps decoupling the autoplay logic
from HTMLMediaElement.

BUG=712606

Review-Url: https://codereview.chromium.org/2813303005
Cr-Commit-Position: refs/heads/master@{#466137}
This commit is contained in:
zqzhang
2017-04-20 14:24:05 -07:00
committed by Commit bot
parent ac9a37e939
commit 12d76adf4b
9 changed files with 480 additions and 291 deletions

@ -14,6 +14,13 @@ There are two ways of initiating autoplay:
* Autoplay by `play()` method: Explicitly calling the `play()` method without
user gesture.
All the autoplay logic is handled by the AutoplayPolicy class. When the media
element wants to perform some action (like unmute, autoplay by attribute or
`play()` method), it will send a request to AutoplayPolicy, and if the request
is approved, the element can autoplay, otherwise it should be paused. Also the
media element should inform the AutoplayPolicy about relevant changes such as
"the element has been moved to a new document".
## User gesture lock
Each media element has a user gesture lock. If the element is allowed to

@ -389,6 +389,8 @@ blink_core_sources("html") {
"imports/HTMLImportsController.h",
"imports/LinkImport.cpp",
"imports/LinkImport.h",
"media/AutoplayPolicy.cpp",
"media/AutoplayPolicy.h",
"media/AutoplayUmaHelper.cpp",
"media/AutoplayUmaHelper.h",
"media/HTMLMediaElementControlsList.cpp",

@ -36,14 +36,11 @@
#include "core/css/MediaList.h"
#include "core/dom/Attribute.h"
#include "core/dom/DOMException.h"
#include "core/dom/DocumentUserGestureToken.h"
#include "core/dom/ElementTraversal.h"
#include "core/dom/ElementVisibilityObserver.h"
#include "core/dom/Fullscreen.h"
#include "core/dom/TaskRunnerHelper.h"
#include "core/dom/shadow/ShadowRoot.h"
#include "core/events/Event.h"
#include "core/frame/ContentSettingsClient.h"
#include "core/frame/FrameView.h"
#include "core/frame/LocalFrame.h"
#include "core/frame/LocalFrameClient.h"
@ -53,7 +50,7 @@
#include "core/html/HTMLSourceElement.h"
#include "core/html/HTMLTrackElement.h"
#include "core/html/TimeRanges.h"
#include "core/html/media/AutoplayUmaHelper.h"
#include "core/html/media/AutoplayPolicy.h"
#include "core/html/media/HTMLMediaElementControlsList.h"
#include "core/html/media/HTMLMediaSource.h"
#include "core/html/media/MediaControls.h"
@ -78,7 +75,6 @@
#include "platform/Histogram.h"
#include "platform/LayoutTestSupport.h"
#include "platform/RuntimeEnabledFeatures.h"
#include "platform/UserGestureIndicator.h"
#include "platform/audio/AudioBus.h"
#include "platform/audio/AudioSourceProviderClient.h"
#include "platform/graphics/GraphicsLayer.h"
@ -338,36 +334,6 @@ bool IsDocumentCrossOrigin(Document& document) {
return frame && frame->IsCrossOriginSubframe();
}
bool IsDocumentWhitelisted(Document& document) {
DCHECK(document.GetSettings());
const String& whitelist_scope =
document.GetSettings()->GetMediaPlaybackGestureWhitelistScope();
if (whitelist_scope.IsNull() || whitelist_scope.IsEmpty())
return false;
return document.Url().GetString().StartsWith(whitelist_scope);
}
// Return true if and only if the document settings specifies media playback
// requires user gesture.
bool ComputeLockedPendingUserGesture(Document& document) {
if (!document.GetSettings())
return false;
if (IsDocumentWhitelisted(document)) {
return false;
}
if (document.GetSettings()
->GetCrossOriginMediaPlaybackRequiresUserGesture() &&
IsDocumentCrossOrigin(document)) {
return true;
}
return document.GetSettings()->GetMediaPlaybackRequiresUserGesture();
}
std::unique_ptr<MediaControls::Factory>& MediaControlsFactory() {
DEFINE_STATIC_LOCAL(std::unique_ptr<MediaControls::Factory>,
media_controls_factory, ());
@ -505,8 +471,6 @@ HTMLMediaElement::HTMLMediaElement(const QualifiedName& tag_name,
official_playback_position_needs_update_(true),
fragment_end_time_(std::numeric_limits<double>::quiet_NaN()),
pending_action_flags_(0),
locked_pending_user_gesture_(false),
locked_pending_user_gesture_if_cross_origin_experiment_enabled_(true),
playing_(false),
should_delay_load_event_(false),
have_fired_loaded_data_(false),
@ -527,17 +491,12 @@ HTMLMediaElement::HTMLMediaElement(const QualifiedName& tag_name,
video_tracks_(this, VideoTrackList::Create(*this)),
text_tracks_(this, nullptr),
audio_source_node_(nullptr),
autoplay_uma_helper_(AutoplayUmaHelper::Create(this)),
autoplay_policy_(new AutoplayPolicy(this)),
remote_playback_client_(nullptr),
autoplay_visibility_observer_(nullptr),
media_controls_(nullptr),
controls_list_(HTMLMediaElementControlsList::Create(this)) {
BLINK_MEDIA_LOG << "HTMLMediaElement(" << (void*)this << ")";
locked_pending_user_gesture_ = ComputeLockedPendingUserGesture(document);
locked_pending_user_gesture_if_cross_origin_experiment_enabled_ =
IsDocumentCrossOrigin(document);
LocalFrame* frame = document.GetFrame();
if (frame) {
remote_playback_client_ =
@ -591,15 +550,7 @@ void HTMLMediaElement::DidMoveToNewDocument(Document& old_document) {
deferred_load_timer_.MoveToNewTaskRunner(
TaskRunnerHelper::Get(TaskType::kUnthrottled, &GetDocument()));
autoplay_uma_helper_->DidMoveToNewDocument(old_document);
// If any experiment is enabled, then we want to enable a user gesture by
// default, otherwise the experiment does nothing.
bool old_document_requires_user_gesture =
ComputeLockedPendingUserGesture(old_document);
bool new_document_requires_user_gesture =
ComputeLockedPendingUserGesture(GetDocument());
if (new_document_requires_user_gesture && !old_document_requires_user_gesture)
locked_pending_user_gesture_ = true;
autoplay_policy_->DidMoveToNewDocument(old_document);
if (should_delay_load_event_) {
GetDocument().IncrementLoadEventDelayCount();
@ -612,10 +563,6 @@ void HTMLMediaElement::DidMoveToNewDocument(Document& old_document) {
old_document.IncrementLoadEventDelayCount();
}
if (IsDocumentCrossOrigin(GetDocument()) &&
!IsDocumentCrossOrigin(old_document))
locked_pending_user_gesture_if_cross_origin_experiment_enabled_ = true;
RemoveElementFromDocumentMap(this, &old_document);
AddElementToDocumentMap(this, &GetDocument());
@ -845,10 +792,7 @@ String HTMLMediaElement::canPlayType(const String& mime_type) const {
void HTMLMediaElement::load() {
BLINK_MEDIA_LOG << "load(" << (void*)this << ")";
if (IsLockedPendingUserGesture() &&
UserGestureIndicator::UtilizeUserGesture()) {
UnlockUserGesture();
}
autoplay_policy_->TryUnlockingUserGesture();
ignore_preload_none_ = true;
InvokeLoadAlgorithm();
@ -1839,40 +1783,11 @@ void HTMLMediaElement::SetReadyState(ReadyState state) {
ScheduleNotifyPlaying();
}
// Check for autoplay, and record metrics about it if needed.
if (ShouldAutoplay()) {
autoplay_uma_helper_->OnAutoplayInitiated(AutoplaySource::kAttribute);
if (!IsGestureNeededForPlayback()) {
if (IsGestureNeededForPlaybackIfCrossOriginExperimentEnabled()) {
autoplay_uma_helper_->RecordCrossOriginAutoplayResult(
CrossOriginAutoplayResult::kAutoplayBlocked);
} else {
autoplay_uma_helper_->RecordCrossOriginAutoplayResult(
CrossOriginAutoplayResult::kAutoplayAllowed);
}
if (IsHTMLVideoElement() && muted() &&
RuntimeEnabledFeatures::autoplayMutedVideosEnabled()) {
// We might end up in a situation where the previous
// observer didn't had time to fire yet. We can avoid
// creating a new one in this case.
if (!autoplay_visibility_observer_) {
autoplay_visibility_observer_ = new ElementVisibilityObserver(
this,
WTF::Bind(&HTMLMediaElement::OnVisibilityChangedForAutoplay,
WrapWeakPersistent(this)));
autoplay_visibility_observer_->Start();
}
} else {
paused_ = false;
ScheduleEvent(EventTypeNames::play);
ScheduleNotifyPlaying();
can_autoplay_ = false;
}
} else {
autoplay_uma_helper_->RecordCrossOriginAutoplayResult(
CrossOriginAutoplayResult::kAutoplayBlocked);
}
if (autoplay_policy_->RequestAutoplayByAttribute()) {
paused_ = false;
ScheduleEvent(EventTypeNames::play);
ScheduleNotifyPlaying();
can_autoplay_ = false;
}
ScheduleEvent(EventTypeNames::canplaythrough);
@ -2213,12 +2128,6 @@ bool HTMLMediaElement::Autoplay() const {
return FastHasAttribute(autoplayAttr);
}
bool HTMLMediaElement::ShouldAutoplay() {
if (GetDocument().IsSandboxed(kSandboxAutomaticFeatures))
return false;
return can_autoplay_ && paused_ && Autoplay();
}
String HTMLMediaElement::preload() const {
return PreloadTypeToString(PreloadType());
}
@ -2283,7 +2192,7 @@ String HTMLMediaElement::EffectivePreload() const {
}
WebMediaPlayer::Preload HTMLMediaElement::EffectivePreloadType() const {
if (Autoplay() && !IsGestureNeededForPlayback())
if (Autoplay() && !autoplay_policy_->IsGestureNeededForPlayback())
return WebMediaPlayer::kPreloadAuto;
WebMediaPlayer::Preload preload = PreloadType();
@ -2329,47 +2238,29 @@ ScriptPromise HTMLMediaElement::playForBindings(ScriptState* script_state) {
Nullable<ExceptionCode> HTMLMediaElement::Play() {
BLINK_MEDIA_LOG << "play(" << (void*)this << ")";
if (!UserGestureIndicator::ProcessingUserGesture()) {
autoplay_uma_helper_->OnAutoplayInitiated(AutoplaySource::kMethod);
if (IsGestureNeededForPlayback()) {
// If we're already playing, then this play would do nothing anyway.
// Call playInternal to handle scheduling the promise resolution.
if (!paused_) {
PlayInternal();
return nullptr;
}
Nullable<ExceptionCode> exception_code = autoplay_policy_->RequestPlay();
autoplay_uma_helper_->RecordCrossOriginAutoplayResult(
CrossOriginAutoplayResult::kAutoplayBlocked);
String message = ExceptionMessages::FailedToExecute(
"play", "HTMLMediaElement",
"API can only be initiated by a user gesture.");
GetDocument().AddConsoleMessage(ConsoleMessage::Create(
kJSMessageSource, kWarningMessageLevel, message));
return kNotAllowedError;
} else {
if (IsGestureNeededForPlaybackIfCrossOriginExperimentEnabled()) {
autoplay_uma_helper_->RecordCrossOriginAutoplayResult(
CrossOriginAutoplayResult::kAutoplayBlocked);
} else {
autoplay_uma_helper_->RecordCrossOriginAutoplayResult(
CrossOriginAutoplayResult::kAutoplayAllowed);
}
if (exception_code == kNotAllowedError) {
// If we're already playing, then this play would do nothing anyway.
// Call playInternal to handle scheduling the promise resolution.
if (!paused_) {
PlayInternal();
return nullptr;
}
} else {
autoplay_uma_helper_->RecordCrossOriginAutoplayResult(
CrossOriginAutoplayResult::kPlayedWithGesture);
UserGestureIndicator::UtilizeUserGesture();
UnlockUserGesture();
String message = ExceptionMessages::FailedToExecute(
"play", "HTMLMediaElement",
"API can only be initiated by a user gesture.");
GetDocument().AddConsoleMessage(ConsoleMessage::Create(
kJSMessageSource, kWarningMessageLevel, message));
return exception_code;
}
autoplay_policy_->StopAutoplayMutedWhenVisible();
if (error_ && error_->code() == MediaError::kMediaErrSrcNotSupported)
return kNotSupportedError;
if (autoplay_visibility_observer_) {
autoplay_visibility_observer_->Stop();
autoplay_visibility_observer_ = nullptr;
}
DCHECK(exception_code.IsNull());
PlayInternal();
@ -2410,11 +2301,7 @@ void HTMLMediaElement::PlayInternal() {
void HTMLMediaElement::pause() {
BLINK_MEDIA_LOG << "pause(" << (void*)this << ")";
if (autoplay_visibility_observer_) {
autoplay_visibility_observer_->Stop();
autoplay_visibility_observer_ = nullptr;
}
autoplay_policy_->StopAutoplayMutedWhenVisible();
PauseInternal();
}
@ -2562,41 +2449,21 @@ void HTMLMediaElement::setMuted(bool muted) {
if (muted_ == muted)
return;
bool was_autoplaying_muted = IsAutoplayingMuted();
bool was_pending_autoplay_muted = autoplay_visibility_observer_ && paused() &&
muted_ && IsLockedPendingUserGesture();
if (UserGestureIndicator::ProcessingUserGesture())
UnlockUserGesture();
muted_ = muted;
ScheduleEvent(EventTypeNames::volumechange);
// If an element autoplayed while muted, it needs to be unlocked to unmute,
// otherwise, it will be paused.
if (was_autoplaying_muted) {
if (IsGestureNeededForPlayback()) {
pause();
autoplay_uma_helper_->RecordAutoplayUnmuteStatus(
AutoplayUnmuteActionStatus::kFailure);
} else {
autoplay_uma_helper_->RecordAutoplayUnmuteStatus(
AutoplayUnmuteActionStatus::kSuccess);
}
}
// If it is unmute and AutoplayPolicy doesn't want the playback to continue,
// pause the playback.
if (!muted_ && !autoplay_policy_->RequestAutoplayUnmute())
pause();
// This is called after the volumechange event to make sure isAutoplayingMuted
// returns the right value when webMediaPlayer receives the volume update.
if (GetWebMediaPlayer())
GetWebMediaPlayer()->SetVolume(EffectiveMediaVolume());
// If an element was a candidate for autoplay muted but not visible, it will
// have a visibility observer ready to start its playback.
if (was_pending_autoplay_muted) {
autoplay_visibility_observer_->Stop();
autoplay_visibility_observer_ = nullptr;
}
autoplay_policy_->StopAutoplayMutedWhenVisible();
}
double HTMLMediaElement::EffectiveMediaVolume() const {
@ -3318,15 +3185,6 @@ WebMediaPlayer::TrackId HTMLMediaElement::GetSelectedVideoTrackId() {
return track->id();
}
bool HTMLMediaElement::IsAutoplayingMuted() {
if (!IsHTMLVideoElement() ||
!RuntimeEnabledFeatures::autoplayMutedVideosEnabled()) {
return false;
}
return !paused() && muted() && IsLockedPendingUserGesture();
}
void HTMLMediaElement::RequestReload(const WebURL& new_url) {
DCHECK(GetWebMediaPlayer());
DCHECK(!src_object_);
@ -3336,6 +3194,10 @@ void HTMLMediaElement::RequestReload(const WebURL& new_url) {
StartPlayerLoad(new_url);
}
bool HTMLMediaElement::IsAutoplayingMuted() {
return autoplay_policy_->IsAutoplayingMuted();
}
// MediaPlayerPresentation methods
void HTMLMediaElement::Repaint() {
if (web_layer_)
@ -3931,9 +3793,8 @@ DEFINE_TRACE(HTMLMediaElement) {
visitor->Trace(play_promise_resolve_list_);
visitor->Trace(play_promise_reject_list_);
visitor->Trace(audio_source_provider_);
visitor->Trace(autoplay_uma_helper_);
visitor->Trace(src_object_);
visitor->Trace(autoplay_visibility_observer_);
visitor->Trace(autoplay_policy_);
visitor->Trace(media_controls_);
visitor->Trace(controls_list_);
visitor->template RegisterWeakMembers<HTMLMediaElement,
@ -3983,60 +3844,6 @@ void HTMLMediaElement::SelectInitialTracksIfNecessary() {
videoTracks().AnonymousIndexedGetter(0)->setSelected(true);
}
bool HTMLMediaElement::IsLockedPendingUserGesture() const {
return locked_pending_user_gesture_;
}
void HTMLMediaElement::UnlockUserGesture() {
locked_pending_user_gesture_ = false;
locked_pending_user_gesture_if_cross_origin_experiment_enabled_ = false;
}
bool HTMLMediaElement::IsGestureNeededForPlayback() const {
if (!locked_pending_user_gesture_)
return false;
return IsGestureNeededForPlaybackIfPendingUserGestureIsLocked();
}
bool HTMLMediaElement::
IsGestureNeededForPlaybackIfCrossOriginExperimentEnabled() const {
if (!locked_pending_user_gesture_if_cross_origin_experiment_enabled_)
return false;
return IsGestureNeededForPlaybackIfPendingUserGestureIsLocked();
}
bool HTMLMediaElement::IsGestureNeededForPlaybackIfPendingUserGestureIsLocked()
const {
if (GetLoadType() == WebMediaPlayer::kLoadTypeMediaStream)
return false;
// We want to allow muted video to autoplay if:
// - the flag is enabled;
// - Data Saver is not enabled;
// - Preload was not disabled (low end devices);
// - Autoplay is enabled in settings;
if (IsHTMLVideoElement() && muted() &&
RuntimeEnabledFeatures::autoplayMutedVideosEnabled() &&
!(GetDocument().GetSettings() &&
GetDocument().GetSettings()->GetDataSaverEnabled()) &&
!(GetDocument().GetSettings() &&
GetDocument().GetSettings()->GetForcePreloadNoneForMediaElements()) &&
IsAutoplayAllowedPerSettings()) {
return false;
}
return true;
}
bool HTMLMediaElement::IsAutoplayAllowedPerSettings() const {
LocalFrame* frame = GetDocument().GetFrame();
if (!frame)
return false;
return frame->GetContentSettingsClient()->AllowAutoplay(true);
}
void HTMLMediaElement::SetNetworkState(NetworkState state) {
if (network_state_ == state)
return;
@ -4049,9 +3856,7 @@ void HTMLMediaElement::SetNetworkState(NetworkState state) {
void HTMLMediaElement::VideoWillBeDrawnToCanvas() const {
DCHECK(IsHTMLVideoElement());
UseCounter::Count(GetDocument(), UseCounter::kVideoInCanvas);
if (autoplay_uma_helper_->HasSource() && !autoplay_uma_helper_->IsVisible())
UseCounter::Count(GetDocument(),
UseCounter::kHiddenAutoplayedVideoInCanvas);
autoplay_policy_->VideoWillBeDrawnToCanvas();
}
void HTMLMediaElement::ScheduleResolvePlayPromises() {
@ -4166,24 +3971,6 @@ EnumerationHistogram& HTMLMediaElement::ShowControlsHistogram() const {
return histogram;
}
void HTMLMediaElement::OnVisibilityChangedForAutoplay(bool is_visible) {
if (!is_visible) {
if (can_autoplay_ && Autoplay()) {
PauseInternal();
can_autoplay_ = true;
}
return;
}
if (ShouldAutoplay()) {
paused_ = false;
ScheduleEvent(EventTypeNames::play);
ScheduleNotifyPlaying();
UpdatePlayState();
}
}
void HTMLMediaElement::ClearWeakMembers(Visitor* visitor) {
if (!ThreadHeap::IsHeapObjectAlive(audio_source_node_)) {
GetAudioSourceProvider().SetClient(nullptr);

@ -51,10 +51,9 @@ namespace blink {
class AudioSourceProviderClient;
class AudioTrack;
class AudioTrackList;
class AutoplayUmaHelper;
class AutoplayPolicy;
class ContentType;
class CueTimeline;
class ElementVisibilityObserver;
class EnumerationHistogram;
class Event;
class ExceptionState;
@ -330,6 +329,8 @@ class CORE_EXPORT HTMLMediaElement
return remote_playback_client_;
}
const AutoplayPolicy& GetAutoplayPolicy() const { return *autoplay_policy_; }
protected:
HTMLMediaElement(const QualifiedName&, Document&);
~HTMLMediaElement() override;
@ -524,33 +525,6 @@ class CORE_EXPORT HTMLMediaElement
// transition to kHaveMetadata.
void SelectInitialTracksIfNecessary();
// Return true if and only if a user gesture is required to unlock this
// media element for unrestricted autoplay / script control. Don't confuse
// this with isGestureNeededForPlayback(). The latter is usually what one
// should use, if checking to see if an action is allowed.
bool IsLockedPendingUserGesture() const;
bool IsLockedPendingUserGestureIfCrossOriginExperimentEnabled() const;
// If the user gesture is required, then this will remove it. Note that
// one should not generally call this method directly; use the one on
// m_helper and give it a reason.
void UnlockUserGesture();
// Return true if and only if a user gesture is requried for playback. Even
// if isLockedPendingUserGesture() return true, this might return false if
// the requirement is currently overridden. This does not check if a user
// gesture is currently being processed.
bool IsGestureNeededForPlayback() const;
bool IsGestureNeededForPlaybackIfCrossOriginExperimentEnabled() const;
bool IsGestureNeededForPlaybackIfPendingUserGestureIsLocked() const;
// Return true if and only if the settings allow autoplay of media on this
// frame.
bool IsAutoplayAllowedPerSettings() const;
void SetNetworkState(NetworkState);
void AudioTracksTimerFired(TimerBase*);
@ -565,8 +539,6 @@ class CORE_EXPORT HTMLMediaElement
EnumerationHistogram& ShowControlsHistogram() const;
void OnVisibilityChangedForAutoplay(bool is_visible);
void ViewportFillDebouncerTimerFired(TimerBase*);
TaskRunnerTimer<HTMLMediaElement> load_timer_;
@ -652,8 +624,6 @@ class CORE_EXPORT HTMLMediaElement
PendingActionFlags pending_action_flags_;
// FIXME: HTMLMediaElement has way too many state bits.
bool locked_pending_user_gesture_ : 1;
bool locked_pending_user_gesture_if_cross_origin_experiment_enabled_ : 1;
bool playing_ : 1;
bool should_delay_load_event_ : 1;
bool have_fired_loaded_data_ : 1;
@ -745,7 +715,7 @@ class CORE_EXPORT HTMLMediaElement
AudioSourceProviderImpl audio_source_provider_;
friend class AutoplayUmaHelper; // for isAutoplayAllowedPerSettings
friend class AutoplayPolicy;
friend class AutoplayUmaHelperTest;
friend class Internals;
friend class TrackDisplayUpdateScope;
@ -755,13 +725,10 @@ class CORE_EXPORT HTMLMediaElement
friend class HTMLVideoElement;
friend class MediaControlsOrientationLockDelegateTest;
Member<AutoplayUmaHelper> autoplay_uma_helper_;
Member<AutoplayPolicy> autoplay_policy_;
WebRemotePlaybackClient* remote_playback_client_;
// class AutoplayVisibilityObserver;
Member<ElementVisibilityObserver> autoplay_visibility_observer_;
IntRect current_intersect_rect_;
Member<MediaControls> media_controls_;

@ -0,0 +1,301 @@
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "core/html/media/AutoplayPolicy.h"
#include "core/dom/Document.h"
#include "core/dom/ElementVisibilityObserver.h"
#include "core/frame/ContentSettingsClient.h"
#include "core/frame/LocalFrame.h"
#include "core/frame/Settings.h"
#include "core/html/HTMLMediaElement.h"
#include "core/html/media/AutoplayUmaHelper.h"
#include "platform/RuntimeEnabledFeatures.h"
#include "platform/UserGestureIndicator.h"
#include "public/platform/WebMediaPlayer.h"
namespace blink {
namespace {
bool IsDocumentCrossOrigin(Document& document) {
const LocalFrame* frame = document.GetFrame();
return frame && frame->IsCrossOriginSubframe();
}
// Returns whether |document| is whitelisted for autoplay. If true, the user
// gesture lock will be initilized as false, indicating that the element is
// allowed to autoplay unmuted without user gesture.
bool IsDocumentWhitelisted(Document& document) {
DCHECK(document.GetSettings());
const String& whitelist_scope =
document.GetSettings()->GetMediaPlaybackGestureWhitelistScope();
if (whitelist_scope.IsNull() || whitelist_scope.IsEmpty())
return false;
return document.Url().GetString().StartsWith(whitelist_scope);
}
// Return true if and only if the document settings specifies media playback
// requires user gesture.
bool ComputeLockedPendingUserGesture(Document& document) {
if (!document.GetSettings())
return false;
if (IsDocumentWhitelisted(document)) {
return false;
}
if (document.GetSettings()
->GetCrossOriginMediaPlaybackRequiresUserGesture() &&
IsDocumentCrossOrigin(document)) {
return true;
}
return document.GetSettings()->GetMediaPlaybackRequiresUserGesture();
}
} // anonymous namespace
AutoplayPolicy::AutoplayPolicy(HTMLMediaElement* element)
: locked_pending_user_gesture_(false),
locked_pending_user_gesture_if_cross_origin_experiment_enabled_(true),
element_(element),
autoplay_visibility_observer_(nullptr),
autoplay_uma_helper_(AutoplayUmaHelper::Create(element)) {
locked_pending_user_gesture_ =
ComputeLockedPendingUserGesture(element->GetDocument());
locked_pending_user_gesture_if_cross_origin_experiment_enabled_ =
IsDocumentCrossOrigin(element->GetDocument());
}
void AutoplayPolicy::VideoWillBeDrawnToCanvas() const {
autoplay_uma_helper_->VideoWillBeDrawnToCanvas();
}
void AutoplayPolicy::DidMoveToNewDocument(Document& old_document) {
// If any experiment is enabled, then we want to enable a user gesture by
// default, otherwise the experiment does nothing.
bool old_document_requires_user_gesture =
ComputeLockedPendingUserGesture(old_document);
bool new_document_requires_user_gesture =
ComputeLockedPendingUserGesture(element_->GetDocument());
if (new_document_requires_user_gesture && !old_document_requires_user_gesture)
locked_pending_user_gesture_ = true;
if (IsDocumentCrossOrigin(element_->GetDocument()) &&
!IsDocumentCrossOrigin(old_document))
locked_pending_user_gesture_if_cross_origin_experiment_enabled_ = true;
autoplay_uma_helper_->DidMoveToNewDocument(old_document);
}
bool AutoplayPolicy::IsEligibleForAutoplayMuted() const {
return element_->IsHTMLVideoElement() && element_->muted() &&
RuntimeEnabledFeatures::autoplayMutedVideosEnabled();
}
void AutoplayPolicy::StartAutoplayMutedWhenVisible() {
// We might end up in a situation where the previous
// observer didn't had time to fire yet. We can avoid
// creating a new one in this case.
if (autoplay_visibility_observer_)
return;
autoplay_visibility_observer_ = new ElementVisibilityObserver(
element_, WTF::Bind(&AutoplayPolicy::OnVisibilityChangedForAutoplay,
WrapWeakPersistent(this)));
autoplay_visibility_observer_->Start();
}
void AutoplayPolicy::StopAutoplayMutedWhenVisible() {
if (!autoplay_visibility_observer_)
return;
autoplay_visibility_observer_->Stop();
autoplay_visibility_observer_ = nullptr;
}
bool AutoplayPolicy::RequestAutoplayUnmute() {
DCHECK(!element_->muted());
bool was_autoplaying_muted = IsAutoplayingMutedInternal(true);
TryUnlockingUserGesture();
if (was_autoplaying_muted) {
if (IsGestureNeededForPlayback()) {
autoplay_uma_helper_->RecordAutoplayUnmuteStatus(
AutoplayUnmuteActionStatus::kFailure);
return false;
}
autoplay_uma_helper_->RecordAutoplayUnmuteStatus(
AutoplayUnmuteActionStatus::kSuccess);
}
return true;
}
bool AutoplayPolicy::RequestAutoplayByAttribute() {
if (!ShouldAutoplay())
return false;
autoplay_uma_helper_->OnAutoplayInitiated(AutoplaySource::kAttribute);
if (IsGestureNeededForPlayback()) {
autoplay_uma_helper_->RecordCrossOriginAutoplayResult(
CrossOriginAutoplayResult::kAutoplayBlocked);
return false;
}
if (IsGestureNeededForPlaybackIfCrossOriginExperimentEnabled()) {
autoplay_uma_helper_->RecordCrossOriginAutoplayResult(
CrossOriginAutoplayResult::kAutoplayBlocked);
} else {
autoplay_uma_helper_->RecordCrossOriginAutoplayResult(
CrossOriginAutoplayResult::kAutoplayAllowed);
}
// At this point the gesture is not needed for playback per the if statement
// above.
if (!IsEligibleForAutoplayMuted())
return true;
// Autoplay muted video should be handled by AutoplayPolicy based on the
// visibily.
StartAutoplayMutedWhenVisible();
return false;
}
Nullable<ExceptionCode> AutoplayPolicy::RequestPlay() {
if (!UserGestureIndicator::ProcessingUserGesture()) {
autoplay_uma_helper_->OnAutoplayInitiated(AutoplaySource::kMethod);
if (IsGestureNeededForPlayback()) {
autoplay_uma_helper_->RecordCrossOriginAutoplayResult(
CrossOriginAutoplayResult::kAutoplayBlocked);
return kNotAllowedError;
}
if (IsGestureNeededForPlaybackIfCrossOriginExperimentEnabled()) {
autoplay_uma_helper_->RecordCrossOriginAutoplayResult(
CrossOriginAutoplayResult::kAutoplayBlocked);
} else {
autoplay_uma_helper_->RecordCrossOriginAutoplayResult(
CrossOriginAutoplayResult::kAutoplayAllowed);
}
} else {
autoplay_uma_helper_->RecordCrossOriginAutoplayResult(
CrossOriginAutoplayResult::kPlayedWithGesture);
TryUnlockingUserGesture();
}
return nullptr;
}
bool AutoplayPolicy::IsAutoplayingMuted() const {
return IsAutoplayingMutedInternal(element_->muted());
}
bool AutoplayPolicy::IsAutoplayingMutedInternal(bool muted) const {
if (!element_->IsHTMLVideoElement() ||
!RuntimeEnabledFeatures::autoplayMutedVideosEnabled()) {
return false;
}
return !element_->paused() && muted && IsLockedPendingUserGesture();
}
bool AutoplayPolicy::IsLockedPendingUserGesture() const {
return locked_pending_user_gesture_;
}
void AutoplayPolicy::TryUnlockingUserGesture() {
if (IsLockedPendingUserGesture() &&
UserGestureIndicator::UtilizeUserGesture()) {
UnlockUserGesture();
}
}
void AutoplayPolicy::UnlockUserGesture() {
locked_pending_user_gesture_ = false;
locked_pending_user_gesture_if_cross_origin_experiment_enabled_ = false;
}
bool AutoplayPolicy::IsGestureNeededForPlayback() const {
if (!locked_pending_user_gesture_)
return false;
return IsGestureNeededForPlaybackIfPendingUserGestureIsLocked();
}
bool AutoplayPolicy::IsGestureNeededForPlaybackIfPendingUserGestureIsLocked()
const {
if (element_->GetLoadType() == WebMediaPlayer::kLoadTypeMediaStream)
return false;
// We want to allow muted video to autoplay if:
// - the flag is enabled;
// - Data Saver is not enabled;
// - Preload was not disabled (low end devices);
// - Autoplay is enabled in settings;
if (element_->IsHTMLVideoElement() && element_->muted() &&
RuntimeEnabledFeatures::autoplayMutedVideosEnabled() &&
!(element_->GetDocument().GetSettings() &&
element_->GetDocument().GetSettings()->GetDataSaverEnabled()) &&
!(element_->GetDocument().GetSettings() &&
element_->GetDocument()
.GetSettings()
->GetForcePreloadNoneForMediaElements()) &&
IsAutoplayAllowedPerSettings()) {
return false;
}
return true;
}
void AutoplayPolicy::OnVisibilityChangedForAutoplay(bool is_visible) {
if (!is_visible) {
if (element_->can_autoplay_ && element_->Autoplay()) {
element_->PauseInternal();
element_->can_autoplay_ = true;
}
return;
}
if (ShouldAutoplay()) {
element_->paused_ = false;
element_->ScheduleEvent(EventTypeNames::play);
element_->ScheduleNotifyPlaying();
element_->UpdatePlayState();
}
}
bool AutoplayPolicy::IsGestureNeededForPlaybackIfCrossOriginExperimentEnabled()
const {
if (!locked_pending_user_gesture_if_cross_origin_experiment_enabled_)
return false;
return IsGestureNeededForPlaybackIfPendingUserGestureIsLocked();
}
bool AutoplayPolicy::IsAutoplayAllowedPerSettings() const {
LocalFrame* frame = element_->GetDocument().GetFrame();
if (!frame)
return false;
return frame->GetContentSettingsClient()->AllowAutoplay(true);
}
bool AutoplayPolicy::ShouldAutoplay() {
if (element_->GetDocument().IsSandboxed(kSandboxAutomaticFeatures))
return false;
return element_->can_autoplay_ && element_->paused_ && element_->Autoplay();
}
DEFINE_TRACE(AutoplayPolicy) {
visitor->Trace(element_);
visitor->Trace(autoplay_visibility_observer_);
visitor->Trace(autoplay_uma_helper_);
}
} // namespace blink

@ -0,0 +1,113 @@
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef AutoplayPolicy_h
#define AutoplayPolicy_h
#include "bindings/core/v8/Nullable.h"
#include "core/dom/ExceptionCode.h"
#include "platform/heap/Handle.h"
namespace blink {
class AutoplayUmaHelper;
class Document;
class ElementVisibilityObserver;
class HTMLMediaElement;
// AutoplayPolicy is the class for handles autoplay logics.
class AutoplayPolicy final : public GarbageCollected<AutoplayPolicy> {
public:
explicit AutoplayPolicy(HTMLMediaElement*);
void VideoWillBeDrawnToCanvas() const;
// Called when the media element is moved to a new document.
void DidMoveToNewDocument(Document& old_document);
// Stop autoplaying the video element whenever its visible.
// TODO(mlamouri): hide these methods from HTMLMediaElement.
void StopAutoplayMutedWhenVisible();
// Request autoplay by attribute. This method will check the autoplay
// restrictions and record metrics. This method can only be called once per
// time the readyState changes to HAVE_ENOUGH_DATA.
bool RequestAutoplayByAttribute();
// Request the playback via play() method. This method will check the autoplay
// restrictions and record metrics. This method can only be called once
// per call of play().
Nullable<ExceptionCode> RequestPlay();
// Returns whether an umute action should pause an autoplaying element. The
// method will check autoplay restrictions and record metrics. This method can
// only be called once per call of setMuted().
bool RequestAutoplayUnmute();
bool IsAutoplayingMuted() const;
// Unlock user gesture if a user gesture can be utilized.
void TryUnlockingUserGesture();
// Return true if and only if a user gesture is requried for playback. Even
// if isLockedPendingUserGesture() return true, this might return false if
// the requirement is currently overridden. This does not check if a user
// gesture is currently being processed.
bool IsGestureNeededForPlayback() const;
DECLARE_VIRTUAL_TRACE();
private:
friend class AutoplayUmaHelper;
friend class AutoplayUmaHelperTest;
// Start autoplaying the video element whenever its visible.
void StartAutoplayMutedWhenVisible();
// Returns whether the media element is eligible to autoplay muted.
bool IsEligibleForAutoplayMuted() const;
bool ShouldAutoplay();
// If the user gesture is required, then this will remove it. Note that
// one should not generally call this method directly; use the one on
// m_helper and give it a reason.
void UnlockUserGesture();
// Return true if and only if a user gesture is required to unlock this
// media element for unrestricted autoplay/script control. Don't confuse
// this with isGestureNeededForPlayback(). The latter is usually what one
// should use, if checking to see if an action is allowed.
bool IsLockedPendingUserGesture() const;
bool IsLockedPendingUserGestureIfCrossOriginExperimentEnabled() const;
bool IsGestureNeededForPlaybackIfCrossOriginExperimentEnabled() const;
bool IsGestureNeededForPlaybackIfPendingUserGestureIsLocked() const;
// Return true if and only if the settings allow autoplay of media on this
// frame.
bool IsAutoplayAllowedPerSettings() const;
bool IsAutoplayingMutedInternal(bool muted) const;
// Called when the video visibility changes while autoplaying muted, will
// pause the video when invisible and resume the video when visible.
void OnVisibilityChangedForAutoplay(bool is_visible);
bool locked_pending_user_gesture_ : 1;
bool locked_pending_user_gesture_if_cross_origin_experiment_enabled_ : 1;
Member<HTMLMediaElement> element_;
Member<ElementVisibilityObserver> autoplay_visibility_observer_;
Member<AutoplayUmaHelper> autoplay_uma_helper_;
DISALLOW_COPY_AND_ASSIGN(AutoplayPolicy);
};
} // namespace blink
#endif // AutoplayPolicy_h

@ -8,7 +8,9 @@
#include "core/dom/ElementVisibilityObserver.h"
#include "core/events/Event.h"
#include "core/frame/Settings.h"
#include "core/frame/UseCounter.h"
#include "core/html/HTMLMediaElement.h"
#include "core/html/media/AutoplayPolicy.h"
#include "platform/Histogram.h"
#include "platform/wtf/CurrentTime.h"
#include "public/platform/Platform.h"
@ -110,7 +112,8 @@ void AutoplayUmaHelper::OnAutoplayInitiated(AutoplaySource source) {
bool data_saver_enabled =
element_->GetDocument().GetSettings() &&
element_->GetDocument().GetSettings()->GetDataSaverEnabled();
bool blocked_by_setting = !element_->IsAutoplayAllowedPerSettings();
bool blocked_by_setting =
!element_->GetAutoplayPolicy().IsAutoplayAllowedPerSettings();
if (data_saver_enabled && blocked_by_setting) {
blocked_muted_video_histogram.Count(
@ -213,6 +216,13 @@ void AutoplayUmaHelper::RecordAutoplayUnmuteStatus(
autoplay_unmute_histogram.Count(static_cast<int>(status));
}
void AutoplayUmaHelper::VideoWillBeDrawnToCanvas() {
if (HasSource() && !IsVisible()) {
UseCounter::Count(element_->GetDocument(),
UseCounter::kHiddenAutoplayedVideoInCanvas);
}
}
void AutoplayUmaHelper::DidMoveToNewDocument(Document& old_document) {
if (!ShouldListenToContextDestroyed())
return;

@ -75,6 +75,7 @@ class CORE_EXPORT AutoplayUmaHelper : public EventListener,
void RecordCrossOriginAutoplayResult(CrossOriginAutoplayResult);
void RecordAutoplayUnmuteStatus(AutoplayUnmuteActionStatus);
void VideoWillBeDrawnToCanvas();
void DidMoveToNewDocument(Document& old_document);
bool IsVisible() const { return is_visible_; }

@ -7,6 +7,7 @@
#include "core/dom/Document.h"
#include "core/html/HTMLMediaElement.h"
#include "core/html/HTMLVideoElement.h"
#include "core/html/media/AutoplayPolicy.h"
#include "core/testing/DummyPageHolder.h"
#include "testing/gmock/include/gmock/gmock.h"
@ -56,7 +57,7 @@ class AutoplayUmaHelperTest : public testing::Test {
ASSERT_NO_EXCEPTION);
HTMLMediaElement& element = MediaElement();
uma_helper_ = new MockAutoplayUmaHelper(&element);
element.autoplay_uma_helper_ = uma_helper_;
element.autoplay_policy_->autoplay_uma_helper_ = uma_helper_;
::testing::Mock::AllowLeak(&UmaHelper());
}