Skip to content

Commit 2fe3da7

Browse files
committed
Improve adaptive buffer time algorithm
This one uses time based EMA instead of the previous event based sliding window, improving buffer stability and reducing stutter in unstable network conditions.
1 parent 589cd52 commit 2fe3da7

File tree

1 file changed

+119
-49
lines changed

1 file changed

+119
-49
lines changed

src/buffer.js

Lines changed: 119 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ function nullBuffer(execute) {
3535
execute("o", text);
3636
},
3737

38-
stop() {},
38+
stop() { },
3939
};
4040
}
4141

@@ -123,71 +123,141 @@ function sleep(t) {
123123
});
124124
}
125125

126-
function adaptiveBufferTimeProvider({ logger }, { minTime = 25, maxLevel = 100, interval = 50, windowSize = 20, smoothingFactor = 0.2, minImprovementDuration = 1000 }) {
127-
let bufferLevel = 0;
128-
let bufferTime = calcBufferTime(bufferLevel);
129-
let latencies = [];
130-
let maxJitter = 0;
131-
let jitterRange = 0;
132-
let improvementTs = null;
133-
134-
function calcBufferTime(level) {
135-
if (level === 0) {
136-
return minTime;
126+
function adaptiveBufferTimeProvider(
127+
{ logger } = {},
128+
{
129+
minBufferTime = 50,
130+
bufferLevelStep = 100,
131+
maxBufferLevel = 50,
132+
transitionDuration = 500,
133+
peakHalfLifeUp = 100,
134+
peakHalfLifeDown = 10000,
135+
floorHalfLifeUp = 5000,
136+
floorHalfLifeDown = 100,
137+
idealHalfLifeUp = 1000,
138+
idealHalfLifeDown = 5000,
139+
safetyMultiplier = 1.2,
140+
minImprovementDuration = 3000,
141+
} = {}
142+
) {
143+
function levelToMs(level) {
144+
return level === 0 ? minBufferTime : bufferLevelStep * level;
145+
}
146+
147+
let bufferLevel = 1;
148+
let bufferTime = levelToMs(bufferLevel);
149+
let lastUpdateTime = performance.now();
150+
let smoothedPeakLatency = null;
151+
let smoothedFloorLatency = null;
152+
let smoothedIdealBufferTime = null;
153+
let stableSince = null;
154+
let targetBufferTime = null;
155+
let transitionRate = null;
156+
157+
return function(latency) {
158+
const now = performance.now();
159+
const dt = Math.max(0, now - lastUpdateTime);
160+
lastUpdateTime = now;
161+
162+
// adjust EMA-smoothed peak latency from current latency
163+
164+
if (smoothedPeakLatency === null) {
165+
smoothedPeakLatency = latency;
166+
} else if (latency > smoothedPeakLatency) {
167+
const alphaUp = 1 - Math.pow(2, -dt / peakHalfLifeUp);
168+
smoothedPeakLatency += alphaUp * (latency - smoothedPeakLatency);
137169
} else {
138-
return interval * level;
170+
const alphaDown = 1 - Math.pow(2, -dt / peakHalfLifeDown);
171+
smoothedPeakLatency += alphaDown * (latency - smoothedPeakLatency);
172+
}
173+
174+
smoothedPeakLatency = Math.max(smoothedPeakLatency, 0);
175+
176+
// adjust EMA-smoothed floor latency from current latency
177+
178+
if (smoothedFloorLatency === null) {
179+
smoothedFloorLatency = latency;
180+
} else if (latency > smoothedFloorLatency) {
181+
const alphaUp = 1 - Math.pow(2, -dt / floorHalfLifeUp);
182+
smoothedFloorLatency += alphaUp * (latency - smoothedFloorLatency);
183+
} else {
184+
const alphaDown = 1 - Math.pow(2, -dt / floorHalfLifeDown);
185+
smoothedFloorLatency += alphaDown * (latency - smoothedFloorLatency);
186+
}
187+
188+
smoothedFloorLatency = Math.max(smoothedFloorLatency, 0);
189+
190+
// adjust EMA-smoothed ideal buffer time
191+
192+
const jitter = smoothedPeakLatency - smoothedFloorLatency;
193+
const idealBufferTime = safetyMultiplier * (smoothedPeakLatency + jitter);
194+
195+
if (smoothedIdealBufferTime === null) {
196+
smoothedIdealBufferTime = idealBufferTime;
197+
} else if (idealBufferTime > smoothedIdealBufferTime) {
198+
const alphaUp = 1 - Math.pow(2, -dt / idealHalfLifeUp);
199+
smoothedIdealBufferTime += + alphaUp * (idealBufferTime - smoothedIdealBufferTime);
200+
} else {
201+
const alphaDown = 1 - Math.pow(2, -dt / idealHalfLifeDown);
202+
smoothedIdealBufferTime += + alphaDown * (idealBufferTime - smoothedIdealBufferTime);
139203
}
140-
}
141204

142-
return (latency) => {
143-
latencies.push(latency);
205+
// quantize smoothed ideal buffer time to discrete buffer level
144206

145-
if (latencies.length < windowSize) {
146-
return bufferTime;
147-
};
207+
let newBufferLevel;
148208

149-
latencies = latencies.slice(-windowSize);
150-
const currentMinJitter = min(latencies);
151-
const currentMaxJitter = max(latencies);
152-
const currentJitterRange = currentMaxJitter - currentMinJitter;
153-
maxJitter = currentMaxJitter * smoothingFactor + maxJitter * (1 - smoothingFactor);
154-
jitterRange = currentJitterRange * smoothingFactor + jitterRange * (1 - smoothingFactor);
155-
const minBufferTime = maxJitter + jitterRange;
209+
if (smoothedIdealBufferTime <= minBufferTime) {
210+
newBufferLevel = 0;
211+
} else {
212+
newBufferLevel = clamp(Math.ceil(smoothedIdealBufferTime / bufferLevelStep), 1, maxBufferLevel);
213+
}
156214

157215
if (latency > bufferTime) {
158-
logger.debug('buffer underrun', { latency, maxJitter, jitterRange, bufferTime });
216+
logger.debug('buffer underrun', { latency, bufferTime });
159217
}
160218

161-
if (bufferLevel < maxLevel && minBufferTime > bufferTime) {
162-
bufferTime = calcBufferTime((bufferLevel += 1));
163-
logger.debug(`jitter increased, raising bufferTime`, { latency, maxJitter, jitterRange, bufferTime });
164-
} else if (
165-
(bufferLevel > 1 && minBufferTime < calcBufferTime(bufferLevel - 2)) ||
166-
(bufferLevel == 1 && minBufferTime < calcBufferTime(bufferLevel - 1))
167-
) {
168-
if (improvementTs === null) {
169-
improvementTs = performance.now();
170-
} else if (performance.now() - improvementTs > minImprovementDuration) {
171-
improvementTs = performance.now();
172-
bufferTime = calcBufferTime((bufferLevel -= 1));
173-
logger.debug(`jitter decreased, lowering bufferTime`, { latency, maxJitter, jitterRange, bufferTime });
219+
// adjust buffer level and target buffer time for new buffer level
220+
221+
if (newBufferLevel > bufferLevel) {
222+
if (latency > bufferTime) { // <- underrun - raise quickly
223+
bufferLevel = Math.min(newBufferLevel, bufferLevel + 3);
224+
} else {
225+
bufferLevel += 1;
174226
}
175227

176-
return bufferTime;
228+
targetBufferTime = levelToMs(bufferLevel);
229+
transitionRate = (targetBufferTime - bufferTime) / transitionDuration;
230+
stableSince = null;
231+
logger.debug('raising buffer', { latency, bufferTime, targetBufferTime });
232+
} else if (newBufferLevel < bufferLevel) {
233+
if (stableSince == null) stableSince = now;
234+
235+
if (now - stableSince >= minImprovementDuration) {
236+
bufferLevel -= 1;
237+
targetBufferTime = levelToMs(bufferLevel);
238+
transitionRate = (targetBufferTime - bufferTime) / transitionDuration;
239+
stableSince = now;
240+
logger.debug('lowering buffer', { latency, bufferTime, targetBufferTime });
241+
}
242+
} else {
243+
stableSince = null;
177244
}
178245

179-
improvementTs = null;
246+
// linear transition to target buffer time
247+
248+
if (targetBufferTime !== null) {
249+
bufferTime += transitionRate * dt;
250+
251+
if (transitionRate >= 0 && bufferTime > targetBufferTime || transitionRate < 0 && bufferTime < targetBufferTime) {
252+
bufferTime = targetBufferTime;
253+
targetBufferTime = null;
254+
}
255+
}
180256

181257
return bufferTime;
182258
};
183259
}
184260

185-
function min(numbers) {
186-
return numbers.reduce((prev, cur) => cur < prev ? cur : prev);
187-
}
188-
189-
function max(numbers) {
190-
return numbers.reduce((prev, cur) => cur > prev ? cur : prev);
191-
}
261+
function clamp(x, lo, hi) { return Math.min(hi, Math.max(lo, x)); }
192262

193263
export default getBuffer;

0 commit comments

Comments
 (0)