Fix Critical: streaming race, dirty buffer, dropped tail, fragmented header
- SeekBeyondBuffer and LoadTrackStreaming assign _activeStreamingTask before awaiting; DrainActiveStreamingTaskAsync awaits previous task before new stream starts, closing the concurrent-seek race on the JS StreamDecoder - Always slice ArrayPool buffer to currentBytes before sending to JS interop; eliminates stale bytes from prior rentals reaching the audio decoder - getSampleAlignedChunkSize accepts streamComplete flag; bypasses minimum chunk guard on final tail so trailing bytes are decoded, not dropped - StreamDecoder accumulates headerSearchChunks until parseHeader succeeds, with 256 KB MAX_HEADER_SEARCH_BYTES bound; handles fragmented first chunks and extended WAV headers with LIST/INFO/JUNK chunks - markStreamComplete early-returns when streamComplete already set to prevent double-drain and incorrect streamingCompleted flag after partial failure - processedBytes advances only after successful decode; failed segments leave cursor in place rather than permanently skipping audio - AudioInteropService.MarkStreamCompleteAsync wires C# loop exit to JS decoder ensuring tail drain fires even when Content-Length header is absent
This commit is contained in:
@@ -173,17 +173,26 @@ class WavUtils {
|
||||
buffer[43] = (audioDataSize >> 24) & 0xFF;
|
||||
}
|
||||
|
||||
static getSampleAlignedChunkSize(header: WavHeader, maxChunkSize: number, availableDataSize: number): number {
|
||||
static getSampleAlignedChunkSize(header: WavHeader, maxChunkSize: number, availableDataSize: number, streamComplete: boolean = false): number {
|
||||
const frameSize = header.blockAlign;
|
||||
|
||||
// Much smaller minimum for streaming - just enough for Web Audio API
|
||||
|
||||
// Much smaller minimum for streaming - just enough for Web Audio API.
|
||||
// The minimum exists to avoid decoding partial-frame artifacts on
|
||||
// mid-stream chunks while the rest is still in flight. Once the stream
|
||||
// is fully received, we must drain whatever remains regardless of size,
|
||||
// otherwise the trailing tail (often <512 bytes) is silently lost.
|
||||
const minAudioBytes = Math.max(512, frameSize * 10); // At least 512 bytes or 10 frames
|
||||
|
||||
// If we don't have enough data, return 0 to wait for more
|
||||
if (availableDataSize < minAudioBytes) {
|
||||
|
||||
// Mid-stream guard: wait for more data if below minimum.
|
||||
if (!streamComplete && availableDataSize < minAudioBytes) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
// Even when complete we still need at least one full frame to decode.
|
||||
if (availableDataSize < frameSize) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Calculate frames for the available data
|
||||
const requestedSize = Math.min(maxChunkSize, availableDataSize);
|
||||
const frames = Math.floor(requestedSize / frameSize);
|
||||
|
||||
Reference in New Issue
Block a user