| // 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 |