blob: 71cad4510129146e95a64a76a2bb982ffaa29e70 [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 "cobalt/media/blink/watch_time_reporter.h"
#include "base/power_monitor/power_monitor.h"
namespace cobalt {
namespace media {
// The minimum amount of media playback which can elapse before we'll report
// watch time metrics for a playback.
constexpr base::TimeDelta kMinimumElapsedWatchTime =
base::TimeDelta::FromSeconds(7);
// The minimum width and height of videos to report watch time metrics for.
constexpr gfx::Size kMinimumVideoSize = gfx::Size(200, 200);
static bool IsOnBatteryPower() {
if (base::PowerMonitor* pm = base::PowerMonitor::Get())
return pm->IsOnBatteryPower();
return false;
}
WatchTimeReporter::WatchTimeReporter(bool has_audio, bool has_video,
bool is_mse, bool is_encrypted,
scoped_refptr<MediaLog> media_log,
const gfx::Size& initial_video_size,
const GetMediaTimeCB& get_media_time_cb)
: has_audio_(has_audio),
has_video_(has_video),
is_mse_(is_mse),
is_encrypted_(is_encrypted),
media_log_(std::move(media_log)),
initial_video_size_(initial_video_size),
get_media_time_cb_(get_media_time_cb) {
DCHECK(!get_media_time_cb_.is_null());
DCHECK(has_audio_ || has_video_);
if (has_video_) DCHECK(!initial_video_size_.IsEmpty());
if (base::PowerMonitor* pm = base::PowerMonitor::Get()) pm->AddObserver(this);
}
WatchTimeReporter::~WatchTimeReporter() {
// If the timer is still running, finalize immediately, this is our last
// chance to capture metrics.
if (reporting_timer_.IsRunning())
MaybeFinalizeWatchTime(FinalizeTime::IMMEDIATELY);
if (base::PowerMonitor* pm = base::PowerMonitor::Get())
pm->RemoveObserver(this);
}
void WatchTimeReporter::OnPlaying() {
is_playing_ = true;
MaybeStartReportingTimer(get_media_time_cb_.Run());
}
void WatchTimeReporter::OnPaused() {
is_playing_ = false;
MaybeFinalizeWatchTime(FinalizeTime::ON_NEXT_UPDATE);
}
void WatchTimeReporter::OnSeeking() {
if (!reporting_timer_.IsRunning()) return;
// Seek is a special case that does not have hysteresis, when this is called
// the seek is imminent, so finalize the previous playback immediately.
// Don't trample an existing end timestamp.
if (end_timestamp_ == kNoTimestamp) end_timestamp_ = get_media_time_cb_.Run();
UpdateWatchTime();
}
void WatchTimeReporter::OnVolumeChange(double volume) {
const double old_volume = volume_;
volume_ = volume;
// We're only interesting in transitions in and out of the muted state.
if (!old_volume && volume)
MaybeStartReportingTimer(get_media_time_cb_.Run());
else if (old_volume && !volume_)
MaybeFinalizeWatchTime(FinalizeTime::ON_NEXT_UPDATE);
}
void WatchTimeReporter::OnShown() {
is_visible_ = true;
MaybeStartReportingTimer(get_media_time_cb_.Run());
}
void WatchTimeReporter::OnHidden() {
is_visible_ = false;
MaybeFinalizeWatchTime(FinalizeTime::ON_NEXT_UPDATE);
}
void WatchTimeReporter::OnPowerStateChange(bool on_battery_power) {
if (!reporting_timer_.IsRunning()) return;
// Defer changing |is_on_battery_power_| until the next watch time report to
// avoid momentary power changes from affecting the results.
if (is_on_battery_power_ != on_battery_power) {
end_timestamp_for_power_ = get_media_time_cb_.Run();
// Restart the reporting timer so the full hysteresis is afforded.
reporting_timer_.Start(FROM_HERE, reporting_interval_, this,
&WatchTimeReporter::UpdateWatchTime);
return;
}
end_timestamp_for_power_ = kNoTimestamp;
}
bool WatchTimeReporter::ShouldReportWatchTime() {
// Only report watch time for media of sufficient size with both audio and
// video tracks present.
return has_audio_ && has_video_ &&
initial_video_size_.height() >= kMinimumVideoSize.height() &&
initial_video_size_.width() >= kMinimumVideoSize.width();
}
void WatchTimeReporter::MaybeStartReportingTimer(
base::TimeDelta start_timestamp) {
// Don't start the timer if any of 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.
if (!ShouldReportWatchTime() || !is_playing_ || !volume_ || !is_visible_) {
// If we reach this point the timer should already have been stopped or
// there is a pending finalize in flight.
DCHECK(!reporting_timer_.IsRunning() || end_timestamp_ != kNoTimestamp);
return;
}
// If we haven't finalized the last watch time metrics yet, count this
// playback as a continuation of the previous metrics.
if (end_timestamp_ != kNoTimestamp) {
DCHECK(reporting_timer_.IsRunning());
end_timestamp_ = kNoTimestamp;
return;
}
// Don't restart the timer if it's already running.
if (reporting_timer_.IsRunning()) return;
last_media_timestamp_ = end_timestamp_for_power_ = kNoTimestamp;
is_on_battery_power_ = IsOnBatteryPower();
start_timestamp_ = start_timestamp_for_power_ = start_timestamp;
reporting_timer_.Start(FROM_HERE, reporting_interval_, this,
&WatchTimeReporter::UpdateWatchTime);
}
void WatchTimeReporter::MaybeFinalizeWatchTime(FinalizeTime finalize_time) {
// Don't finalize if the timer is already stopped.
if (!reporting_timer_.IsRunning()) return;
// Don't trample an existing finalize; the first takes precedence.
if (end_timestamp_ == kNoTimestamp) end_timestamp_ = get_media_time_cb_.Run();
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);
reporting_timer_.Start(FROM_HERE, reporting_interval_, this,
&WatchTimeReporter::UpdateWatchTime);
}
void WatchTimeReporter::UpdateWatchTime() {
DCHECK(ShouldReportWatchTime());
const bool is_finalizing = end_timestamp_ != kNoTimestamp;
const bool is_power_change_pending = end_timestamp_for_power_ != kNoTimestamp;
// If we're finalizing the log, use the media time value at the time of
// finalization.
const base::TimeDelta current_timestamp =
is_finalizing ? end_timestamp_ : get_media_time_cb_.Run();
const base::TimeDelta elapsed = current_timestamp - start_timestamp_;
// Only report watch time after some minimum amount has elapsed. Don't update
// watch time if media time hasn't changed since the last run; this may occur
// if a seek is taking some time to complete or the playback is stalled for
// some reason.
if (elapsed >= kMinimumElapsedWatchTime &&
last_media_timestamp_ != current_timestamp) {
last_media_timestamp_ = current_timestamp;
std::unique_ptr<MediaLogEvent> log_event =
media_log_->CreateEvent(MediaLogEvent::Type::WATCH_TIME_UPDATE);
log_event->params.SetDoubleWithoutPathExpansion(
MediaLog::kWatchTimeAudioVideoAll, elapsed.InSecondsF());
if (is_mse_) {
log_event->params.SetDoubleWithoutPathExpansion(
MediaLog::kWatchTimeAudioVideoMse, elapsed.InSecondsF());
} else {
log_event->params.SetDoubleWithoutPathExpansion(
MediaLog::kWatchTimeAudioVideoSrc, elapsed.InSecondsF());
}
if (is_encrypted_) {
log_event->params.SetDoubleWithoutPathExpansion(
MediaLog::kWatchTimeAudioVideoEme, elapsed.InSecondsF());
}
// Record watch time using the last known value for |is_on_battery_power_|;
// if there's a |pending_power_change_| use that to accurately finalize the
// last bits of time in the previous bucket.
const base::TimeDelta elapsed_power =
(is_power_change_pending ? end_timestamp_for_power_
: current_timestamp) -
start_timestamp_for_power_;
// Again, only update watch time if enough time has elapsed; we need to
// recheck the elapsed time here since the power source can change anytime.
if (elapsed_power >= kMinimumElapsedWatchTime) {
if (is_on_battery_power_) {
log_event->params.SetDoubleWithoutPathExpansion(
MediaLog::kWatchTimeAudioVideoBattery, elapsed_power.InSecondsF());
} else {
log_event->params.SetDoubleWithoutPathExpansion(
MediaLog::kWatchTimeAudioVideoAc, elapsed_power.InSecondsF());
}
}
if (is_finalizing)
log_event->params.SetBoolean(MediaLog::kWatchTimeFinalize, true);
else if (is_power_change_pending)
log_event->params.SetBoolean(MediaLog::kWatchTimeFinalizePower, true);
DVLOG(2) << "Sending watch time update.";
media_log_->AddEvent(std::move(log_event));
}
if (is_power_change_pending) {
// Invert battery power status here instead of using the value returned by
// the PowerObserver since there may be a pending OnPowerStateChange().
is_on_battery_power_ = !is_on_battery_power_;
start_timestamp_for_power_ = end_timestamp_for_power_;
end_timestamp_for_power_ = kNoTimestamp;
}
// Stop the timer if this is supposed to be our last tick.
if (is_finalizing) {
end_timestamp_ = kNoTimestamp;
reporting_timer_.Stop();
}
}
} // namespace media
} // namespace cobalt