blob: 0e3745f13a06a8079b46d0d2db80e3455176f8db [file] [log] [blame]
// Copyright 2015 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/filters/video_cadence_estimator.h"
#include <algorithm>
#include <cmath>
#include <iterator>
#include <limits>
#include <string>
#include "base/metrics/histogram.h"
namespace cobalt {
namespace media {
// To prevent oscillation in and out of cadence or between cadence values, we
// require some time to elapse before a cadence switch is accepted.
const int kMinimumCadenceDurationMs = 100;
// The numbers are used to decide whether the current video is variable FPS or
// constant FPS. If ratio of the sample deviation and the render length is
// above |kVariableFPSFactor|, then it is recognized as a variable FPS, and if
// the ratio is below |kConstantFPSFactor|, then it is recognized as a constant
// FPS, and if the ratio is in between the two factors, then we do not change
// previous recognition.
const double kVariableFPSFactor = 0.55;
const double kConstantFPSFactor = 0.45;
// Records the number of cadence changes to UMA.
static void HistogramCadenceChangeCount(int cadence_changes) {
const int kCadenceChangeMax = 10;
UMA_HISTOGRAM_CUSTOM_COUNTS("Media.VideoRenderer.CadenceChanges",
cadence_changes, 1, kCadenceChangeMax,
kCadenceChangeMax);
}
// Construct a Cadence vector, a vector of integers satisfying the following
// conditions:
// 1. Size is |n|.
// 2. Sum of entries is |k|.
// 3. Each entry is in {|k|/|n|, |k|/|n| + 1}.
// 4. Distribution of |k|/|n| and |k|/|n| + 1 is as even as possible.
VideoCadenceEstimator::Cadence ConstructCadence(int k, int n) {
const int quotient = k / n;
std::vector<int> output(n, 0);
// Fill the vector entries with |quotient| or |quotient + 1|, and make sure
// the two values are distributed as evenly as possible.
int target_accumulate = 0;
int actual_accumulate = 0;
for (int i = 0; i < n; ++i) {
// After each loop
// target_accumulate = (i + 1) * k
// actual_accumulate = \sum_{j = 0}^i {n * V[j]} where V is output vector
// We want to make actual_accumulate as close to target_accumulate as
// possible.
// One exception is that in case k < n, we always want the vector to start
// with 1 to make sure the first frame is always rendered.
// (To avoid float calculation, we use scaled version of accumulated count)
target_accumulate += k;
const int target_current = target_accumulate - actual_accumulate;
if ((i == 0 && k < n) || target_current * 2 >= n * (quotient * 2 + 1)) {
output[i] = quotient + 1;
} else {
output[i] = quotient;
}
actual_accumulate += output[i] * n;
}
return output;
}
VideoCadenceEstimator::VideoCadenceEstimator(
base::TimeDelta minimum_time_until_max_drift)
: cadence_hysteresis_threshold_(
base::TimeDelta::FromMilliseconds(kMinimumCadenceDurationMs)),
minimum_time_until_max_drift_(minimum_time_until_max_drift),
is_variable_frame_rate_(false) {
Reset();
}
VideoCadenceEstimator::~VideoCadenceEstimator() {}
void VideoCadenceEstimator::Reset() {
cadence_.clear();
pending_cadence_.clear();
cadence_changes_ = render_intervals_cadence_held_ = 0;
first_update_call_ = true;
}
bool VideoCadenceEstimator::UpdateCadenceEstimate(
base::TimeDelta render_interval, base::TimeDelta frame_duration,
base::TimeDelta frame_duration_deviation,
base::TimeDelta max_acceptable_drift) {
DCHECK_GT(render_interval, base::TimeDelta());
DCHECK_GT(frame_duration, base::TimeDelta());
if (frame_duration_deviation > kVariableFPSFactor * render_interval) {
is_variable_frame_rate_ = true;
} else if (frame_duration_deviation < kConstantFPSFactor * render_interval) {
is_variable_frame_rate_ = false;
}
// Variable FPS detected, turn off Cadence by force.
if (is_variable_frame_rate_) {
render_intervals_cadence_held_ = 0;
if (!cadence_.empty()) {
cadence_.clear();
return true;
}
return false;
}
base::TimeDelta time_until_max_drift;
// See if we can find a cadence which fits the data.
Cadence new_cadence =
CalculateCadence(render_interval, frame_duration, max_acceptable_drift,
&time_until_max_drift);
// If this is the first time UpdateCadenceEstimate() has been called,
// initialize the histogram with a zero count for cadence changes; this
// allows us to track the number of playbacks which have cadence at all.
if (first_update_call_) {
DCHECK_EQ(cadence_changes_, 0);
first_update_call_ = false;
HistogramCadenceChangeCount(0);
}
// If nothing changed, do nothing.
if (new_cadence == cadence_) {
// Clear cadence hold to pending values from accumulating incorrectly.
render_intervals_cadence_held_ = 0;
return false;
}
// Wait until enough render intervals have elapsed before accepting the
// cadence change. Prevents oscillation of the cadence selection.
bool update_pending_cadence = true;
if (new_cadence == pending_cadence_ ||
cadence_hysteresis_threshold_ <= render_interval) {
if (++render_intervals_cadence_held_ * render_interval >=
cadence_hysteresis_threshold_) {
DVLOG(1) << "Cadence switch: " << CadenceToString(cadence_) << " -> "
<< CadenceToString(new_cadence)
<< " :: Time until drift exceeded: " << time_until_max_drift;
cadence_.swap(new_cadence);
// Note: Because this class is transitively owned by a garbage collected
// object, WebMediaPlayer, we log cadence changes as they are encountered.
HistogramCadenceChangeCount(++cadence_changes_);
return true;
}
update_pending_cadence = false;
}
DVLOG(2) << "Hysteresis prevented cadence switch: "
<< CadenceToString(cadence_) << " -> "
<< CadenceToString(new_cadence);
if (update_pending_cadence) {
pending_cadence_.swap(new_cadence);
render_intervals_cadence_held_ = 1;
}
return false;
}
int VideoCadenceEstimator::GetCadenceForFrame(uint64_t frame_number) const {
DCHECK(has_cadence());
return cadence_[frame_number % cadence_.size()];
}
VideoCadenceEstimator::Cadence VideoCadenceEstimator::CalculateCadence(
base::TimeDelta render_interval, base::TimeDelta frame_duration,
base::TimeDelta max_acceptable_drift,
base::TimeDelta* time_until_max_drift) const {
// The perfect cadence is the number of render intervals per frame.
const double perfect_cadence =
frame_duration.InSecondsF() / render_interval.InSecondsF();
// This case is very simple, just return a single frame cadence, because it
// is impossible for us to accumulate drift as large as max_acceptable_drift
// within minimum_time_until_max_drift.
if (max_acceptable_drift >= minimum_time_until_max_drift_) {
int cadence_value = round(perfect_cadence);
if (cadence_value == 0) cadence_value = 1;
Cadence result = ConstructCadence(cadence_value, 1);
const double error = std::fabs(1.0 - perfect_cadence / cadence_value);
*time_until_max_drift = max_acceptable_drift / error;
return result;
}
// We want to construct a cadence pattern to approximate the perfect cadence
// while ensuring error doesn't accumulate too quickly.
const double drift_ratio = max_acceptable_drift.InSecondsF() /
minimum_time_until_max_drift_.InSecondsF();
const double minimum_acceptable_cadence =
perfect_cadence / (1.0 + drift_ratio);
const double maximum_acceptable_cadence =
perfect_cadence / (1.0 - drift_ratio);
// We've arbitrarily chosen the maximum allowable cadence length as 5. It's
// proven sufficient to support most standard frame and render rates, while
// being small enough that small frame and render timing errors don't render
// it useless.
const int kMaxCadenceSize = 5;
double best_error = 0;
int best_n = 0;
int best_k = 0;
for (int n = 1; n <= kMaxCadenceSize; ++n) {
// A cadence pattern only exists if there exists an integer K such that K/N
// is between |minimum_acceptable_cadence| and |maximum_acceptable_cadence|.
// The best pattern is the one with the smallest error over time relative to
// the |perfect_cadence|.
if (std::floor(minimum_acceptable_cadence * n) <
std::floor(maximum_acceptable_cadence * n)) {
const int k = round(perfect_cadence * n);
const double error = std::fabs(1.0 - perfect_cadence * n / k);
// Prefer the shorter cadence pattern unless a longer one "significantly"
// reduces the error.
if (!best_n || error < best_error * 0.99) {
best_error = error;
best_k = k;
best_n = n;
}
}
}
if (!best_n) return Cadence();
// If we've found a solution.
Cadence best_result = ConstructCadence(best_k, best_n);
*time_until_max_drift = max_acceptable_drift / best_error;
return best_result;
}
std::string VideoCadenceEstimator::CadenceToString(
const Cadence& cadence) const {
if (cadence.empty()) return std::string("[]");
std::ostringstream os;
os << "[";
std::copy(cadence.begin(), cadence.end() - 1,
std::ostream_iterator<int>(os, ":"));
os << cadence.back() << "]";
return os.str();
}
} // namespace media
} // namespace cobalt