blob: 5c6f10ee81e5d6ad8dcf3a5142331108637cde4c [file] [log] [blame]
// Copyright 2016 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "third_party/blink/public/common/media/watch_time_reporter.h"
#include <numeric>
#include "base/bind.h"
#include "base/containers/cxx20_erase.h"
#include "base/power_monitor/power_monitor.h"
#include "base/time/time.h"
#include "media/base/pipeline_status.h"
#include "media/base/timestamp_constants.h"
#include "media/base/watch_time_keys.h"
namespace blink {
// The minimum width and height of videos to report watch time metrics for.
constexpr gfx::Size kMinimumVideoSize = gfx::Size(200, 140);
static bool IsOnBatteryPower() {
if (base::PowerMonitor::IsInitialized())
return base::PowerMonitor::IsOnBatteryPower();
return false;
}
// Helper function for managing property changes. If the watch time timer is
// running it sets the pending value otherwise it sets the current value and
// then returns true if the component needs finalize.
enum class PropertyAction { kNoActionRequired, kFinalizeRequired };
template <typename T>
PropertyAction HandlePropertyChange(T new_value,
bool is_timer_running,
WatchTimeComponent<T>* component) {
if (!component)
return PropertyAction::kNoActionRequired;
if (is_timer_running)
component->SetPendingValue(new_value);
else
component->SetCurrentValue(new_value);
return component->NeedsFinalize() ? PropertyAction::kFinalizeRequired
: PropertyAction::kNoActionRequired;
}
WatchTimeReporter::WatchTimeReporter(
media::mojom::PlaybackPropertiesPtr properties,
const gfx::Size& natural_size,
GetMediaTimeCB get_media_time_cb,
GetPipelineStatsCB get_pipeline_stats_cb,
media::mojom::MediaMetricsProvider* provider,
scoped_refptr<base::SequencedTaskRunner> task_runner,
const base::TickClock* tick_clock)
: WatchTimeReporter(std::move(properties),
false /* is_background */,
false /* is_muted */,
natural_size,
std::move(get_media_time_cb),
std::move(get_pipeline_stats_cb),
provider,
task_runner,
tick_clock) {}
WatchTimeReporter::WatchTimeReporter(
media::mojom::PlaybackPropertiesPtr properties,
bool is_background,
bool is_muted,
const gfx::Size& natural_size,
GetMediaTimeCB get_media_time_cb,
GetPipelineStatsCB get_pipeline_stats_cb,
media::mojom::MediaMetricsProvider* provider,
scoped_refptr<base::SequencedTaskRunner> task_runner,
const base::TickClock* tick_clock)
: properties_(std::move(properties)),
is_background_(is_background),
is_muted_(is_muted),
get_media_time_cb_(std::move(get_media_time_cb)),
get_pipeline_stats_cb_(std::move(get_pipeline_stats_cb)),
reporting_timer_(tick_clock),
natural_size_(natural_size) {
DCHECK(get_media_time_cb_);
DCHECK(get_pipeline_stats_cb_);
DCHECK(properties_->has_audio || properties_->has_video);
DCHECK_EQ(is_background, properties_->is_background);
// The background reporter receives play/pause events instead of visibility
// changes, so it must always be visible to function correctly.
if (is_background_)
DCHECK(is_visible_);
// The muted reporter receives play/pause events instead of volume changes, so
// its volume must always be audible to function correctly.
if (is_muted_)
DCHECK_EQ(volume_, 1.0);
base::PowerMonitor::AddPowerStateObserver(this);
provider->AcquireWatchTimeRecorder(properties_->Clone(),
recorder_.BindNewPipeAndPassReceiver());
reporting_timer_.SetTaskRunner(task_runner);
base_component_ = CreateBaseComponent();
power_component_ = CreatePowerComponent();
if (!is_background_) {
controls_component_ = CreateControlsComponent();
if (properties_->has_video)
display_type_component_ = CreateDisplayTypeComponent();
}
// If this is a sub-reporter we're done.
if (is_background_ || is_muted_)
return;
// Background watch time is reported by creating an background only watch time
// reporter which receives play when hidden and pause when shown. This avoids
// unnecessary complexity inside the UpdateWatchTime() for handling this case.
auto prop_copy = properties_.Clone();
prop_copy->is_background = true;
background_reporter_.reset(new WatchTimeReporter(
std::move(prop_copy), true /* is_background */, false /* is_muted */,
natural_size_, get_media_time_cb_, get_pipeline_stats_cb_, provider,
task_runner, tick_clock));
// Muted watch time is only reported for audio+video playback.
if (!properties_->has_video || !properties_->has_audio)
return;
// Similar to the above, muted watch time is reported by creating a muted only
// watch time reporter which receives play when muted and pause when audible.
prop_copy = properties_.Clone();
prop_copy->is_muted = true;
muted_reporter_.reset(new WatchTimeReporter(
std::move(prop_copy), false /* is_background */, true /* is_muted */,
natural_size_, get_media_time_cb_, get_pipeline_stats_cb_, provider,
task_runner, tick_clock));
}
WatchTimeReporter::~WatchTimeReporter() {
background_reporter_.reset();
muted_reporter_.reset();
// This is our last chance, so finalize now if there's anything remaining.
in_shutdown_ = true;
MaybeFinalizeWatchTime(FinalizeTime::IMMEDIATELY);
base::PowerMonitor::RemovePowerStateObserver(this);
}
void WatchTimeReporter::OnPlaying() {
if (background_reporter_ && !is_visible_)
background_reporter_->OnPlaying();
if (muted_reporter_ && !volume_)
muted_reporter_->OnPlaying();
is_playing_ = true;
is_seeking_ = false;
MaybeStartReportingTimer(get_media_time_cb_.Run());
}
void WatchTimeReporter::OnPaused() {
if (background_reporter_)
background_reporter_->OnPaused();
if (muted_reporter_)
muted_reporter_->OnPaused();
is_playing_ = false;
MaybeFinalizeWatchTime(FinalizeTime::ON_NEXT_UPDATE);
}
void WatchTimeReporter::OnSeeking() {
if (background_reporter_)
background_reporter_->OnSeeking();
if (muted_reporter_)
muted_reporter_->OnSeeking();
// Seek is a special case that does not have hysteresis, when this is called
// the seek is imminent, so finalize the previous playback immediately.
is_seeking_ = true;
MaybeFinalizeWatchTime(FinalizeTime::IMMEDIATELY);
}
void WatchTimeReporter::OnVolumeChange(double volume) {
if (background_reporter_)
background_reporter_->OnVolumeChange(volume);
// The muted reporter should never receive volume changes.
DCHECK(!is_muted_);
const double old_volume = volume_;
volume_ = volume;
// We're only interesting in transitions in and out of the muted state.
if (!old_volume && volume) {
if (muted_reporter_)
muted_reporter_->OnPaused();
MaybeStartReportingTimer(get_media_time_cb_.Run());
} else if (old_volume && !volume_) {
if (muted_reporter_ && is_playing_)
muted_reporter_->OnPlaying();
MaybeFinalizeWatchTime(FinalizeTime::ON_NEXT_UPDATE);
}
}
void WatchTimeReporter::OnShown() {
// The background reporter should never receive visibility changes.
DCHECK(!is_background_);
if (background_reporter_)
background_reporter_->OnPaused();
if (muted_reporter_)
muted_reporter_->OnShown();
is_visible_ = true;
MaybeStartReportingTimer(get_media_time_cb_.Run());
}
void WatchTimeReporter::OnHidden() {
// The background reporter should never receive visibility changes.
DCHECK(!is_background_);
if (background_reporter_ && is_playing_)
background_reporter_->OnPlaying();
if (muted_reporter_)
muted_reporter_->OnHidden();
is_visible_ = false;
MaybeFinalizeWatchTime(FinalizeTime::ON_NEXT_UPDATE);
}
void WatchTimeReporter::OnError(media::PipelineStatus status) {
// Since playback should have stopped by this point, go ahead and send the
// error directly instead of on the next timer tick. It won't be recorded
// until finalization anyways.
recorder_->OnError(status);
if (background_reporter_)
background_reporter_->OnError(status);
if (muted_reporter_)
muted_reporter_->OnError(status);
}
void WatchTimeReporter::OnUnderflow() {
if (background_reporter_)
background_reporter_->OnUnderflow();
if (muted_reporter_)
muted_reporter_->OnUnderflow();
if (!reporting_timer_.IsRunning())
return;
if (!pending_underflow_events_.empty())
DCHECK_NE(pending_underflow_events_.back().duration, media::kNoTimestamp);
// In the event of a pending finalize, we don't want to count underflow events
// that occurred after the finalize time. Yet if the finalize is canceled we
// want to ensure they are all recorded.
pending_underflow_events_.push_back(
{false, get_media_time_cb_.Run(), media::kNoTimestamp});
}
void WatchTimeReporter::OnUnderflowComplete(base::TimeDelta elapsed) {
if (background_reporter_)
background_reporter_->OnUnderflowComplete(elapsed);
if (muted_reporter_)
muted_reporter_->OnUnderflowComplete(elapsed);
if (!reporting_timer_.IsRunning())
return;
// Drop this underflow completion if we don't have a corresponding underflow
// start event; this can happen if a finalize occurs between the underflow and
// the completion.
if (pending_underflow_events_.empty())
return;
// There should only ever be one outstanding underflow, so stick the duration
// in the last underflow event.
DCHECK_EQ(pending_underflow_events_.back().duration, media::kNoTimestamp);
pending_underflow_events_.back().duration = elapsed;
}
void WatchTimeReporter::OnNativeControlsEnabled() {
OnNativeControlsChanged(true);
}
void WatchTimeReporter::OnNativeControlsDisabled() {
OnNativeControlsChanged(false);
}
void WatchTimeReporter::OnDisplayTypeInline() {
OnDisplayTypeChanged(DisplayType::kInline);
}
void WatchTimeReporter::OnDisplayTypeFullscreen() {
OnDisplayTypeChanged(DisplayType::kFullscreen);
}
void WatchTimeReporter::OnDisplayTypePictureInPicture() {
OnDisplayTypeChanged(DisplayType::kPictureInPicture);
}
void WatchTimeReporter::UpdateSecondaryProperties(
media::mojom::SecondaryPlaybackPropertiesPtr secondary_properties) {
// Flush any unrecorded watch time before updating the secondary properties to
// ensure the UKM record is finalized with up-to-date watch time information.
if (reporting_timer_.IsRunning())
RecordWatchTime();
recorder_->UpdateSecondaryProperties(secondary_properties.Clone());
if (background_reporter_) {
background_reporter_->UpdateSecondaryProperties(
secondary_properties.Clone());
}
if (muted_reporter_)
muted_reporter_->UpdateSecondaryProperties(secondary_properties.Clone());
// A change in resolution may affect ShouldReportingTimerRun().
bool original_should_run = ShouldReportingTimerRun();
natural_size_ = secondary_properties->natural_size;
bool should_run = ShouldReportingTimerRun();
if (original_should_run != should_run) {
if (should_run) {
MaybeStartReportingTimer(get_media_time_cb_.Run());
} else {
MaybeFinalizeWatchTime(FinalizeTime::ON_NEXT_UPDATE);
}
}
}
void WatchTimeReporter::SetAutoplayInitiated(bool autoplay_initiated) {
recorder_->SetAutoplayInitiated(autoplay_initiated);
if (background_reporter_)
background_reporter_->SetAutoplayInitiated(autoplay_initiated);
if (muted_reporter_)
muted_reporter_->SetAutoplayInitiated(autoplay_initiated);
}
void WatchTimeReporter::OnDurationChanged(base::TimeDelta duration) {
recorder_->OnDurationChanged(duration);
if (background_reporter_)
background_reporter_->OnDurationChanged(duration);
if (muted_reporter_)
muted_reporter_->OnDurationChanged(duration);
}
void WatchTimeReporter::OnPowerStateChange(bool on_battery_power) {
if (HandlePropertyChange<bool>(on_battery_power, reporting_timer_.IsRunning(),
power_component_.get()) ==
PropertyAction::kFinalizeRequired) {
RestartTimerForHysteresis();
}
}
void WatchTimeReporter::OnNativeControlsChanged(bool has_native_controls) {
if (muted_reporter_)
muted_reporter_->OnNativeControlsChanged(has_native_controls);
if (HandlePropertyChange<bool>(
has_native_controls, reporting_timer_.IsRunning(),
controls_component_.get()) == PropertyAction::kFinalizeRequired) {
RestartTimerForHysteresis();
}
}
void WatchTimeReporter::OnDisplayTypeChanged(DisplayType display_type) {
if (muted_reporter_)
muted_reporter_->OnDisplayTypeChanged(display_type);
if (HandlePropertyChange<DisplayType>(
display_type, reporting_timer_.IsRunning(),
display_type_component_.get()) == PropertyAction::kFinalizeRequired) {
RestartTimerForHysteresis();
}
}
bool WatchTimeReporter::ShouldReportWatchTime() const {
// Report listen time or watch time for videos of sufficient size.
return properties_->has_video
? (natural_size_.height() >= kMinimumVideoSize.height() &&
natural_size_.width() >= kMinimumVideoSize.width())
: properties_->has_audio;
}
bool WatchTimeReporter::ShouldReportingTimerRun() const {
// TODO(dalecurtis): We should only consider |volume_| when there is actually
// an audio track; requires updating lots of tests to fix.
return ShouldReportWatchTime() && is_playing_ && volume_ && is_visible_ &&
!in_shutdown_ && !is_seeking_ && has_valid_start_timestamp_;
}
void WatchTimeReporter::MaybeStartReportingTimer(
base::TimeDelta start_timestamp) {
DCHECK_GE(start_timestamp, base::TimeDelta());
// It's possible for |current_time| to be kInfiniteDuration here if the page
// seeks to kInfiniteDuration (2**64 - 1) when Duration() is infinite. There
// is no possible elapsed watch time when this occurs, so don't start the
// WatchTimeReporter at this time. If a later seek puts us earlier in the
// stream this method will be called again after OnSeeking().
has_valid_start_timestamp_ = start_timestamp != media::kInfiniteDuration;
// Don't start the timer if our state indicates we shouldn't; this check is
// important since the various event handlers do not have to care about the
// state of other events.
const bool should_start = ShouldReportingTimerRun();
if (reporting_timer_.IsRunning()) {
base_component_->SetPendingValue(should_start);
return;
}
base_component_->SetCurrentValue(should_start);
if (!should_start)
return;
if (properties_->has_video) {
initial_stats_ = get_pipeline_stats_cb_.Run();
last_stats_ = media::PipelineStatistics();
}
ResetUnderflowState();
base_component_->OnReportingStarted(start_timestamp);
power_component_->OnReportingStarted(start_timestamp);
if (controls_component_)
controls_component_->OnReportingStarted(start_timestamp);
if (display_type_component_)
display_type_component_->OnReportingStarted(start_timestamp);
reporting_timer_.Start(FROM_HERE, reporting_interval_, this,
&WatchTimeReporter::UpdateWatchTime);
}
void WatchTimeReporter::MaybeFinalizeWatchTime(FinalizeTime finalize_time) {
if (HandlePropertyChange<bool>(
ShouldReportingTimerRun(), reporting_timer_.IsRunning(),
base_component_.get()) == PropertyAction::kNoActionRequired) {
return;
}
if (finalize_time == FinalizeTime::IMMEDIATELY) {
UpdateWatchTime();
return;
}
// Always restart the timer when finalizing, so that we allow for the full
// length of |kReportingInterval| to elapse for hysteresis purposes.
DCHECK_EQ(finalize_time, FinalizeTime::ON_NEXT_UPDATE);
RestartTimerForHysteresis();
}
void WatchTimeReporter::RestartTimerForHysteresis() {
// Restart the reporting timer so the full hysteresis is afforded.
DCHECK(reporting_timer_.IsRunning());
reporting_timer_.Start(FROM_HERE, reporting_interval_, this,
&WatchTimeReporter::UpdateWatchTime);
}
void WatchTimeReporter::RecordWatchTime() {
// If we're finalizing, use the media time at time of finalization.
const base::TimeDelta current_timestamp =
base_component_->NeedsFinalize() ? base_component_->end_timestamp()
: get_media_time_cb_.Run();
// Pass along any underflow events which have occurred since the last report.
if (!pending_underflow_events_.empty()) {
const int last_underflow_count = total_underflow_count_;
const int last_completed_underflow_count = total_completed_underflow_count_;
for (auto& ufe : pending_underflow_events_) {
// Since the underflow occurred after finalize, ignore the event and mark
// it for deletion.
if (ufe.timestamp > current_timestamp) {
ufe.reported = true;
ufe.duration = base::TimeDelta();
continue;
}
if (!ufe.reported) {
ufe.reported = true;
++total_underflow_count_;
}
// Drop any rebuffer completions that took more than a minute. For our
// purposes these are considered as timeouts. We want a maximum since
// rebuffer duration is in real time and not media time, which means if
// the rebuffer spans a suspend/resume the time can be arbitrarily long.
constexpr base::TimeDelta kMaximumRebufferDuration = base::Minutes(1);
if (ufe.duration != media::kNoTimestamp &&
ufe.duration <= kMaximumRebufferDuration) {
++total_completed_underflow_count_;
total_underflow_duration_ += ufe.duration;
}
}
base::EraseIf(pending_underflow_events_, [](const UnderflowEvent& ufe) {
return ufe.reported && ufe.duration != media::kNoTimestamp;
});
if (last_underflow_count != total_underflow_count_)
recorder_->UpdateUnderflowCount(total_underflow_count_);
if (last_completed_underflow_count != total_completed_underflow_count_) {
recorder_->UpdateUnderflowDuration(total_completed_underflow_count_,
total_underflow_duration_);
}
}
if (properties_->has_video) {
auto stats = get_pipeline_stats_cb_.Run();
DCHECK_GE(stats.video_frames_decoded, initial_stats_.video_frames_decoded);
DCHECK_GE(stats.video_frames_dropped, initial_stats_.video_frames_dropped);
// Offset the stats based on where they were when we started reporting.
stats.video_frames_decoded -= initial_stats_.video_frames_decoded;
stats.video_frames_dropped -= initial_stats_.video_frames_dropped;
// Only send updates.
if (last_stats_.video_frames_decoded != stats.video_frames_decoded ||
last_stats_.video_frames_dropped != stats.video_frames_dropped) {
recorder_->UpdateVideoDecodeStats(stats.video_frames_decoded,
stats.video_frames_dropped);
last_stats_ = stats;
}
}
// Record watch time for all components.
base_component_->RecordWatchTime(current_timestamp);
power_component_->RecordWatchTime(current_timestamp);
if (display_type_component_)
display_type_component_->RecordWatchTime(current_timestamp);
if (controls_component_)
controls_component_->RecordWatchTime(current_timestamp);
// Update the last timestamp with the current timestamp.
recorder_->OnCurrentTimestampChanged(current_timestamp);
}
void WatchTimeReporter::UpdateWatchTime() {
// First record watch time.
RecordWatchTime();
// Second, process any pending finalize events.
std::vector<media::WatchTimeKey> keys_to_finalize;
if (power_component_->NeedsFinalize())
power_component_->Finalize(&keys_to_finalize);
if (display_type_component_ && display_type_component_->NeedsFinalize())
display_type_component_->Finalize(&keys_to_finalize);
if (controls_component_ && controls_component_->NeedsFinalize())
controls_component_->Finalize(&keys_to_finalize);
// Then finalize the base component.
if (!base_component_->NeedsFinalize()) {
if (!keys_to_finalize.empty())
recorder_->FinalizeWatchTime(keys_to_finalize);
return;
}
// Always send finalize, even if we don't currently have any data, it's
// harmless to send since nothing will be logged if we've already finalized.
base_component_->Finalize(&keys_to_finalize);
recorder_->FinalizeWatchTime({});
// Stop the timer if this is supposed to be our last tick.
ResetUnderflowState();
reporting_timer_.Stop();
}
void WatchTimeReporter::ResetUnderflowState() {
total_underflow_count_ = total_completed_underflow_count_ = 0;
total_underflow_duration_ = base::TimeDelta();
pending_underflow_events_.clear();
}
#define NORMAL_KEY(key) \
((properties_->has_video && properties_->has_audio) \
? (is_background_ \
? media::WatchTimeKey::kAudioVideoBackground##key \
: (is_muted_ ? media::WatchTimeKey::kAudioVideoMuted##key \
: media::WatchTimeKey::kAudioVideo##key)) \
: properties_->has_video \
? (is_background_ ? media::WatchTimeKey::kVideoBackground##key \
: media::WatchTimeKey::kVideo##key) \
: (is_background_ ? media::WatchTimeKey::kAudioBackground##key \
: media::WatchTimeKey::kAudio##key))
std::unique_ptr<WatchTimeComponent<bool>>
WatchTimeReporter::CreateBaseComponent() {
std::vector<media::WatchTimeKey> keys_to_finalize;
keys_to_finalize.emplace_back(NORMAL_KEY(All));
if (properties_->is_mse)
keys_to_finalize.emplace_back(NORMAL_KEY(Mse));
else
keys_to_finalize.emplace_back(NORMAL_KEY(Src));
if (properties_->is_eme)
keys_to_finalize.emplace_back(NORMAL_KEY(Eme));
if (properties_->is_embedded_media_experience)
keys_to_finalize.emplace_back(NORMAL_KEY(EmbeddedExperience));
return std::make_unique<WatchTimeComponent<bool>>(
false, std::move(keys_to_finalize),
WatchTimeComponent<bool>::ValueToKeyCB(), get_media_time_cb_,
recorder_.get());
}
std::unique_ptr<WatchTimeComponent<bool>>
WatchTimeReporter::CreatePowerComponent() {
std::vector<media::WatchTimeKey> keys_to_finalize{NORMAL_KEY(Battery),
NORMAL_KEY(Ac)};
return std::make_unique<WatchTimeComponent<bool>>(
IsOnBatteryPower(), std::move(keys_to_finalize),
base::BindRepeating(&WatchTimeReporter::GetPowerKey,
base::Unretained(this)),
get_media_time_cb_, recorder_.get());
}
media::WatchTimeKey WatchTimeReporter::GetPowerKey(bool is_on_battery_power) {
return is_on_battery_power ? NORMAL_KEY(Battery) : NORMAL_KEY(Ac);
}
#undef NORMAL_KEY
#define FOREGROUND_KEY(key) \
((properties_->has_video && properties_->has_audio) \
? (is_muted_ ? media::WatchTimeKey::kAudioVideoMuted##key \
: media::WatchTimeKey::kAudioVideo##key) \
: properties_->has_audio ? media::WatchTimeKey::kAudio##key \
: media::WatchTimeKey::kVideo##key)
std::unique_ptr<WatchTimeComponent<bool>>
WatchTimeReporter::CreateControlsComponent() {
DCHECK(!is_background_);
std::vector<media::WatchTimeKey> keys_to_finalize{
FOREGROUND_KEY(NativeControlsOn), FOREGROUND_KEY(NativeControlsOff)};
return std::make_unique<WatchTimeComponent<bool>>(
false, std::move(keys_to_finalize),
base::BindRepeating(&WatchTimeReporter::GetControlsKey,
base::Unretained(this)),
get_media_time_cb_, recorder_.get());
}
media::WatchTimeKey WatchTimeReporter::GetControlsKey(
bool has_native_controls) {
return has_native_controls ? FOREGROUND_KEY(NativeControlsOn)
: FOREGROUND_KEY(NativeControlsOff);
}
#undef FOREGROUND_KEY
#define DISPLAY_TYPE_KEY(key) \
(properties_->has_audio \
? (is_muted_ ? media::WatchTimeKey::kAudioVideoMuted##key \
: media::WatchTimeKey::kAudioVideo##key) \
: media::WatchTimeKey::kVideo##key)
std::unique_ptr<WatchTimeComponent<DisplayType>>
WatchTimeReporter::CreateDisplayTypeComponent() {
DCHECK(properties_->has_video);
DCHECK(!is_background_);
std::vector<media::WatchTimeKey> keys_to_finalize{
DISPLAY_TYPE_KEY(DisplayInline), DISPLAY_TYPE_KEY(DisplayFullscreen),
DISPLAY_TYPE_KEY(DisplayPictureInPicture)};
return std::make_unique<WatchTimeComponent<DisplayType>>(
DisplayType::kInline, std::move(keys_to_finalize),
base::BindRepeating(&WatchTimeReporter::GetDisplayTypeKey,
base::Unretained(this)),
get_media_time_cb_, recorder_.get());
}
media::WatchTimeKey WatchTimeReporter::GetDisplayTypeKey(
DisplayType display_type) {
switch (display_type) {
case DisplayType::kInline:
return DISPLAY_TYPE_KEY(DisplayInline);
case DisplayType::kFullscreen:
return DISPLAY_TYPE_KEY(DisplayFullscreen);
case DisplayType::kPictureInPicture:
return DISPLAY_TYPE_KEY(DisplayPictureInPicture);
}
}
#undef DISPLAY_TYPE_KEY
} // namespace blink