refactor(audio): extract IFormatDecoder/WavFormatDecoder and wire Content-Type to JS format selection
StreamDecoder is now format-agnostic; WavFormatDecoder delegates to WavUtils; contentType flows C# to JS.
This commit is contained in:
@@ -10,6 +10,8 @@
|
||||
import { AudioContextManager } from './AudioContextManager.js';
|
||||
import { StreamDecoder } from './StreamDecoder.js';
|
||||
import { PlaybackScheduler } from './PlaybackScheduler.js';
|
||||
import { IFormatDecoder } from './IFormatDecoder.js';
|
||||
import { WavFormatDecoder } from './WavFormatDecoder.js';
|
||||
|
||||
export interface AudioResult {
|
||||
success: boolean;
|
||||
@@ -89,7 +91,7 @@ export class AudioPlayer {
|
||||
|
||||
// ==================== Streaming ====================
|
||||
|
||||
initializeStreaming(totalStreamLength: number): AudioResult {
|
||||
initializeStreaming(totalStreamLength: number, contentType: string): AudioResult {
|
||||
try {
|
||||
// Full cleanup before starting new stream
|
||||
this.stopProgressTracking();
|
||||
@@ -97,15 +99,26 @@ export class AudioPlayer {
|
||||
this.streamDecoder.reset();
|
||||
this.resetState();
|
||||
|
||||
// Initialize new stream
|
||||
// Initialize new stream with the format decoder selected from Content-Type.
|
||||
this.isStreamingMode = true;
|
||||
this.streamDecoder.initialize(totalStreamLength);
|
||||
const formatDecoder = AudioPlayer.createFormatDecoder(contentType);
|
||||
this.streamDecoder.initialize(totalStreamLength, formatDecoder);
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
return { success: false, error: (error as Error).message };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Select a format decoder from the response Content-Type. MP3 and FLAC decoders
|
||||
* arrive in Wave 2; until then every format falls back to WAV. When Wave 2 lands,
|
||||
* add the MP3/FLAC branches here, e.g.:
|
||||
* if (contentType.includes('audio/mpeg')) return new Mp3FormatDecoder();
|
||||
*/
|
||||
private static createFormatDecoder(_contentType: string): IFormatDecoder {
|
||||
return new WavFormatDecoder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Signal to the decoder that the C# streaming loop has finished sending bytes.
|
||||
* This sets streamComplete=true and flushes any remaining decoded tail segments.
|
||||
@@ -321,22 +334,16 @@ export class AudioPlayer {
|
||||
*/
|
||||
private seekBeyondBuffer(position: number): AudioResult {
|
||||
try {
|
||||
const audioOffset = this.streamDecoder.calculateByteOffset(position);
|
||||
// 0 is a valid offset (seek to start of audio data). Only a negative result
|
||||
// indicates calculation failure — typically a missing/unparsed WAV header.
|
||||
if (audioOffset < 0) {
|
||||
// The header must be parsed for byte-offset math; without it we cannot
|
||||
// build a valid Range request.
|
||||
if (!this.streamDecoder.getFormatInfo()) {
|
||||
return { success: false, error: 'Cannot calculate byte offset' };
|
||||
}
|
||||
|
||||
// The Range request is file-absolute: byte position from the start of the
|
||||
// file on disk, header included. calculateByteOffset returns an audio-data-
|
||||
// relative offset, so add headerSize to land on the right byte. (The old
|
||||
// ?offset= contract was audio-relative; the server added the header itself.)
|
||||
const header = this.streamDecoder.getWavHeader();
|
||||
if (!header) {
|
||||
return { success: false, error: 'Cannot calculate byte offset' };
|
||||
}
|
||||
const fileOffset = header.headerSize + audioOffset;
|
||||
// calculateByteOffset returns a file-absolute offset (byte position from the
|
||||
// start of the file on disk, header included) — exactly what the Range request
|
||||
// needs. The format decoder owns the header-offset addition and frame alignment.
|
||||
const fileOffset = this.streamDecoder.calculateByteOffset(position);
|
||||
|
||||
// Signal that C# needs to request a new stream from this file-absolute offset
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user