feature: OpusFormatDecoder — Ogg-page-aligned segmenting, sidecar parser, accurate index-based seek (Phase 18.4)
This commit is contained in:
@@ -14,6 +14,8 @@ import { IFormatDecoder } from './IFormatDecoder.js';
|
||||
import { WavFormatDecoder } from './WavFormatDecoder.js';
|
||||
import { Mp3FormatDecoder } from './Mp3FormatDecoder.js';
|
||||
import { FlacFormatDecoder } from './FlacFormatDecoder.js';
|
||||
import { OpusFormatDecoder } from './OpusFormatDecoder.js';
|
||||
import { OpusSeekData, parseSidecar } from './OpusSidecar.js';
|
||||
|
||||
export interface AudioResult {
|
||||
success: boolean;
|
||||
@@ -62,6 +64,11 @@ export class AudioPlayer {
|
||||
private onEndCallback: EndCallback | null = null;
|
||||
private progressInterval: number | null = null;
|
||||
|
||||
// Pending Opus sidecar (setup header + seek index), parsed from the one-time sidecar fetch and
|
||||
// applied to the OpusFormatDecoder when the next Opus stream initializes. Wave 18.5 sets this
|
||||
// (via setOpusSidecar) before initializeStreaming; this class never fetches it.
|
||||
private pendingOpusSidecar: OpusSeekData | null = null;
|
||||
|
||||
constructor() {
|
||||
this.contextManager = new AudioContextManager();
|
||||
this.streamDecoder = new StreamDecoder(this.contextManager);
|
||||
@@ -103,7 +110,7 @@ export class AudioPlayer {
|
||||
|
||||
// Initialize new stream with the format decoder selected from Content-Type.
|
||||
this.isStreamingMode = true;
|
||||
const formatDecoder = AudioPlayer.createFormatDecoder(contentType);
|
||||
const formatDecoder = this.createFormatDecoder(contentType);
|
||||
this.streamDecoder.initialize(totalStreamLength, formatDecoder);
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
@@ -112,15 +119,40 @@ export class AudioPlayer {
|
||||
}
|
||||
|
||||
/**
|
||||
* Select a format decoder from the response Content-Type.
|
||||
* Inject the Opus sidecar (setup header + seek index) for the next Opus stream. Wave 18.5 calls
|
||||
* this with the raw sidecar bytes (from its one-time HTTP fetch) BEFORE initializeStreaming; the
|
||||
* parsed result is applied to the OpusFormatDecoder when the stream initializes. This is the
|
||||
* injection seam — the player owns no transport, only the parse + hand-off.
|
||||
*
|
||||
* @returns success:false with an error if the bytes are not a valid sidecar blob.
|
||||
*/
|
||||
private static createFormatDecoder(contentType: string): IFormatDecoder {
|
||||
setOpusSidecar(sidecarBytes: Uint8Array): AudioResult {
|
||||
const parsed = parseSidecar(sidecarBytes);
|
||||
if (!parsed) {
|
||||
return { success: false, error: 'Invalid Opus sidecar blob' };
|
||||
}
|
||||
this.pendingOpusSidecar = parsed;
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Select a format decoder from the response Content-Type. For Opus, applies the pending sidecar
|
||||
* (if 18.5 has set one) so the decoder has its setup bytes + seek index before stream init.
|
||||
*/
|
||||
private createFormatDecoder(contentType: string): IFormatDecoder {
|
||||
if (contentType.includes('audio/mpeg') || contentType.includes('audio/mp3')) {
|
||||
return new Mp3FormatDecoder();
|
||||
}
|
||||
if (contentType.includes('audio/flac') || contentType.includes('audio/x-flac')) {
|
||||
return new FlacFormatDecoder();
|
||||
}
|
||||
if (contentType.includes('audio/ogg') || contentType.includes('audio/opus')) {
|
||||
const decoder = new OpusFormatDecoder();
|
||||
if (this.pendingOpusSidecar) {
|
||||
decoder.setSidecar(this.pendingOpusSidecar);
|
||||
}
|
||||
return decoder;
|
||||
}
|
||||
return new WavFormatDecoder(); // default (audio/wav, unknown)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user