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:
@@ -107,12 +107,40 @@ export class AudioPlayer {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Signal to the decoder that the C# streaming loop has finished sending bytes.
|
||||
* This sets streamComplete=true and flushes any remaining decoded tail segments.
|
||||
* Must be called after the ReadAsync loop exits, regardless of whether
|
||||
* Content-Length was known — without it the tail-decode path is dead when
|
||||
* Content-Length is absent.
|
||||
*/
|
||||
async markStreamComplete(): Promise<StreamingResult> {
|
||||
try {
|
||||
const results = await this.streamDecoder.markStreamComplete();
|
||||
if (results.length > 0) {
|
||||
for (const result of results) {
|
||||
this.scheduler.addBuffer(result.buffer);
|
||||
}
|
||||
if (this.streamingStarted && this.isPlaying) {
|
||||
this.scheduler.scheduleNewBuffers();
|
||||
}
|
||||
}
|
||||
this.streamingCompleted = true;
|
||||
console.log('Stream marked complete by C# signal');
|
||||
return { success: true, bufferCount: this.scheduler.getBufferCount() };
|
||||
} catch (error) {
|
||||
return { success: false, error: (error as Error).message };
|
||||
}
|
||||
}
|
||||
|
||||
async processStreamingChunk(chunk: Uint8Array): Promise<StreamingResult> {
|
||||
try {
|
||||
const result = await this.streamDecoder.processChunk(chunk);
|
||||
const results = await this.streamDecoder.processChunk(chunk);
|
||||
|
||||
if (result) {
|
||||
this.scheduler.addBuffer(result.buffer);
|
||||
if (results.length > 0) {
|
||||
for (const result of results) {
|
||||
this.scheduler.addBuffer(result.buffer);
|
||||
}
|
||||
|
||||
// Update duration estimate
|
||||
const estimatedDuration = this.streamDecoder.getEstimatedDuration();
|
||||
|
||||
Reference in New Issue
Block a user