fix(audio): align AudioContext to 48kHz up front for Opus streams
Opus resolved its 48kHz context lazily on the first chunk, close()ing and rebuilding the live graph mid-decode. Move the recreate into initializeStreaming so it runs before any bytes flow; the lazy call early-returns. WAV path unchanged.
This commit is contained in:
@@ -16,7 +16,7 @@ import { WavFormatDecoder } from './WavFormatDecoder.js';
|
|||||||
import { Mp3FormatDecoder } from './Mp3FormatDecoder.js';
|
import { Mp3FormatDecoder } from './Mp3FormatDecoder.js';
|
||||||
import { FlacFormatDecoder } from './FlacFormatDecoder.js';
|
import { FlacFormatDecoder } from './FlacFormatDecoder.js';
|
||||||
import { OpusStreamDecoder } from './OpusStreamDecoder.js';
|
import { OpusStreamDecoder } from './OpusStreamDecoder.js';
|
||||||
import { OpusSeekData, parseSidecar, resolveOpusByteOffset, OpusSeekResolution } from './OpusSidecar.js';
|
import { OpusSeekData, parseSidecar, resolveOpusByteOffset, OpusSeekResolution, OPUS_SAMPLE_RATE } from './OpusSidecar.js';
|
||||||
|
|
||||||
export interface AudioResult {
|
export interface AudioResult {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
@@ -118,7 +118,7 @@ export class AudioPlayer {
|
|||||||
|
|
||||||
// ==================== Streaming ====================
|
// ==================== Streaming ====================
|
||||||
|
|
||||||
initializeStreaming(totalStreamLength: number, contentType: string): AudioResult {
|
async initializeStreaming(totalStreamLength: number, contentType: string): Promise<AudioResult> {
|
||||||
try {
|
try {
|
||||||
// Full cleanup before starting new stream
|
// Full cleanup before starting new stream
|
||||||
this.stopProgressTracking();
|
this.stopProgressTracking();
|
||||||
@@ -137,6 +137,19 @@ export class AudioPlayer {
|
|||||||
// selects Opus when the sidecar parsed, so the null branch is defensive.
|
// selects Opus when the sidecar parsed, so the null branch is defensive.
|
||||||
if (this.isOpusContentType(contentType) && this.pendingOpusSidecar) {
|
if (this.isOpusContentType(contentType) && this.pendingOpusSidecar) {
|
||||||
this.activeOpusSidecar = this.pendingOpusSidecar;
|
this.activeOpusSidecar = this.pendingOpusSidecar;
|
||||||
|
|
||||||
|
// Align the AudioContext to 48 kHz NOW, before any Opus bytes flow — the format is
|
||||||
|
// already resolved (C# resolves Opus + injects the sidecar before this call), so the
|
||||||
|
// target rate is known up front. Done here, the decoder's own lazy
|
||||||
|
// recreateWithSampleRate(48000) in ensureConfigured hits its sampleRate-equal early
|
||||||
|
// return and is a no-op; the live graph is never close()'d and rebuilt mid-decode (the
|
||||||
|
// teardown that double-decoded the stream and OOM'd the tab with HW accel off). The
|
||||||
|
// recreate seam itself stays — it is the WAV path's mechanism for non-44.1 sources and
|
||||||
|
// remains the defensive backstop here.
|
||||||
|
if (this.contextManager.sampleRate !== OPUS_SAMPLE_RATE) {
|
||||||
|
await this.contextManager.recreateWithSampleRate(OPUS_SAMPLE_RATE);
|
||||||
|
}
|
||||||
|
|
||||||
// Pass the shared back-pressure signal (21.2b): the Opus decoder stops demuxing/
|
// 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
|
// 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
|
// and decodedQueue do not balloon behind a throttled socket (OQ7). Same signal the
|
||||||
@@ -148,7 +161,9 @@ export class AudioPlayer {
|
|||||||
return { success: true };
|
return { success: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Non-Opus (or Opus-without-sidecar): the existing StreamDecoder path, unchanged.
|
// Non-Opus (or Opus-without-sidecar): the existing StreamDecoder path, unchanged. The
|
||||||
|
// context sample rate is untouched here, so the WAV/lossless path is byte-for-byte
|
||||||
|
// unaffected by the Opus up-front alignment above.
|
||||||
const formatDecoder = this.createFormatDecoder(contentType);
|
const formatDecoder = this.createFormatDecoder(contentType);
|
||||||
this.streamDecoder.initialize(totalStreamLength, formatDecoder);
|
this.streamDecoder.initialize(totalStreamLength, formatDecoder);
|
||||||
return { success: true };
|
return { success: true };
|
||||||
|
|||||||
@@ -123,7 +123,11 @@ export class OpusStreamDecoder implements IStreamingDecoder {
|
|||||||
// Copy the OpusHead into a standalone buffer — the sidecar subarray is a view we keep.
|
// Copy the OpusHead into a standalone buffer — the sidecar subarray is a view we keep.
|
||||||
this.opusHeadDescription = opusHead.slice();
|
this.opusHeadDescription = opusHead.slice();
|
||||||
|
|
||||||
// Opus decodes at 48 kHz; align the context so no resample is needed.
|
// Opus decodes at 48 kHz; align the context so no resample is needed. AudioPlayer.initializeStreaming
|
||||||
|
// already aligned it to 48 kHz up front (the format is resolved before any bytes flow), so in the
|
||||||
|
// common path this is an early-return no-op — the live graph is NOT close()'d and rebuilt mid-decode.
|
||||||
|
// Kept as the defensive backstop for any path that reaches a configured decoder on a non-48 kHz
|
||||||
|
// context (the same recreate seam the WAV path uses for non-44.1 sources).
|
||||||
if (this.contextManager.sampleRate !== OPUS_SAMPLE_RATE) {
|
if (this.contextManager.sampleRate !== OPUS_SAMPLE_RATE) {
|
||||||
await this.contextManager.recreateWithSampleRate(OPUS_SAMPLE_RATE);
|
await this.contextManager.recreateWithSampleRate(OPUS_SAMPLE_RATE);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ const DeepDrftAudio = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
initializeStreaming: (playerId: string, totalStreamLength: number, contentType: string): AudioResult => {
|
initializeStreaming: async (playerId: string, totalStreamLength: number, contentType: string): Promise<AudioResult> => {
|
||||||
const player = audioPlayers.get(playerId);
|
const player = audioPlayers.get(playerId);
|
||||||
if (!player) return { success: false, error: 'Player not found' };
|
if (!player) return { success: false, error: 'Player not found' };
|
||||||
return player.initializeStreaming(totalStreamLength, contentType);
|
return player.initializeStreaming(totalStreamLength, contentType);
|
||||||
|
|||||||
Reference in New Issue
Block a user