fix auto quality recovery after a downswitch

The downswitch callback armed the upswitch probe, but `triggerDownswitch` reset the stall history right after the callback returned, which disarmed it again. Auto quality stayed on the sub stream until the next chunk boundary no matter how much the connection recovered. The governor now arms the probe itself after the reset.

The chunk boundary effect also ran on mount, so when coverage was already cached it immediately undid the low quality cold start the seed effect had just picked. It now only runs when the chunk actually changes.
This commit is contained in:
Josh Hawkins
2026-09-28 08:46:58 -05:00
parent 6791df7971
commit 3f7e2c4686
2 changed files with 17 additions and 5 deletions
@@ -340,8 +340,11 @@ export class AutoQualityGovernor {
private triggerDownswitch(reason: DownswitchReason): boolean {
const handled = this.requestDownswitch(reason);
if (handled) {
// the low stream starts with a clean record
// the low stream starts with a clean record, and the probe lets a
// recovered connection (or a wrong downswitch) return to full
// quality mid-chunk rather than at the next boundary
this.resetStallHistory();
this.armUpswitchProbe();
}
return handled;
}
@@ -484,9 +484,6 @@ export default function DynamicVideoPlayer({
}
setAutoLowQuality(true);
setAutoLowReason(reason === "codec" ? "codec" : "bandwidth");
// so a recovered connection (or a wrong downswitch) returns to
// full quality mid-chunk rather than at the next boundary
governor.armUpswitchProbe();
return true;
};
tryUpswitchRef.current = () => {
@@ -495,7 +492,7 @@ export default function DynamicVideoPlayer({
setAutoLowReason(undefined);
}
};
}, [resolvedQuality, subAvailable, governor]);
}, [resolvedQuality, subAvailable]);
// persisted across sessions so a device on a known-slow connection
// starts low instead of paying the first stall to find out
@@ -616,7 +613,19 @@ export default function DynamicVideoPlayer({
governor.sourceLoadStarted();
}, [source, isScrubbing, governor]);
const lastChunkRef = useRef(timeRange);
useEffect(() => {
// the seed effect picks the starting quality, so only a later chunk
// boundary reconsiders it
const lastChunk = lastChunkRef.current;
lastChunkRef.current = timeRange;
if (
lastChunk.after === timeRange.after &&
lastChunk.before === timeRange.before
) {
return;
}
// a chunk boundary is where full quality may be retried, and a
// natural point to persist what the governor has learned
setAutoLowQuality((prev) => prev && !governor.shouldRetryMain());