Files
v10/dist/default/network/bandwidth-estimator.js
T

96 lines
3.7 KiB
JavaScript

import { applyZeroFactor, calculateEwma } from "./ewma.js";
//#region src/network/bandwidth-estimator.ts
/**
* Dual EWMA Bandwidth Estimator
*
* Estimates available bandwidth using two EWMA calculations with different
* half-lives, taking the minimum of both. This approach (from Shaka Player):
*
* - **Fast EWMA** (2s half-life): Reacts quickly to bandwidth drops
* - **Slow EWMA** (5s half-life): Provides stability during fluctuations
* - **min(fast, slow)**: Adapts down quickly, up slowly
*
* This naturally provides asymmetric behavior needed for good QoE:
* avoiding stalls (quick downgrade) while preventing oscillation (slow upgrade).
*/
/**
* Default bandwidth estimator configuration.
*
* Values match Shaka Player defaults based on experimentation.
*/
const DEFAULT_BANDWIDTH_CONFIG = {
fastHalfLife: 2,
slowHalfLife: 5,
minTotalBytes: 128e3,
minBytes: 16e3,
minDuration: 5
};
/**
* Add a bandwidth sample from a segment download.
*
* Samples are filtered based on:
* - Minimum bytes (filters TTFB-dominated small segments)
* - Minimum duration (filters cached responses)
*
* Valid samples update both fast and slow EWMA estimates.
*
* @param state - Current estimator state
* @param durationMs - Download duration in milliseconds
* @param numBytes - Number of bytes downloaded
* @param config - Optional estimator configuration (uses defaults if not provided)
* @returns New estimator state with sample incorporated (or unchanged if filtered)
*
* @example
* let state = { fastEstimate: 0, fastTotalWeight: 0, ... };
* // Sample: 1MB in 1 second
* state = sampleBandwidth(state, 1000, 1_000_000);
*/
function sampleBandwidth(state, durationMs, numBytes, config = DEFAULT_BANDWIDTH_CONFIG) {
const updatedBytesSampled = state.bytesSampled + numBytes;
if (numBytes < config.minBytes) return {
...state,
bytesSampled: updatedBytesSampled
};
if (durationMs < config.minDuration) return {
...state,
bytesSampled: updatedBytesSampled
};
const bandwidth = 8e3 * numBytes / durationMs;
const weight = durationMs / 1e3;
return {
fastEstimate: calculateEwma(state.fastEstimate, bandwidth, weight, config.fastHalfLife),
fastTotalWeight: state.fastTotalWeight + weight,
slowEstimate: calculateEwma(state.slowEstimate, bandwidth, weight, config.slowHalfLife),
slowTotalWeight: state.slowTotalWeight + weight,
bytesSampled: updatedBytesSampled
};
}
/**
* Get the current bandwidth estimate.
*
* Returns the **minimum** of the fast and slow EWMA estimates.
* This provides the key asymmetric behavior:
* - When bandwidth drops, fast EWMA reacts first and dominates (quick adaptation)
* - When bandwidth rises, slow EWMA lags behind and dominates (slow adaptation)
*
* Uses default estimate until enough data has been sampled — and when no
* estimator state exists at all (`state === undefined`).
*
* @param state - Current estimator state, or `undefined` before any samples have been collected
* @param defaultEstimate - Fallback estimate before sufficient samples (bps)
* @param config - Optional estimator configuration (uses defaults if not provided)
* @returns Bandwidth estimate in bits per second
*
* @example
* const estimate = getBandwidthEstimate(state, 5_000_000); // 5 Mbps default
*/
function getBandwidthEstimate(state, defaultEstimate, config = DEFAULT_BANDWIDTH_CONFIG) {
if (!state || state.bytesSampled < config.minTotalBytes) return defaultEstimate;
const fastEstimate = applyZeroFactor(state.fastEstimate, state.fastTotalWeight, config.fastHalfLife);
const slowEstimate = applyZeroFactor(state.slowEstimate, state.slowTotalWeight, config.slowHalfLife);
return Math.min(fastEstimate, slowEstimate);
}
//#endregion
export { DEFAULT_BANDWIDTH_CONFIG, getBandwidthEstimate, sampleBandwidth };
//# sourceMappingURL=bandwidth-estimator.js.map