blob: 4fa1e11e7c0e188e2c44ba8c32ba39eda7fa5f03 [file] [log] [blame]
// Copyright 2016 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_uma_helper.h"
#include "services/metrics/public/cpp/ukm_builders.h"
#include "services/metrics/public/cpp/ukm_recorder.h"
#include "third_party/blink/public/platform/interface_provider.h"
#include "third_party/blink/public/platform/platform.h"
#include "third_party/blink/renderer/core/dom/document.h"
#include "third_party/blink/renderer/core/dom/element_visibility_observer.h"
#include "third_party/blink/renderer/core/dom/events/event.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/frame/use_counter.h"
#include "third_party/blink/renderer/core/html/media/autoplay_policy.h"
#include "third_party/blink/renderer/core/html/media/html_media_element.h"
#include "third_party/blink/renderer/core/page/page.h"
#include "third_party/blink/renderer/platform/histogram.h"
#include "third_party/blink/renderer/platform/network/network_state_notifier.h"
namespace blink {
namespace {
constexpr TimeDelta kMaxOffscreenDurationUma = TimeDelta::FromHours(1);
constexpr int32_t kOffscreenDurationUmaBucketCount = 50;
constexpr TimeDelta kMaxWaitTimeUma = TimeDelta::FromSeconds(30);
constexpr int32_t kWaitTimeBucketCount = 50;
// Returns a int64_t with the following structure:
// 0b0001 set if there is a user gesture on the stack.
// 0b0010 set if there was a user gesture on the page.
// 0b0100 set if there was a user gesture propagated after navigation.
int64_t GetUserGestureStatusForUkmMetric(LocalFrame* frame) {
DCHECK(frame);
int64_t result = 0;
if (LocalFrame::HasTransientUserActivation(frame, false))
result |= 0x01;
if (frame->HasBeenActivated())
result |= 0x02;
if (frame->HasReceivedUserGestureBeforeNavigation())
result |= 0x04;
return result;
}
} // namespace
AutoplayUmaHelper* AutoplayUmaHelper::Create(HTMLMediaElement* element) {
return new AutoplayUmaHelper(element);
}
AutoplayUmaHelper::AutoplayUmaHelper(HTMLMediaElement* element)
: EventListener(kCPPEventListenerType),
ContextLifecycleObserver(nullptr),
element_(element),
muted_video_play_method_visibility_observer_(nullptr),
is_visible_(false),
muted_video_offscreen_duration_visibility_observer_(nullptr) {
element->addEventListener(event_type_names::kLoadstart, this, false);
}
AutoplayUmaHelper::~AutoplayUmaHelper() = default;
bool AutoplayUmaHelper::operator==(const EventListener& other) const {
return this == &other;
}
void AutoplayUmaHelper::OnLoadStarted() {
if (element_->GetLoadType() == WebMediaPlayer::kLoadTypeURL)
load_start_time_ = CurrentTimeTicks();
}
void AutoplayUmaHelper::OnAutoplayInitiated(AutoplaySource source) {
base::Optional<TimeDelta> autoplay_wait_time;
if (!load_start_time_.is_null())
autoplay_wait_time = CurrentTimeTicks() - load_start_time_;
DEFINE_STATIC_LOCAL(EnumerationHistogram, video_histogram,
("Media.Video.Autoplay",
static_cast<int>(AutoplaySource::kNumberOfUmaSources)));
DEFINE_STATIC_LOCAL(EnumerationHistogram, muted_video_histogram,
("Media.Video.Autoplay.Muted",
static_cast<int>(AutoplaySource::kNumberOfUmaSources)));
DEFINE_STATIC_LOCAL(EnumerationHistogram, audio_histogram,
("Media.Audio.Autoplay",
static_cast<int>(AutoplaySource::kNumberOfUmaSources)));
DEFINE_STATIC_LOCAL(
EnumerationHistogram, blocked_muted_video_histogram,
("Media.Video.Autoplay.Muted.Blocked", kAutoplayBlockedReasonMax));
// Autoplay already initiated
if (sources_.count(source))
return;
sources_.insert(source);
// Record the source.
if (element_->IsHTMLVideoElement()) {
video_histogram.Count(static_cast<int>(source));
if (element_->muted())
muted_video_histogram.Count(static_cast<int>(source));
if (autoplay_wait_time.has_value()) {
if (source == AutoplaySource::kAttribute) {
UMA_HISTOGRAM_CUSTOM_TIMES("Media.Video.Autoplay.Attribute.WaitTime",
*autoplay_wait_time,
TimeDelta::FromMilliseconds(1),
kMaxWaitTimeUma, kWaitTimeBucketCount);
} else if (source == AutoplaySource::kMethod) {
UMA_HISTOGRAM_CUSTOM_TIMES("Media.Video.Autoplay.PlayMethod.WaitTime",
*autoplay_wait_time,
TimeDelta::FromMilliseconds(1),
kMaxWaitTimeUma, kWaitTimeBucketCount);
}
}
} else {
audio_histogram.Count(static_cast<int>(source));
if (autoplay_wait_time.has_value()) {
if (source == AutoplaySource::kAttribute) {
UMA_HISTOGRAM_CUSTOM_TIMES("Media.Audio.Autoplay.Attribute.WaitTime",
*autoplay_wait_time,
TimeDelta::FromMilliseconds(1),
kMaxWaitTimeUma, kWaitTimeBucketCount);
} else if (source == AutoplaySource::kMethod) {
UMA_HISTOGRAM_CUSTOM_TIMES("Media.Audio.Autoplay.PlayMethod.WaitTime",
*autoplay_wait_time,
TimeDelta::FromMilliseconds(1),
kMaxWaitTimeUma, kWaitTimeBucketCount);
}
}
}
// Record dual source.
if (sources_.size() ==
static_cast<size_t>(AutoplaySource::kNumberOfSources)) {
if (element_->IsHTMLVideoElement()) {
video_histogram.Count(static_cast<int>(AutoplaySource::kDualSource));
if (element_->muted())
muted_video_histogram.Count(
static_cast<int>(AutoplaySource::kDualSource));
} else {
audio_histogram.Count(static_cast<int>(AutoplaySource::kDualSource));
}
}
// Record the child frame and top-level frame URLs for autoplay muted videos
// by attribute.
if (element_->IsHTMLVideoElement() && element_->muted()) {
if (sources_.size() ==
static_cast<size_t>(AutoplaySource::kNumberOfSources)) {
Platform::Current()->RecordRapporURL(
"Media.Video.Autoplay.Muted.DualSource.Frame",
element_->GetDocument().Url());
} else if (source == AutoplaySource::kAttribute) {
Platform::Current()->RecordRapporURL(
"Media.Video.Autoplay.Muted.Attribute.Frame",
element_->GetDocument().Url());
} else {
DCHECK(source == AutoplaySource::kMethod);
Platform::Current()->RecordRapporURL(
"Media.Video.Autoplay.Muted.PlayMethod.Frame",
element_->GetDocument().Url());
}
}
// Record if it will be blocked by Data Saver or Autoplay setting.
if (element_->IsHTMLVideoElement() && element_->muted() &&
AutoplayPolicy::DocumentShouldAutoplayMutedVideos(
element_->GetDocument())) {
bool data_saver_enabled_for_autoplay =
GetNetworkStateNotifier().SaveDataEnabled() &&
element_->GetDocument().GetSettings() &&
!element_->GetDocument().GetSettings()->GetDataSaverHoldbackMediaApi();
bool blocked_by_setting =
!element_->GetAutoplayPolicy().IsAutoplayAllowedPerSettings();
if (data_saver_enabled_for_autoplay && blocked_by_setting) {
blocked_muted_video_histogram.Count(
kAutoplayBlockedReasonDataSaverAndSetting);
} else if (data_saver_enabled_for_autoplay) {
blocked_muted_video_histogram.Count(kAutoplayBlockedReasonDataSaver);
} else if (blocked_by_setting) {
blocked_muted_video_histogram.Count(kAutoplayBlockedReasonSetting);
}
}
element_->addEventListener(event_type_names::kPlaying, this, false);
// Record UKM autoplay event.
if (!element_->GetDocument().IsActive())
return;
LocalFrame* frame = element_->GetDocument().GetFrame();
DCHECK(frame);
DCHECK(element_->GetDocument().GetPage());
ukm::UkmRecorder* ukm_recorder = element_->GetDocument().UkmRecorder();
DCHECK(ukm_recorder);
ukm::builders::Media_Autoplay_Attempt(element_->GetDocument().UkmSourceID())
.SetSource(source == AutoplaySource::kMethod)
.SetAudioTrack(element_->HasAudio())
.SetVideoTrack(element_->HasVideo())
.SetUserGestureRequired(
element_->GetAutoplayPolicy().IsGestureNeededForPlayback())
.SetMuted(element_->muted())
.SetHighMediaEngagement(AutoplayPolicy::DocumentHasHighMediaEngagement(
element_->GetDocument()))
.SetUserGestureStatus(GetUserGestureStatusForUkmMetric(frame))
.Record(ukm_recorder);
}
void AutoplayUmaHelper::RecordCrossOriginAutoplayResult(
CrossOriginAutoplayResult result) {
DEFINE_STATIC_LOCAL(
EnumerationHistogram, autoplay_result_histogram,
("Media.Autoplay.CrossOrigin.Result",
static_cast<int>(CrossOriginAutoplayResult::kNumberOfResults)));
if (!element_->IsHTMLVideoElement())
return;
if (!element_->IsInCrossOriginFrame())
return;
// Record each metric only once per element, since the metric focuses on the
// site distribution. If a page calls play() multiple times, it will be
// recorded only once.
if (recorded_cross_origin_autoplay_results_.count(result))
return;
switch (result) {
case CrossOriginAutoplayResult::kAutoplayAllowed:
// Record metric
Platform::Current()->RecordRapporURL(
"Media.Autoplay.CrossOrigin.Allowed.ChildFrame",
element_->GetDocument().Url());
Platform::Current()->RecordRapporURL(
"Media.Autoplay.CrossOrigin.Allowed.TopLevelFrame",
element_->GetDocument().TopDocument().Url());
autoplay_result_histogram.Count(static_cast<int>(result));
recorded_cross_origin_autoplay_results_.insert(result);
break;
case CrossOriginAutoplayResult::kAutoplayBlocked:
Platform::Current()->RecordRapporURL(
"Media.Autoplay.CrossOrigin.Blocked.ChildFrame",
element_->GetDocument().Url());
Platform::Current()->RecordRapporURL(
"Media.Autoplay.CrossOrigin.Blocked.TopLevelFrame",
element_->GetDocument().TopDocument().Url());
autoplay_result_histogram.Count(static_cast<int>(result));
recorded_cross_origin_autoplay_results_.insert(result);
break;
case CrossOriginAutoplayResult::kPlayedWithGesture:
// Record this metric only when the video has been blocked from autoplay
// previously. This is to record the sites having videos that are blocked
// to autoplay but the user starts the playback by gesture.
if (!recorded_cross_origin_autoplay_results_.count(
CrossOriginAutoplayResult::kAutoplayBlocked)) {
return;
}
Platform::Current()->RecordRapporURL(
"Media.Autoplay.CrossOrigin.PlayedWithGestureAfterBlock.ChildFrame",
element_->GetDocument().Url());
Platform::Current()->RecordRapporURL(
"Media.Autoplay.CrossOrigin.PlayedWithGestureAfterBlock."
"TopLevelFrame",
element_->GetDocument().TopDocument().Url());
autoplay_result_histogram.Count(static_cast<int>(result));
recorded_cross_origin_autoplay_results_.insert(result);
break;
case CrossOriginAutoplayResult::kUserPaused:
if (!ShouldRecordUserPausedAutoplayingCrossOriginVideo())
return;
if (element_->ended() || element_->seeking())
return;
Platform::Current()->RecordRapporURL(
"Media.Autoplay.CrossOrigin.UserPausedAutoplayingVideo.ChildFrame",
element_->GetDocument().Url());
Platform::Current()->RecordRapporURL(
"Media.Autoplay.CrossOrigin.UserPausedAutoplayingVideo."
"TopLevelFrame",
element_->GetDocument().TopDocument().Url());
autoplay_result_histogram.Count(static_cast<int>(result));
recorded_cross_origin_autoplay_results_.insert(result);
break;
default:
NOTREACHED();
}
}
void AutoplayUmaHelper::RecordAutoplayUnmuteStatus(
AutoplayUnmuteActionStatus status) {
DEFINE_STATIC_LOCAL(
EnumerationHistogram, autoplay_unmute_histogram,
("Media.Video.Autoplay.Muted.UnmuteAction",
static_cast<int>(AutoplayUnmuteActionStatus::kNumberOfStatus)));
autoplay_unmute_histogram.Count(static_cast<int>(status));
// Record UKM event for unmute muted autoplay.
if (element_->GetDocument().IsInMainFrame()) {
int source = static_cast<int>(AutoplaySource::kAttribute);
if (sources_.size() ==
static_cast<size_t>(AutoplaySource::kNumberOfSources)) {
source = static_cast<int>(AutoplaySource::kDualSource);
} else if (sources_.count(AutoplaySource::kMethod)) {
source = static_cast<int>(AutoplaySource::kAttribute);
}
ukm::UkmRecorder* ukm_recorder = element_->GetDocument().UkmRecorder();
DCHECK(ukm_recorder);
ukm::builders::Media_Autoplay_Muted_UnmuteAction(
element_->GetDocument().UkmSourceID())
.SetSource(source)
.SetResult(status == AutoplayUnmuteActionStatus::kSuccess)
.Record(ukm_recorder);
}
}
void AutoplayUmaHelper::VideoWillBeDrawnToCanvas() {
if (HasSource() && !IsVisible()) {
UseCounter::Count(element_->GetDocument(),
WebFeature::kHiddenAutoplayedVideoInCanvas);
}
}
void AutoplayUmaHelper::DidMoveToNewDocument(Document& old_document) {
if (!ShouldListenToContextDestroyed())
return;
SetContext(&element_->GetDocument());
}
void AutoplayUmaHelper::OnVisibilityChangedForMutedVideoPlayMethodBecomeVisible(
bool is_visible) {
if (!is_visible || !muted_video_play_method_visibility_observer_)
return;
MaybeStopRecordingMutedVideoPlayMethodBecomeVisible(true);
}
void AutoplayUmaHelper::OnVisibilityChangedForMutedVideoOffscreenDuration(
bool is_visible) {
if (is_visible == is_visible_)
return;
if (is_visible) {
muted_video_autoplay_offscreen_duration_ +=
CurrentTimeTicks() - muted_video_autoplay_offscreen_start_time_;
} else {
muted_video_autoplay_offscreen_start_time_ = CurrentTimeTicks();
}
is_visible_ = is_visible;
}
void AutoplayUmaHelper::Invoke(ExecutionContext* execution_context,
Event* event) {
if (event->type() == event_type_names::kLoadstart)
OnLoadStarted();
else if (event->type() == event_type_names::kPlaying)
HandlePlayingEvent();
else if (event->type() == event_type_names::kPause)
HandlePauseEvent();
else
NOTREACHED();
}
void AutoplayUmaHelper::HandlePlayingEvent() {
MaybeStartRecordingMutedVideoPlayMethodBecomeVisible();
MaybeStartRecordingMutedVideoOffscreenDuration();
element_->removeEventListener(event_type_names::kPlaying, this, false);
}
void AutoplayUmaHelper::HandlePauseEvent() {
MaybeStopRecordingMutedVideoOffscreenDuration();
MaybeRecordUserPausedAutoplayingCrossOriginVideo();
}
void AutoplayUmaHelper::ContextDestroyed(ExecutionContext*) {
HandleContextDestroyed();
}
void AutoplayUmaHelper::HandleContextDestroyed() {
MaybeStopRecordingMutedVideoPlayMethodBecomeVisible(false);
MaybeStopRecordingMutedVideoOffscreenDuration();
}
void AutoplayUmaHelper::MaybeStartRecordingMutedVideoPlayMethodBecomeVisible() {
if (!sources_.count(AutoplaySource::kMethod) ||
!element_->IsHTMLVideoElement() || !element_->muted())
return;
muted_video_play_method_visibility_observer_ =
MakeGarbageCollected<ElementVisibilityObserver>(
element_,
WTF::BindRepeating(
&AutoplayUmaHelper::
OnVisibilityChangedForMutedVideoPlayMethodBecomeVisible,
WrapWeakPersistent(this)));
muted_video_play_method_visibility_observer_->Start();
SetContext(&element_->GetDocument());
}
void AutoplayUmaHelper::MaybeStopRecordingMutedVideoPlayMethodBecomeVisible(
bool visible) {
if (!muted_video_play_method_visibility_observer_)
return;
DEFINE_STATIC_LOCAL(BooleanHistogram, histogram,
("Media.Video.Autoplay.Muted.PlayMethod.BecomesVisible"));
histogram.Count(visible);
muted_video_play_method_visibility_observer_->Stop();
muted_video_play_method_visibility_observer_ = nullptr;
MaybeUnregisterContextDestroyedObserver();
}
void AutoplayUmaHelper::MaybeStartRecordingMutedVideoOffscreenDuration() {
if (!element_->IsHTMLVideoElement() || !element_->muted() ||
!sources_.count(AutoplaySource::kMethod))
return;
// Start recording muted video playing offscreen duration.
muted_video_autoplay_offscreen_start_time_ = CurrentTimeTicks();
is_visible_ = false;
muted_video_offscreen_duration_visibility_observer_ = MakeGarbageCollected<
ElementVisibilityObserver>(
element_,
WTF::BindRepeating(
&AutoplayUmaHelper::OnVisibilityChangedForMutedVideoOffscreenDuration,
WrapWeakPersistent(this)));
muted_video_offscreen_duration_visibility_observer_->Start();
element_->addEventListener(event_type_names::kPause, this, false);
SetContext(&element_->GetDocument());
}
void AutoplayUmaHelper::MaybeStopRecordingMutedVideoOffscreenDuration() {
if (!muted_video_offscreen_duration_visibility_observer_)
return;
if (!is_visible_) {
muted_video_autoplay_offscreen_duration_ +=
CurrentTimeTicks() - muted_video_autoplay_offscreen_start_time_;
}
DCHECK(sources_.count(AutoplaySource::kMethod));
UMA_HISTOGRAM_CUSTOM_TIMES(
"Media.Video.Autoplay.Muted.PlayMethod.OffscreenDuration",
muted_video_autoplay_offscreen_duration_, TimeDelta::FromMilliseconds(1),
kMaxOffscreenDurationUma, kOffscreenDurationUmaBucketCount);
muted_video_offscreen_duration_visibility_observer_->Stop();
muted_video_offscreen_duration_visibility_observer_ = nullptr;
muted_video_autoplay_offscreen_duration_ = TimeDelta();
MaybeUnregisterMediaElementPauseListener();
MaybeUnregisterContextDestroyedObserver();
}
void AutoplayUmaHelper::MaybeRecordUserPausedAutoplayingCrossOriginVideo() {
RecordCrossOriginAutoplayResult(CrossOriginAutoplayResult::kUserPaused);
MaybeUnregisterMediaElementPauseListener();
}
void AutoplayUmaHelper::MaybeUnregisterContextDestroyedObserver() {
if (!ShouldListenToContextDestroyed()) {
SetContext(nullptr);
}
}
void AutoplayUmaHelper::MaybeUnregisterMediaElementPauseListener() {
if (muted_video_offscreen_duration_visibility_observer_)
return;
if (ShouldRecordUserPausedAutoplayingCrossOriginVideo())
return;
element_->removeEventListener(event_type_names::kPause, this, false);
}
bool AutoplayUmaHelper::ShouldListenToContextDestroyed() const {
return muted_video_play_method_visibility_observer_ ||
muted_video_offscreen_duration_visibility_observer_;
}
bool AutoplayUmaHelper::ShouldRecordUserPausedAutoplayingCrossOriginVideo()
const {
return element_->IsInCrossOriginFrame() && element_->IsHTMLVideoElement() &&
!sources_.empty() &&
!recorded_cross_origin_autoplay_results_.count(
CrossOriginAutoplayResult::kUserPaused);
}
void AutoplayUmaHelper::Trace(blink::Visitor* visitor) {
EventListener::Trace(visitor);
ContextLifecycleObserver::Trace(visitor);
visitor->Trace(element_);
visitor->Trace(muted_video_play_method_visibility_observer_);
visitor->Trace(muted_video_offscreen_duration_visibility_observer_);
}
} // namespace blink