Source code

Revision control

Copy as Markdown

Other Tools

/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "DriftController.h"
#include <atomic>
#include <cmath>
#include <mutex>
#include "mozilla/CheckedInt.h"
#include "mozilla/Logging.h"
namespace mozilla {
LazyLogModule gDriftControllerGraphsLog("DriftControllerGraphs");
extern LazyLogModule gMediaTrackGraphLog;
#define LOG_CONTROLLER(level, controller, format, ...) \
MOZ_LOG(gMediaTrackGraphLog, level, \
("DriftController %p: (plot-id %u) " format, controller, \
(controller)->mPlotId, ##__VA_ARGS__))
#define LOG_PLOT_NAMES() \
MOZ_LOG( \
gDriftControllerGraphsLog, LogLevel::Verbose, \
("id,t,buffering,avgbuffered,desired,buffersize,inlatency,outlatency," \
"inframesavg,outframesavg,inrate,outrate,steadystaterate," \
"nearthreshold,corrected,hysteresiscorrected,configured"))
#define LOG_PLOT_VALUES(id, t, buffering, avgbuffered, desired, buffersize, \
inlatency, outlatency, inframesavg, outframesavg, \
inrate, outrate, steadystaterate, nearthreshold, \
corrected, hysteresiscorrected, configured) \
MOZ_LOG(gDriftControllerGraphsLog, LogLevel::Verbose, \
("DriftController %u,%.3f,%u,%.5f,%" PRId64 ",%u,%" PRId64 "," \
"%" PRId64 ",%.5f,%.5f,%u,%u," \
"%.5f,%" PRId64 ",%.5f,%.5f," \
"%ld", \
id, t, buffering, avgbuffered, desired, buffersize, inlatency, \
outlatency, inframesavg, outframesavg, inrate, outrate, \
steadystaterate, nearthreshold, corrected, hysteresiscorrected, \
configured))
static uint8_t GenerateId() {
static std::atomic<uint8_t> id{0};
return ++id;
}
DriftController::DriftController(uint32_t aSourceRate, uint32_t aTargetRate,
media::TimeUnit aDesiredBuffering)
: mPlotId(GenerateId()),
mSourceRate(aSourceRate),
mTargetRate(aTargetRate),
mDesiredBuffering(aDesiredBuffering),
mCorrectedSourceRate(static_cast<float>(aSourceRate)),
mMeasuredSourceLatency(5),
mMeasuredTargetLatency(5) {
LOG_CONTROLLER(
LogLevel::Info, this,
"Created. Resampling %uHz->%uHz. Initial desired buffering: %.2fms.",
mSourceRate, mTargetRate, mDesiredBuffering.ToSeconds() * 1000.0);
static std::once_flag sOnceFlag;
std::call_once(sOnceFlag, [] { LOG_PLOT_NAMES(); });
}
void DriftController::SetDesiredBuffering(media::TimeUnit aDesiredBuffering) {
LOG_CONTROLLER(LogLevel::Debug, this, "SetDesiredBuffering %.2fms->%.2fms",
mDesiredBuffering.ToSeconds() * 1000.0,
aDesiredBuffering.ToSeconds() * 1000.0);
mLastDesiredBufferingChangeTime = mTotalTargetClock;
mDesiredBuffering = aDesiredBuffering.ToBase(mSourceRate);
}
void DriftController::ResetAfterUnderrun() {
mIsHandlingUnderrun = true;
// Trigger a recalculation on the next clock update.
mTargetClock = mAdjustmentInterval;
}
uint32_t DriftController::GetCorrectedSourceRate() const {
return std::lround(mCorrectedSourceRate);
}
int64_t DriftController::NearThreshold() const {
// mDesiredBuffering is divided by this to calculate a maximum error that
// would be considered "near" desired buffering. A denominator of 5
// corresponds to an error of +/- 20% of the desired buffering.
static constexpr uint32_t kNearDenominator = 5; // +/- 20%
// +/- 10ms band maximum half-width.
const media::TimeUnit nearCap = media::TimeUnit::FromSeconds(0.01);
// For the minimum desired buffering of 10ms we have a "near" error band
// of +/- 2ms (20%). This goes up to +/- 10ms (clamped) at most for when the
// desired buffering is 50 ms or higher. AudioDriftCorrection uses this
// threshold when deciding whether to reduce buffering.
return std::min(nearCap, mDesiredBuffering / kNearDenominator)
.ToTicksAtRate(mSourceRate);
}
void DriftController::UpdateClock(media::TimeUnit aSourceDuration,
media::TimeUnit aTargetDuration,
uint32_t aBufferedFrames,
uint32_t aBufferSize) {
MOZ_ASSERT(!aTargetDuration.IsZero());
mTargetClock += aTargetDuration;
mTotalTargetClock += aTargetDuration;
mMeasuredTargetLatency.insert(aTargetDuration);
if (aSourceDuration.IsZero()) {
// Only update after having received input, so that controller input,
// packet sizes and buffering measurements, are more stable when the input
// stream's callback interval is much larger than that of the output
// stream. The buffer level is therefore sampled at high points (rather
// than being an average of all points), which is consistent with the
// desired level of pre-buffering set on the DynamicResampler only after
// an input packet has recently arrived. There is some symmetry with
// output durations, which are similarly never zero: the buffer level is
// sampled at the lesser of input and output callback rates.
return;
}
media::TimeUnit targetDuration =
mTotalTargetClock - mTargetClockAfterLastSourcePacket;
mTargetClockAfterLastSourcePacket = mTotalTargetClock;
mMeasuredSourceLatency.insert(aSourceDuration);
double sourceDurationSecs = aSourceDuration.ToSeconds();
double targetDurationSecs = targetDuration.ToSeconds();
if (mOutputDurationAvg == 0.0) {
// Initialize the packet duration moving averages with equal values for an
// initial estimate of zero clock drift. When the input packets are much
// larger than output packets, targetDurationSecs may initially be much
// smaller. Use the maximum for a better estimate of the average output
// duration per input packet (or average input duration per output packet
// if input packets are smaller than output packets).
mInputDurationAvg = mOutputDurationAvg =
std::max(sourceDurationSecs, targetDurationSecs);
}
// UpdateAverageWithMeasurement() implements an exponential moving average
// with a weight small enough so that the influence of short term variations
// is small, but not so small that response time is delayed more than
// necessary.
//
// For the packet duration averages, a constant weight means that the moving
// averages behave similarly to sums of durations, and so can be used in a
// ratio for the drift estimate. Input arriving shortly before or after
// an UpdateClock() call, in response to an output request, is weighted
// similarly.
//
// For 10 ms packet durations, a weight of 0.01 corresponds to a time
// constant of about 1 second (the time over which the effect of old data
// points attenuates with a factor of exp(-1)).
auto UpdateAverageWithMeasurement = [](double* aAvg, double aData) {
constexpr double kMovingAvgWeight = 0.01;
*aAvg += kMovingAvgWeight * (aData - *aAvg);
};
UpdateAverageWithMeasurement(&mInputDurationAvg, sourceDurationSecs);
UpdateAverageWithMeasurement(&mOutputDurationAvg, targetDurationSecs);
double driftEstimate = mInputDurationAvg / mOutputDurationAvg;
// The driftEstimate is susceptible to changes in the input packet timing or
// duration, so use exponential smoothing to reduce the effect of short term
// variations. Apply a cascade of two exponential smoothing filters, which
// is a second order low pass filter, which attenuates high frequency
// components better than a single first order filter with the same total
// time constant. The attenuations of multiple filters are multiplicative
// while the time constants are only additive.
UpdateAverageWithMeasurement(&mStage1Drift, driftEstimate);
UpdateAverageWithMeasurement(&mDriftEstimate, mStage1Drift);
// Adjust the average buffer level estimates for drift and for the
// correction that was applied with this output packet, so that it still
// provides an estimate of the average buffer level.
double adjustment = targetDurationSecs *
(mSourceRate * mDriftEstimate - GetCorrectedSourceRate());
mStage1Buffered += adjustment;
mAvgBufferedFramesEst += adjustment;
// Include the current buffer level as a data point in the average buffer
// level estimate.
UpdateAverageWithMeasurement(&mStage1Buffered, aBufferedFrames);
UpdateAverageWithMeasurement(&mAvgBufferedFramesEst, mStage1Buffered);
if (mIsHandlingUnderrun) {
mIsHandlingUnderrun = false;
// Underrun handling invalidates the average buffer level estimate
// because silent input frames are inserted. Reset the estimate.
// This reset also performs the initial estimate when no previous
// input packets have been received.
mAvgBufferedFramesEst =
static_cast<double>(mDesiredBuffering.ToTicksAtRate(mSourceRate));
mStage1Buffered = mAvgBufferedFramesEst;
}
uint32_t desiredBufferedFrames = mDesiredBuffering.ToTicksAtRate(mSourceRate);
int32_t error =
(CheckedInt32(aBufferedFrames) - desiredBufferedFrames).value();
if (std::abs(error) > NearThreshold()) {
// The error is outside a threshold boundary.
mDurationNearDesired = media::TimeUnit::Zero();
} else {
// The error is within the "near" threshold boundaries.
mDurationNearDesired += mTargetClock;
};
if (mTargetClock >= mAdjustmentInterval) {
// The adjustment interval has passed. Recalculate.
CalculateCorrection(aBufferedFrames, aBufferSize);
}
}
void DriftController::CalculateCorrection(uint32_t aBufferedFrames,
uint32_t aBufferSize) {
// Maximum 0.1% change per update.
const float cap = static_cast<float>(mSourceRate) / 1000.0f;
// Resampler source rate that is expected to maintain a constant average
// buffering level.
float steadyStateRate =
static_cast<float>(mDriftEstimate) * static_cast<float>(mSourceRate);
// Use nominal (not corrected) source rate when interpreting desired
// buffering so that the set point is independent of the control value.
uint32_t desiredBufferedFrames = mDesiredBuffering.ToTicksAtRate(mSourceRate);
float avgError = static_cast<float>(mAvgBufferedFramesEst) -
static_cast<float>(desiredBufferedFrames);
// rateError is positive when pushing the buffering towards the desired level.
float rateError =
(mCorrectedSourceRate - steadyStateRate) * std::copysign(1.f, avgError);
float absAvgError = std::abs(avgError);
// Longest period over which convergence to the desired buffering level is
// accepted.
constexpr float slowConvergenceSecs = 30;
// Convergence period to use when resetting the sample rate.
constexpr float resetConvergenceSecs = 15;
float correctedRate = steadyStateRate + avgError / resetConvergenceSecs;
// Allow slower or faster convergence to the desired buffering level, within
// acceptable limits, if it means that the same resampling rate can be used,
// so that the resampler filters do not need to be recalculated.
float hysteresisCorrectedRate = mCorrectedSourceRate;
// Allow up to 1 frame/sec resampling rate difference beyond the slowest
// convergence boundary, which provides hysteresis to avoid frequent
// oscillations in the rate as avgError changes sign when around the
// desired buffering level.
constexpr float slowHysteresis = 1.f;
if (/* current rate is slower than will converge in acceptable time, or */
(rateError + slowHysteresis) * slowConvergenceSecs <= absAvgError ||
/* current rate is so fast as to overshoot. */
rateError * mAdjustmentInterval.ToSeconds() >= absAvgError) {
hysteresisCorrectedRate = correctedRate;
float cappedRate = std::clamp(correctedRate, mCorrectedSourceRate - cap,
mCorrectedSourceRate + cap);
if (std::lround(mCorrectedSourceRate) != std::lround(cappedRate)) {
LOG_CONTROLLER(
LogLevel::Verbose, this,
"Updating Correction: Nominal: %uHz->%uHz, Corrected: "
"%.2fHz->%uHz (diff %.2fHz), error: %.2fms (nearThreshold: "
"%.2fms), buffering: %.2fms, desired buffering: %.2fms",
mSourceRate, mTargetRate, cappedRate, mTargetRate,
cappedRate - mCorrectedSourceRate,
media::TimeUnit(CheckedInt64(aBufferedFrames) - desiredBufferedFrames,
mSourceRate)
.ToSeconds() *
1000.0,
media::TimeUnit(NearThreshold(), mSourceRate).ToSeconds() * 1000.0,
media::TimeUnit(aBufferedFrames, mSourceRate).ToSeconds() * 1000.0,
mDesiredBuffering.ToSeconds() * 1000.0);
++mNumCorrectionChanges;
}
mCorrectedSourceRate = std::max(1.f, cappedRate);
}
LOG_PLOT_VALUES(
mPlotId, mTotalTargetClock.ToSeconds(), aBufferedFrames,
mAvgBufferedFramesEst, mDesiredBuffering.ToTicksAtRate(mSourceRate),
aBufferSize, mMeasuredSourceLatency.mean().ToTicksAtRate(mSourceRate),
mMeasuredTargetLatency.mean().ToTicksAtRate(mTargetRate),
mInputDurationAvg * mSourceRate, mOutputDurationAvg * mTargetRate,
mSourceRate, mTargetRate, steadyStateRate, NearThreshold(), correctedRate,
hysteresisCorrectedRate, std::lround(mCorrectedSourceRate));
// Reset the counters to prepare for the next period.
mTargetClock = media::TimeUnit::Zero();
}
} // namespace mozilla
#undef LOG_PLOT_VALUES
#undef LOG_PLOT_NAMES
#undef LOG_CONTROLLER