diff options
Diffstat (limited to 'dom/media/driftcontrol/DriftController.cpp')
-rw-r--r-- | dom/media/driftcontrol/DriftController.cpp | 237 |
1 files changed, 237 insertions, 0 deletions
diff --git a/dom/media/driftcontrol/DriftController.cpp b/dom/media/driftcontrol/DriftController.cpp new file mode 100644 index 0000000000..b5603f72bb --- /dev/null +++ b/dom/media/driftcontrol/DriftController.cpp @@ -0,0 +1,237 @@ +/* -*- 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,desired,buffersize,inlatency,outlatency,inrate," \ + "outrate,hysteresisthreshold,corrected,hysteresiscorrected,configured," \ + "p,i,d,kpp,kii,kdd,control")) +#define LOG_PLOT_VALUES(id, t, buffering, desired, buffersize, inlatency, \ + outlatency, inrate, outrate, hysteresisthreshold, \ + corrected, hysteresiscorrected, configured, p, i, d, \ + kpp, kii, kdd, control) \ + MOZ_LOG( \ + gDriftControllerGraphsLog, LogLevel::Verbose, \ + ("DriftController %u,%.3f,%u,%" PRId64 ",%u,%" PRId64 ",%" PRId64 \ + ",%u,%u,%" PRId64 ",%.5f,%.5f,%ld,%d,%.5f,%.5f,%.5f,%.5f,%.5f,%.5f", \ + id, t, buffering, desired, buffersize, inlatency, outlatency, inrate, \ + outrate, hysteresisthreshold, corrected, hysteresiscorrected, \ + configured, p, i, d, kpp, kii, kdd, control)) + +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), + mCorrectedTargetRate(static_cast<float>(aTargetRate)), + 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() { + mIntegral = 0.0; + mPreviousError = 0.0; + // Trigger a recalculation on the next clock update. + mTargetClock = mAdjustmentInterval; +} + +uint32_t DriftController::GetCorrectedTargetRate() const { + return std::lround(mCorrectedTargetRate); +} + +void DriftController::UpdateClock(media::TimeUnit aSourceDuration, + media::TimeUnit aTargetDuration, + uint32_t aBufferedFrames, + uint32_t aBufferSize) { + mTargetClock += aTargetDuration; + mTotalTargetClock += aTargetDuration; + + mMeasuredTargetLatency.insert(aTargetDuration); + + if (aSourceDuration.IsZero()) { + // Only update the clock after having received input, so input buffering + // estimates are somewhat recent. This helps stabilize the controller + // input (buffering measurements) when the input stream's callback + // interval is much larger than that of the output stream. + return; + } + + mMeasuredSourceLatency.insert(aSourceDuration); + + if (mTargetClock >= mAdjustmentInterval) { + // The adjustment interval has passed. Recalculate. + CalculateCorrection(aBufferedFrames, aBufferSize); + } +} + +void DriftController::CalculateCorrection(uint32_t aBufferedFrames, + uint32_t aBufferSize) { + static constexpr float kProportionalGain = 0.07; + static constexpr float kIntegralGain = 0.006; + static constexpr float kDerivativeGain = 0.12; + + // Maximum 0.1% change per update. + const float cap = static_cast<float>(mTargetRate) / 1000.0f; + + // The integral term can make us grow far outside the cap. Impose a cap on + // it individually that is roughly equivalent to the final cap. + const float integralCap = cap / kIntegralGain; + + int32_t error = CheckedInt32(mDesiredBuffering.ToTicksAtRate(mSourceRate) - + aBufferedFrames) + .value(); + int32_t proportional = error; + // targetClockSec is the number of target clock seconds since last + // correction. + float targetClockSec = static_cast<float>(mTargetClock.ToSeconds()); + // delta-t is targetClockSec. + float integralStep = std::clamp(static_cast<float>(error) * targetClockSec, + -integralCap, integralCap); + mIntegral += integralStep; + float derivative = + static_cast<float>(error - mPreviousError) / targetClockSec; + float controlSignal = kProportionalGain * static_cast<float>(proportional) + + kIntegralGain * mIntegral + + kDerivativeGain * derivative; + float correctedRate = + std::clamp(static_cast<float>(mTargetRate) + controlSignal, + mCorrectedTargetRate - cap, mCorrectedTargetRate + cap); + + // mDesiredBuffering is divided by this to calculate the amount of + // hysteresis to apply. With a denominator of 5, an error within +/- 20% of + // the desired buffering will not make corrections to the target sample + // rate. + static constexpr uint32_t kHysteresisDenominator = 5; // +/- 20% + + // +/- 10ms hysteresis maximum. + const media::TimeUnit hysteresisCap = media::TimeUnit::FromSeconds(0.01); + + // For the minimum desired buffering of 10ms we have a hysteresis threshold + // of +/- 2ms (20%). This goes up to +/- 10ms (clamped) at most for when the + // desired buffering is 50 ms or higher. + const auto hysteresisThreshold = + std::min(hysteresisCap, mDesiredBuffering / kHysteresisDenominator) + .ToTicksAtRate(mSourceRate); + + float hysteresisCorrectedRate = [&] { + uint32_t abserror = std::abs(error); + if (abserror > hysteresisThreshold) { + // The error is outside a hysteresis threshold boundary. + mDurationWithinHysteresis = media::TimeUnit::Zero(); + mIntegralCenterForCap = Nothing(); + mLastHysteresisBoundaryCorrection = Some(error); + return correctedRate; + } + + // The error is within the hysteresis threshold boundaries. + mDurationWithinHysteresis += mTargetClock; + if (!mIntegralCenterForCap) { + mIntegralCenterForCap = Some(mIntegral); + } + + // Would prefer std::signbit, but.. + // https://github.com/microsoft/STL/issues/519. + if (mLastHysteresisBoundaryCorrection && + (*mLastHysteresisBoundaryCorrection < 0) != (error < 0) && + abserror > hysteresisThreshold * 3 / 10) { + // The error came from a boundary and just went 30% past the center line + // (of the distance between center and boundary). Correct now rather + // than when reaching the opposite boundary, so we have a chance of + // finding a stable rate. + mLastHysteresisBoundaryCorrection = Nothing(); + return correctedRate; + } + + return mCorrectedTargetRate; + }(); + + if (mDurationWithinHysteresis > mIntegralCapTimeLimit) { + // Impose a cap on the integral term to not let it grow unboundedly + // while we're within the hysteresis threshold boundaries. Since the + // integral is what finds the drift we center the cap around the integral's + // value when we entered the hysteresis threshold rarther than around 0. We + // impose the cap only after the error has been within the hysteresis + // threshold boundaries for some time, since it would otherwise increase the + // time it takes to reach stability. + mIntegral = std::clamp(mIntegral, *mIntegralCenterForCap - integralCap, + *mIntegralCenterForCap + integralCap); + } + + LOG_CONTROLLER( + LogLevel::Verbose, this, + "Recalculating Correction: Nominal: %uHz->%uHz, Corrected: " + "%uHz->%.2fHz (diff %.2fHz), error: %.2fms (hysteresisThreshold: " + "%.2fms), buffering: %.2fms, desired buffering: %.2fms", + mSourceRate, mTargetRate, mSourceRate, hysteresisCorrectedRate, + hysteresisCorrectedRate - mCorrectedTargetRate, + media::TimeUnit(error, mSourceRate).ToSeconds() * 1000.0, + media::TimeUnit(hysteresisThreshold, mSourceRate).ToSeconds() * 1000.0, + media::TimeUnit(aBufferedFrames, mSourceRate).ToSeconds() * 1000.0, + mDesiredBuffering.ToSeconds() * 1000.0); + LOG_PLOT_VALUES(mPlotId, mTotalTargetClock.ToSeconds(), aBufferedFrames, + mDesiredBuffering.ToTicksAtRate(mSourceRate), aBufferSize, + mMeasuredSourceLatency.mean().ToTicksAtRate(mSourceRate), + mMeasuredTargetLatency.mean().ToTicksAtRate(mTargetRate), + mSourceRate, mTargetRate, hysteresisThreshold, correctedRate, + hysteresisCorrectedRate, std::lround(hysteresisCorrectedRate), + proportional, mIntegral, derivative, + kProportionalGain * proportional, kIntegralGain * mIntegral, + kDerivativeGain * derivative, controlSignal); + + if (std::lround(mCorrectedTargetRate) != + std::lround(hysteresisCorrectedRate)) { + ++mNumCorrectionChanges; + } + + mPreviousError = error; + mCorrectedTargetRate = hysteresisCorrectedRate; + + // 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 |