blob: c5aaff6418f48f15ff10e51d8aab0d169391f7d9 [file] [log] [blame]
// 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 "third_party/blink/renderer/core/html/media/autoplay_policy.h"
#include "build/build_config.h"
#include "third_party/blink/public/mojom/autoplay/autoplay.mojom-blink.h"
#include "third_party/blink/public/mojom/feature_policy/feature_policy.mojom-blink.h"
#include "third_party/blink/public/mojom/webpreferences/web_preferences.mojom-blink.h"
#include "third_party/blink/public/platform/web_media_player.h"
#include "third_party/blink/public/web/web_local_frame.h"
#include "third_party/blink/public/web/web_local_frame_client.h"
#include "third_party/blink/public/web/web_settings.h"
#include "third_party/blink/renderer/core/dom/document.h"
#include "third_party/blink/renderer/core/frame/local_frame.h"
#include "third_party/blink/renderer/core/frame/settings.h"
#include "third_party/blink/renderer/core/html/media/autoplay_uma_helper.h"
#include "third_party/blink/renderer/core/html/media/html_media_element.h"
#include "third_party/blink/renderer/core/inspector/console_message.h"
#include "third_party/blink/renderer/core/intersection_observer/intersection_observer.h"
#include "third_party/blink/renderer/core/intersection_observer/intersection_observer_entry.h"
#include "third_party/blink/renderer/core/page/page.h"
#include "third_party/blink/renderer/platform/heap/heap.h"
#include "third_party/blink/renderer/platform/network/network_state_notifier.h"
#include "third_party/blink/renderer/platform/runtime_enabled_features.h"
#include "third_party/blink/renderer/platform/wtf/assertions.h"
namespace blink {
namespace {
const char kWarningUnmuteFailed[] =
"Unmuting failed and the element was paused instead because the user "
"didn't interact with the document before. https://goo.gl/xX8pDD";
const char kErrorAutoplayFuncUnified[] =
"play() failed because the user didn't interact with the document first. "
"https://goo.gl/xX8pDD";
const char kErrorAutoplayFuncMobile[] =
"play() can only be initiated by a user gesture.";
// Return true if and only if the document settings specifies media playback
// requires user gesture on the element.
bool ComputeLockPendingUserGestureRequired(const Document& document) {
switch (AutoplayPolicy::GetAutoplayPolicyForDocument(document)) {
case AutoplayPolicy::Type::kNoUserGestureRequired:
return false;
case AutoplayPolicy::Type::kUserGestureRequired:
return true;
// kDocumentUserActivationRequired policy does not imply that a user gesture
// is required on the element but instead requires a user gesture on the
// document, therefore the element is not locked.
case AutoplayPolicy::Type::kDocumentUserActivationRequired:
return false;
}
NOTREACHED();
return true;
}
} // anonymous namespace
// static
AutoplayPolicy::Type AutoplayPolicy::GetAutoplayPolicyForDocument(
const Document& document) {
if (!document.GetSettings())
return Type::kNoUserGestureRequired;
if (document.IsInWebAppScope())
return Type::kNoUserGestureRequired;
if (DocumentHasUserExceptionFlag(document))
return Type::kNoUserGestureRequired;
if (document.GetSettings()->GetPresentationReceiver())
return Type::kNoUserGestureRequired;
return document.GetSettings()->GetAutoplayPolicy();
}
// static
bool AutoplayPolicy::IsDocumentAllowedToPlay(const Document& document) {
if (DocumentHasForceAllowFlag(document))
return true;
if (DocumentIsCapturingUserMedia(document))
return true;
if (!document.GetFrame())
return false;
bool feature_policy_enabled =
document.GetExecutionContext()->IsFeatureEnabled(
mojom::blink::FeaturePolicyFeature::kAutoplay);
for (Frame* frame = document.GetFrame(); frame;
frame = frame->Tree().Parent()) {
if (frame->HasStickyUserActivation() ||
frame->HadStickyUserActivationBeforeNavigation()) {
return true;
}
if (RuntimeEnabledFeatures::
MediaEngagementBypassAutoplayPoliciesEnabled() &&
frame->IsMainFrame() && DocumentHasHighMediaEngagement(document)) {
return true;
}
if (!feature_policy_enabled)
return false;
}
return false;
}
// static
bool AutoplayPolicy::DocumentHasHighMediaEngagement(const Document& document) {
if (!document.GetPage())
return false;
return document.GetPage()->AutoplayFlags() &
mojom::blink::kAutoplayFlagHighMediaEngagement;
}
// static
bool AutoplayPolicy::DocumentHasForceAllowFlag(const Document& document) {
if (!document.GetPage())
return false;
return document.GetPage()->AutoplayFlags() &
mojom::blink::kAutoplayFlagForceAllow;
}
// static
bool AutoplayPolicy::DocumentHasUserExceptionFlag(const Document& document) {
if (!document.GetPage())
return false;
return document.GetPage()->AutoplayFlags() &
mojom::blink::kAutoplayFlagUserException;
}
// static
bool AutoplayPolicy::DocumentShouldAutoplayMutedVideos(
const Document& document) {
return GetAutoplayPolicyForDocument(document) !=
AutoplayPolicy::Type::kNoUserGestureRequired;
}
// static
bool AutoplayPolicy::DocumentIsCapturingUserMedia(const Document& document) {
if (auto* local_frame = document.GetFrame())
return local_frame->IsCapturingMedia();
return false;
}
AutoplayPolicy::AutoplayPolicy(HTMLMediaElement* element)
: locked_pending_user_gesture_(false),
element_(element),
autoplay_uma_helper_(MakeGarbageCollected<AutoplayUmaHelper>(element)) {
locked_pending_user_gesture_ =
ComputeLockPendingUserGestureRequired(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 =
ComputeLockPendingUserGestureRequired(old_document);
bool new_document_requires_user_gesture =
ComputeLockPendingUserGestureRequired(element_->GetDocument());
if (new_document_requires_user_gesture && !old_document_requires_user_gesture)
locked_pending_user_gesture_ = true;
autoplay_uma_helper_->DidMoveToNewDocument(old_document);
}
bool AutoplayPolicy::IsEligibleForAutoplayMuted() const {
if (!IsA<HTMLVideoElement>(element_.Get()))
return false;
if (RuntimeEnabledFeatures::VideoAutoFullscreenEnabled() &&
!element_->FastHasAttribute(html_names::kPlaysinlineAttr)) {
return false;
}
return !element_->EffectiveMediaVolume() &&
DocumentShouldAutoplayMutedVideos(element_->GetDocument());
}
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_intersection_observer_)
return;
autoplay_intersection_observer_ = IntersectionObserver::Create(
{}, {IntersectionObserver::kMinimumThreshold}, &element_->GetDocument(),
WTF::BindRepeating(&AutoplayPolicy::OnIntersectionChangedForAutoplay,
WrapWeakPersistent(this)),
LocalFrameUkmAggregator::kMediaIntersectionObserver);
autoplay_intersection_observer_->observe(element_);
}
void AutoplayPolicy::StopAutoplayMutedWhenVisible() {
if (!autoplay_intersection_observer_)
return;
autoplay_intersection_observer_->disconnect();
autoplay_intersection_observer_ = nullptr;
}
bool AutoplayPolicy::RequestAutoplayUnmute() {
DCHECK_NE(0, element_->EffectiveMediaVolume());
bool was_autoplaying_muted = IsAutoplayingMutedInternal(true);
TryUnlockingUserGesture();
if (was_autoplaying_muted) {
if (IsGestureNeededForPlayback()) {
if (IsUsingDocumentUserActivationRequiredPolicy()) {
element_->GetDocument().AddConsoleMessage(
MakeGarbageCollected<ConsoleMessage>(
mojom::ConsoleMessageSource::kJavaScript,
mojom::ConsoleMessageLevel::kWarning, kWarningUnmuteFailed));
}
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())
return false;
// If it's the first playback, track that it started because of autoplay.
MaybeSetAutoplayInitiated();
// 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;
}
base::Optional<DOMExceptionCode> AutoplayPolicy::RequestPlay() {
if (!LocalFrame::HasTransientUserActivation(
element_->GetDocument().GetFrame())) {
autoplay_uma_helper_->OnAutoplayInitiated(AutoplaySource::kMethod);
if (IsGestureNeededForPlayback())
return DOMExceptionCode::kNotAllowedError;
} else {
TryUnlockingUserGesture();
}
MaybeSetAutoplayInitiated();
return base::nullopt;
}
bool AutoplayPolicy::IsAutoplayingMutedInternal(bool muted) const {
return !element_->paused() && IsOrWillBeAutoplayingMutedInternal(muted);
}
bool AutoplayPolicy::IsOrWillBeAutoplayingMuted() const {
return IsOrWillBeAutoplayingMutedInternal(!element_->EffectiveMediaVolume());
}
bool AutoplayPolicy::IsOrWillBeAutoplayingMutedInternal(bool muted) const {
if (!IsA<HTMLVideoElement>(element_.Get()) ||
!DocumentShouldAutoplayMutedVideos(element_->GetDocument())) {
return false;
}
return muted && IsLockedPendingUserGesture();
}
bool AutoplayPolicy::IsLockedPendingUserGesture() const {
if (IsUsingDocumentUserActivationRequiredPolicy())
return !IsDocumentAllowedToPlay(element_->GetDocument());
return locked_pending_user_gesture_;
}
void AutoplayPolicy::TryUnlockingUserGesture() {
if (IsLockedPendingUserGesture() && LocalFrame::HasTransientUserActivation(
element_->GetDocument().GetFrame())) {
locked_pending_user_gesture_ = false;
}
}
bool AutoplayPolicy::IsGestureNeededForPlayback() const {
if (!IsLockedPendingUserGesture())
return false;
// We want to allow muted video to autoplay if the element is allowed to
// autoplay muted.
return !IsEligibleForAutoplayMuted();
}
String AutoplayPolicy::GetPlayErrorMessage() const {
return IsUsingDocumentUserActivationRequiredPolicy()
? kErrorAutoplayFuncUnified
: kErrorAutoplayFuncMobile;
}
bool AutoplayPolicy::WasAutoplayInitiated() const {
if (!autoplay_initiated_.has_value())
return false;
return *autoplay_initiated_;
}
void AutoplayPolicy::EnsureAutoplayInitiatedSet() {
if (autoplay_initiated_)
return;
autoplay_initiated_ = false;
}
void AutoplayPolicy::OnIntersectionChangedForAutoplay(
const HeapVector<Member<IntersectionObserverEntry>>& entries) {
bool is_visible = (entries.back()->intersectionRatio() > 0);
if (!is_visible) {
if (element_->can_autoplay_ && element_->Autoplay()) {
element_->PauseInternal();
element_->can_autoplay_ = true;
}
return;
}
if (ShouldAutoplay()) {
element_->paused_ = false;
element_->SetShowPosterFlag(false);
element_->ScheduleEvent(event_type_names::kPlay);
element_->ScheduleNotifyPlaying();
element_->UpdatePlayState();
}
}
bool AutoplayPolicy::IsUsingDocumentUserActivationRequiredPolicy() const {
return GetAutoplayPolicyForDocument(element_->GetDocument()) ==
AutoplayPolicy::Type::kDocumentUserActivationRequired;
}
void AutoplayPolicy::MaybeSetAutoplayInitiated() {
if (autoplay_initiated_.has_value())
return;
autoplay_initiated_ = true;
bool feature_policy_enabled =
element_->GetExecutionContext() &&
element_->GetExecutionContext()->IsFeatureEnabled(
mojom::blink::FeaturePolicyFeature::kAutoplay);
for (Frame* frame = element_->GetDocument().GetFrame(); frame;
frame = frame->Tree().Parent()) {
if (frame->HasStickyUserActivation() ||
frame->HadStickyUserActivationBeforeNavigation()) {
autoplay_initiated_ = false;
break;
}
if (!feature_policy_enabled)
break;
}
}
bool AutoplayPolicy::ShouldAutoplay() {
if (!element_->GetExecutionContext() ||
element_->GetExecutionContext()->IsSandboxed(
network::mojom::blink::WebSandboxFlags::kAutomaticFeatures)) {
return false;
}
return element_->can_autoplay_ && element_->paused_ && element_->Autoplay();
}
void AutoplayPolicy::Trace(Visitor* visitor) const {
visitor->Trace(element_);
visitor->Trace(autoplay_intersection_observer_);
visitor->Trace(autoplay_uma_helper_);
}
STATIC_ASSERT_ENUM(mojom::blink::AutoplayPolicy::kNoUserGestureRequired,
AutoplayPolicy::Type::kNoUserGestureRequired);
STATIC_ASSERT_ENUM(mojom::blink::AutoplayPolicy::kUserGestureRequired,
AutoplayPolicy::Type::kUserGestureRequired);
STATIC_ASSERT_ENUM(
mojom::blink::AutoplayPolicy::kDocumentUserActivationRequired,
AutoplayPolicy::Type::kDocumentUserActivationRequired);
} // namespace blink