feat: replace ?offset= seek with HTTP Range streaming across API, proxy, and client

- API: enableRangeProcessing true on no-offset FileStream path
- Proxy: transparent Range relay, forwards 206/416/Content-Range verbatim
- TrackMediaClient: Range: bytes=X- replaces ?offset=X; response disposed via TrackMediaResponse
- StreamDecoder: reinitializeForRangeContinuation retains wavHeader, counts raw PCM against 206 Content-Length
- AudioPlayer: seekBeyondBuffer adds headerSize for file-absolute offset; duration guard prevents continuation overwriting full-track duration
- StreamingAudioPlayerService: seek guard corrected to >= 0 (file-absolute offset contract)
This commit is contained in:
daniel-c-harvey
2026-06-09 07:00:35 -04:00
parent 5c3c3c3d0c
commit aaa9f732ae
6 changed files with 132 additions and 44 deletions
+18 -6
View File
@@ -321,18 +321,28 @@ export class AudioPlayer {
*/
private seekBeyondBuffer(position: number): AudioResult {
try {
const byteOffset = this.streamDecoder.calculateByteOffset(position);
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 (byteOffset < 0) {
if (audioOffset < 0) {
return { success: false, error: 'Cannot calculate byte offset' };
}
// Signal that C# needs to request new stream from 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;
// Signal that C# needs to request a new stream from this file-absolute offset
return {
success: true,
seekBeyondBuffer: true,
byteOffset: byteOffset
byteOffset: fileOffset
};
} catch (error) {
return { success: false, error: (error as Error).message };
@@ -368,8 +378,10 @@ export class AudioPlayer {
this.scheduler.clearForSeek();
this.scheduler.setPlaybackOffset(seekPosition);
// Reinitialize decoder for new stream
this.streamDecoder.reinitializeForOffset(totalStreamLength);
// Reinitialize decoder for the Range-continuation stream. totalStreamLength
// here is the 206 Content-Length (range start → EOF), not the full file size —
// the decoder uses it to detect stream-complete against raw audio bytes.
this.streamDecoder.reinitializeForRangeContinuation(totalStreamLength);
// Update state
this.pausePosition = seekPosition;