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:
@@ -159,6 +159,25 @@ public class AudioInteropService : IAsyncDisposable
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Phase 21.2a back-pressure poll: ask whether the scheduler is still over its forward
|
||||
/// high/low-water band. The read loop calls this only WHILE already throttled, to learn when it
|
||||
/// may resume reading — the steady-state loop reads the piggybacked <c>ProductionPaused</c> flag
|
||||
/// off each chunk result instead. Defaults to false on any interop failure so a torn-down player
|
||||
/// never wedges a loop that is exiting anyway.
|
||||
/// </summary>
|
||||
public async Task<bool> IsProductionPaused(string playerId)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await _jsRuntime.InvokeAsync<bool>("DeepDrftAudio.isProductionPaused", playerId);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<AudioOperationResult> ReinitializeFromOffset(string playerId, long totalStreamLength, double seekPosition)
|
||||
{
|
||||
return await InvokeJsAsync<AudioOperationResult>("DeepDrftAudio.reinitializeFromOffset", playerId, totalStreamLength, seekPosition);
|
||||
@@ -419,6 +438,11 @@ public class StreamingResult : AudioOperationResult
|
||||
public bool HeaderParsed { get; set; }
|
||||
public int BufferCount { get; set; }
|
||||
public double? Duration { get; set; } // Duration in seconds calculated from WAV header
|
||||
|
||||
// Phase 21.2a back-pressure: true when the scheduler's forward decoded fill is over the
|
||||
// high-water mark and the C# read loop should stop calling ReadAsync until it drains. Read off
|
||||
// the chunk result the loop already awaits — no extra interop hop in the unthrottled steady state.
|
||||
public bool ProductionPaused { get; set; }
|
||||
}
|
||||
|
||||
public class AudioPlayerState
|
||||
|
||||
@@ -16,6 +16,14 @@ public class StreamingAudioPlayerService : AudioPlayerService, IStreamingPlayerS
|
||||
// Adaptive chunk sizing
|
||||
private const int MinBufferSize = 16 * 1024; // 16KB minimum
|
||||
private const int MaxBufferSize = 64 * 1024; // 64KB maximum
|
||||
|
||||
// Phase 21.2a back-pressure poll interval. While the scheduler is over its forward high-water
|
||||
// mark, the read loop stops calling ReadAsync and polls IsProductionPaused at this cadence
|
||||
// until the fill drains below low-water. 100 ms is well under the low-water lookahead (seconds),
|
||||
// so resume is prompt relative to the playhead — no starvation (AC3) — while keeping the poll
|
||||
// cheap. The poll honors the loop's cancellation token, so a track switch/seek during a pause
|
||||
// exits through the same drain discipline as a pause during ReadAsync (C6).
|
||||
private const int BackpressurePollMs = 100;
|
||||
private int _currentBufferSize = DefaultBufferSize;
|
||||
private int _consecutiveSlowReads = 0;
|
||||
|
||||
@@ -465,6 +473,26 @@ public class StreamingAudioPlayerService : AudioPlayerService, IStreamingPlayerS
|
||||
}
|
||||
|
||||
await ThrottledNotifyStateChanged();
|
||||
|
||||
// Phase 21.2a back-pressure (serves BOTH paths). The chunk we just processed
|
||||
// reported the scheduler's forward fill is over the high-water mark — stop
|
||||
// reading the socket so the unplayed decoded region stays bounded. Pausing
|
||||
// ReadAsync lets the kernel TCP window close (we are working WITH transport flow
|
||||
// control, not against it). Poll until the fill drains below low-water, then
|
||||
// resume the loop. For WAV this is the whole story (StreamDecoder decodes
|
||||
// synchronously into the scheduler); the Opus feed additionally self-throttles
|
||||
// its demux/decode off the SAME signal (21.2b), so its upstream queues stay
|
||||
// near-empty too. The poll awaits on cancellationToken, so a track switch/seek
|
||||
// mid-pause throws OCE and unwinds through the existing drain discipline (C6) —
|
||||
// no separate cancellation path, no stale read racing a reinit.
|
||||
if (chunkResult.ProductionPaused)
|
||||
{
|
||||
while (await _audioInterop.IsProductionPaused(PlayerId))
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
await Task.Delay(BackpressurePollMs, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
} while (currentBytes > 0);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user