Fix streaming minors: isActive_ sentinel, WAV error message, TextDecoder hoist, MIME 400, fmt first-match, processedBytes comment

This commit is contained in:
Daniel Harvey
2026-05-17 18:55:05 -04:00
parent 9f32c70e0f
commit 4420975cd2
6 changed files with 43 additions and 13 deletions
@@ -64,7 +64,10 @@ export class PlaybackScheduler {
* Get current playback position in seconds (includes playbackOffset for seek-beyond-buffer)
*/
getCurrentPosition(): number {
if (this.playbackAnchorTime === 0) {
// Use isActive_ as the sentinel for "playback is running", not playbackAnchorTime == 0.
// AudioContext.currentTime can legitimately be 0 at context creation, so comparing
// against 0 would incorrectly treat an active stream started at t=0 as paused.
if (!this.isActive_) {
return this.playbackAnchorPosition + this.playbackOffset;
}
const elapsed = this.contextManager.currentTime - this.playbackAnchorTime;
@@ -143,8 +146,11 @@ export class PlaybackScheduler {
return; // No new buffers
}
if (this.nextScheduleTime === 0) {
this.nextScheduleTime = this.contextManager.currentTime + 0.01;
// Use isActive_ as the sentinel for "playback is running", not nextScheduleTime === 0.
// AudioContext.currentTime can legitimately be 0 at context creation, which would cause
// nextScheduleTime === 0 to incorrectly reset a value already set by playFromPosition.
if (!this.isActive_) {
return;
}
this.scheduleBuffersFrom(this.nextBufferIndex, 0);
@@ -51,6 +51,9 @@ export class StreamDecoder {
private contextManager: AudioContextManager;
private wavHeader: WavHeader | null = null;
private rawChunks: Uint8Array[] = [];
// totalRawBytes and processedBytes are JS number (IEEE 754 double), which can
// represent integers exactly up to 2^53 bytes (~8 PB). WAV files are bounded
// at 4 GB by the 32-bit RIFF size field, so overflow is not a practical concern.
private totalRawBytes: number = 0;
private processedBytes: number = 0;
private totalStreamLength: number = 0;
+8 -3
View File
@@ -22,11 +22,16 @@ class WavUtils {
// Need a DataView that spans the entire buffer for chunk searching
const view = new DataView(concatenated.buffer);
// Allocate TextDecoder once for the entire parse pass. Constructing it
// inside the chunk-walk loop would create a new instance per iteration,
// which is non-trivial and unnecessary — a single instance is reusable.
const decoder = new TextDecoder();
// Check RIFF header
const riff = new TextDecoder().decode(concatenated.slice(0, 4));
const riff = decoder.decode(concatenated.slice(0, 4));
if (riff !== 'RIFF') return null;
const wave = new TextDecoder().decode(concatenated.slice(8, 12));
const wave = decoder.decode(concatenated.slice(8, 12));
if (wave !== 'WAVE') return null;
// Variables to store parsed header info
@@ -43,7 +48,7 @@ class WavUtils {
// Find fmt and data chunks
let chunkOffset = 12;
while (chunkOffset < totalSize - 8) {
const chunkId = new TextDecoder().decode(concatenated.slice(chunkOffset, chunkOffset + 4));
const chunkId = decoder.decode(concatenated.slice(chunkOffset, chunkOffset + 4));
const chunkSize = view.getUint32(chunkOffset + 4, true);
if (chunkId === 'fmt ') {