21.2 review remediation: pause-spin, OQ7 comment, rename, C2 cross-check

Skip the back-pressure interop poll while paused (UC5). Document complete()
draining the stash in full by design. Rename scheduler isProductionPaused to
evaluateProductionPause (latch-advancing); window exposure name unchanged.
This commit is contained in:
daniel-c-harvey
2026-06-23 23:28:42 -04:00
parent 518479e7ae
commit 29e8747c69
5 changed files with 42 additions and 24 deletions
@@ -46,9 +46,10 @@ const DEFAULT_BACK_RETAIN_SECONDS = 10;
*
* Provisional time-based defaults (OQ1 — 21.4 tunes them):
* - HIGH (30 s): the most decoded lookahead we hold ahead of the playhead before throttling.
* Comfortably above the playback-start minimum (6 buffers ≈ a second or two), so C2 holds —
* first audio never waits on a throttle (the high-water is reached only well after playback
* is already running).
* Comfortably above the playback-start minimum (`AudioPlayer.minBuffersForPlayback = 6`
* buffers, each typically 0.06 1 s depending on format/chunk size; at most a few seconds
* even at the high end), so C2 holds — first audio never waits on a throttle (the high-water
* is reached only well after playback is already running).
* - LOW (15 s): resume producing here. Kept generous so the forward fill never drains to the
* ~500 ms scheduler lookahead under normal network jitter (AC3 — no starvation).
*
@@ -93,7 +94,7 @@ export class PlaybackScheduler {
private backRetainSeconds: number = DEFAULT_BACK_RETAIN_SECONDS;
// Forward back-pressure water-marks + the OQ3 hard byte ceiling (Phase 21.2). This is the
// single shared window policy (OQ6): both producers read isProductionPaused() and honor it
// single shared window policy (OQ6): both producers call evaluateProductionPause() and honor it
// in their own way — the C# read loop stops ReadAsync, the Opus feed stops demux/decode.
private forwardHighWaterSeconds: number = DEFAULT_FORWARD_HIGH_WATER_SECONDS;
private forwardLowWaterSeconds: number = DEFAULT_FORWARD_LOW_WATER_SECONDS;
@@ -103,6 +104,7 @@ export class PlaybackScheduler {
// stay paused until it drains below the low-water mark, so the two producers do not flap
// on/off around a single threshold (and a paused producer does not resume for one chunk only
// to re-pause immediately). False until first crossing; flips on the band edges.
// Mutated by evaluateProductionPause() — named to signal the state-advance on each call.
private productionPaused_: boolean = false;
// Callbacks
@@ -215,8 +217,13 @@ export class PlaybackScheduler {
* drained below the low-water mark AND the byte estimate is back under the ceiling. The
* byte-ceiling test has no separate low-water band — it is the hard guard rail, so it releases
* as soon as eviction brings the footprint back under the cap.
*
* Named `evaluateProductionPause` (not `isProductionPaused`) because each call may ADVANCE the
* hysteresis latch (`productionPaused_`), making it a state-advancing evaluation, not a pure
* read. `AudioPlayer.isProductionPaused()` is the pure-predicate wrapper exposed to callers
* outside the scheduler.
*/
isProductionPaused(): boolean {
evaluateProductionPause(): boolean {
const lookahead = this.getForwardLookaheadSeconds();
const overByteCeiling = this.maxDecodedBytes > 0 && this.getDecodedByteEstimate() > this.maxDecodedBytes;