From 12d76adf4bb340d8e430a18c3a664940157800f6 Mon Sep 17 00:00:00 2001 From: zqzhang Date: Thu, 20 Apr 2017 14:24:05 -0700 Subject: [PATCH] [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} --- docs/media/autoplay.md | 7 + third_party/WebKit/Source/core/html/BUILD.gn | 2 + .../Source/core/html/HTMLMediaElement.cpp | 289 +++-------------- .../Source/core/html/HTMLMediaElement.h | 43 +-- .../Source/core/html/media/AutoplayPolicy.cpp | 301 ++++++++++++++++++ .../Source/core/html/media/AutoplayPolicy.h | 113 +++++++ .../core/html/media/AutoplayUmaHelper.cpp | 12 +- .../core/html/media/AutoplayUmaHelper.h | 1 + .../core/html/media/AutoplayUmaHelperTest.cpp | 3 +- 9 files changed, 480 insertions(+), 291 deletions(-) create mode 100644 third_party/WebKit/Source/core/html/media/AutoplayPolicy.cpp create mode 100644 third_party/WebKit/Source/core/html/media/AutoplayPolicy.h diff --git a/docs/media/autoplay.md b/docs/media/autoplay.md index 0312713f9ca362..3d0594a7b93bfd 100644 --- a/docs/media/autoplay.md +++ b/docs/media/autoplay.md @@ -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 diff --git a/third_party/WebKit/Source/core/html/BUILD.gn b/third_party/WebKit/Source/core/html/BUILD.gn index c7cf026c1eee41..253fa3f40f6743 100644 --- a/third_party/WebKit/Source/core/html/BUILD.gn +++ b/third_party/WebKit/Source/core/html/BUILD.gn @@ -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", diff --git a/third_party/WebKit/Source/core/html/HTMLMediaElement.cpp b/third_party/WebKit/Source/core/html/HTMLMediaElement.cpp index afe41b35b08291..b7dad162630a4a 100644 --- a/third_party/WebKit/Source/core/html/HTMLMediaElement.cpp +++ b/third_party/WebKit/Source/core/html/HTMLMediaElement.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& MediaControlsFactory() { DEFINE_STATIC_LOCAL(std::unique_ptr, 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::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 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 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 RegisterWeakMemberssetSelected(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); diff --git a/third_party/WebKit/Source/core/html/HTMLMediaElement.h b/third_party/WebKit/Source/core/html/HTMLMediaElement.h index c61d5df71763a3..b313f6968cee40 100644 --- a/third_party/WebKit/Source/core/html/HTMLMediaElement.h +++ b/third_party/WebKit/Source/core/html/HTMLMediaElement.h @@ -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 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 autoplay_uma_helper_; + Member autoplay_policy_; WebRemotePlaybackClient* remote_playback_client_; - // class AutoplayVisibilityObserver; - Member autoplay_visibility_observer_; - IntRect current_intersect_rect_; Member media_controls_; diff --git a/third_party/WebKit/Source/core/html/media/AutoplayPolicy.cpp b/third_party/WebKit/Source/core/html/media/AutoplayPolicy.cpp new file mode 100644 index 00000000000000..68dff512c76065 --- /dev/null +++ b/third_party/WebKit/Source/core/html/media/AutoplayPolicy.cpp @@ -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 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 diff --git a/third_party/WebKit/Source/core/html/media/AutoplayPolicy.h b/third_party/WebKit/Source/core/html/media/AutoplayPolicy.h new file mode 100644 index 00000000000000..8e20d46c478082 --- /dev/null +++ b/third_party/WebKit/Source/core/html/media/AutoplayPolicy.h @@ -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 { + 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 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 element_; + Member autoplay_visibility_observer_; + + Member autoplay_uma_helper_; + + DISALLOW_COPY_AND_ASSIGN(AutoplayPolicy); +}; + +} // namespace blink + +#endif // AutoplayPolicy_h diff --git a/third_party/WebKit/Source/core/html/media/AutoplayUmaHelper.cpp b/third_party/WebKit/Source/core/html/media/AutoplayUmaHelper.cpp index a706ffbebf58d6..410dcc50e41cef 100644 --- a/third_party/WebKit/Source/core/html/media/AutoplayUmaHelper.cpp +++ b/third_party/WebKit/Source/core/html/media/AutoplayUmaHelper.cpp @@ -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(status)); } +void AutoplayUmaHelper::VideoWillBeDrawnToCanvas() { + if (HasSource() && !IsVisible()) { + UseCounter::Count(element_->GetDocument(), + UseCounter::kHiddenAutoplayedVideoInCanvas); + } +} + void AutoplayUmaHelper::DidMoveToNewDocument(Document& old_document) { if (!ShouldListenToContextDestroyed()) return; diff --git a/third_party/WebKit/Source/core/html/media/AutoplayUmaHelper.h b/third_party/WebKit/Source/core/html/media/AutoplayUmaHelper.h index 917ab9bd0862af..55b5e1302270e1 100644 --- a/third_party/WebKit/Source/core/html/media/AutoplayUmaHelper.h +++ b/third_party/WebKit/Source/core/html/media/AutoplayUmaHelper.h @@ -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_; } diff --git a/third_party/WebKit/Source/core/html/media/AutoplayUmaHelperTest.cpp b/third_party/WebKit/Source/core/html/media/AutoplayUmaHelperTest.cpp index e515e28be4471f..f3bc12d8ce04f3 100644 --- a/third_party/WebKit/Source/core/html/media/AutoplayUmaHelperTest.cpp +++ b/third_party/WebKit/Source/core/html/media/AutoplayUmaHelperTest.cpp @@ -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()); }