Phase 21.2: back-pressure to bound the unplayed decoded region
Shared scheduler fill signal (forward water-marks + hard byte cap) pauses the C# read loop above high-water and, for Opus, stops the demux/decode feed so WebCodecs queues stay near-empty. Routes through the existing cancellation discipline; releases the latch on clear/seek.
This commit is contained in:
@@ -30,6 +30,10 @@ export interface StreamingResult extends AudioResult {
|
||||
headerParsed?: boolean;
|
||||
bufferCount?: number;
|
||||
duration?: number;
|
||||
// Phase 21.2a back-pressure signal piggybacked on the chunk result the C# read loop already
|
||||
// awaits — true means the scheduler's forward fill is over the high-water mark and the loop
|
||||
// should stop calling ReadAsync until it drains (no extra interop hop in the common case).
|
||||
productionPaused?: boolean;
|
||||
}
|
||||
|
||||
export interface AudioState {
|
||||
@@ -133,7 +137,14 @@ export class AudioPlayer {
|
||||
// selects Opus when the sidecar parsed, so the null branch is defensive.
|
||||
if (this.isOpusContentType(contentType) && this.pendingOpusSidecar) {
|
||||
this.activeOpusSidecar = this.pendingOpusSidecar;
|
||||
this.opusDecoder = new OpusStreamDecoder(this.contextManager, this.pendingOpusSidecar);
|
||||
// Pass the shared back-pressure signal (21.2b): the Opus decoder stops demuxing/
|
||||
// decoding new packets while the scheduler is full, so the WebCodecs decode queue
|
||||
// and decodedQueue do not balloon behind a throttled socket (OQ7). Same signal the
|
||||
// C# read loop honors — one policy, two thin hooks.
|
||||
this.opusDecoder = new OpusStreamDecoder(
|
||||
this.contextManager,
|
||||
this.pendingOpusSidecar,
|
||||
() => this.scheduler.isProductionPaused());
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
@@ -263,7 +274,8 @@ export class AudioPlayer {
|
||||
canStartStreaming: canStart,
|
||||
headerParsed,
|
||||
bufferCount: this.scheduler.getBufferCount(),
|
||||
duration: this.duration
|
||||
duration: this.duration,
|
||||
productionPaused: this.scheduler.isProductionPaused()
|
||||
};
|
||||
} catch (error) {
|
||||
return { success: false, error: (error as Error).message };
|
||||
@@ -307,7 +319,8 @@ export class AudioPlayer {
|
||||
canStartStreaming: canStart,
|
||||
headerParsed: this.streamDecoder.headerParsed,
|
||||
bufferCount: this.scheduler.getBufferCount(),
|
||||
duration: this.duration
|
||||
duration: this.duration,
|
||||
productionPaused: this.scheduler.isProductionPaused()
|
||||
};
|
||||
} catch (error) {
|
||||
return { success: false, error: (error as Error).message };
|
||||
@@ -508,6 +521,17 @@ export class AudioPlayer {
|
||||
return this.scheduler.getTotalDuration() + this.scheduler.getPlaybackOffset();
|
||||
}
|
||||
|
||||
/**
|
||||
* The shared back-pressure signal (Phase 21.2a), polled by the C# read loop WHILE it is
|
||||
* already throttled to learn when the forward fill has drained below the low-water mark and it
|
||||
* may resume reading. The steady-state (unthrottled) loop never calls this — it reads the
|
||||
* piggybacked productionPaused flag off each chunk result instead, so there is no extra
|
||||
* interop hop until back-pressure actually engages.
|
||||
*/
|
||||
isProductionPaused(): boolean {
|
||||
return this.scheduler.isProductionPaused();
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate byte offset for a time position (for C# layer)
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user