Streaming Seek Support

This commit is contained in:
daniel-c-harvey
2025-12-07 04:44:54 -05:00
parent 8c58edd5f9
commit 20db222a0f
12 changed files with 493 additions and 26 deletions
+41 -4
View File
@@ -26,6 +26,11 @@ export class PlaybackScheduler {
private nextScheduleTime: number = 0; // AudioContext time for next buffer
private isActive_: boolean = false; // Prevents scheduling during pause/stop
// Offset for seek-beyond-buffer scenarios
// When seeking to position T beyond buffers, we clear buffers and set playbackOffset = T
// The new stream starts at T, so buffer positions are relative to T
private playbackOffset: number = 0;
// Callbacks
public onPlaybackEnded: (() => void) | null = null;
@@ -56,14 +61,30 @@ export class PlaybackScheduler {
}
/**
* Get current playback position in seconds
* Get current playback position in seconds (includes playbackOffset for seek-beyond-buffer)
*/
getCurrentPosition(): number {
if (this.playbackAnchorTime === 0) {
return this.playbackAnchorPosition;
return this.playbackAnchorPosition + this.playbackOffset;
}
const elapsed = this.contextManager.currentTime - this.playbackAnchorTime;
return Math.min(this.playbackAnchorPosition + elapsed, this.getTotalDuration());
return Math.min(this.playbackAnchorPosition + this.playbackOffset + elapsed, this.getTotalDuration() + this.playbackOffset);
}
/**
* Set the playback offset for seek-beyond-buffer scenarios
* This represents the absolute time position where the current buffers start
*/
setPlaybackOffset(offset: number): void {
this.playbackOffset = offset;
console.log(`📍 Playback offset set to ${offset.toFixed(3)}s`);
}
/**
* Get the current playback offset
*/
getPlaybackOffset(): number {
return this.playbackOffset;
}
/**
@@ -244,7 +265,7 @@ export class PlaybackScheduler {
}
/**
* Full reset - clears all buffers
* Full reset - clears all buffers and resets offset
*/
clear(): void {
this.isActive_ = false;
@@ -254,9 +275,25 @@ export class PlaybackScheduler {
this.playbackAnchorTime = 0;
this.nextBufferIndex = 0;
this.nextScheduleTime = 0;
this.playbackOffset = 0;
console.log('🗑️ Scheduler cleared');
}
/**
* Clear buffers but keep offset - for seek-beyond-buffer scenarios
*/
clearForSeek(): void {
this.isActive_ = false;
this.stopAllSources();
this.buffers = [];
this.playbackAnchorPosition = 0;
this.playbackAnchorTime = 0;
this.nextBufferIndex = 0;
this.nextScheduleTime = 0;
// Note: playbackOffset is NOT reset - it will be set by the caller
console.log('🗑️ Scheduler cleared for seek (offset preserved)');
}
/**
* Check if we have buffers
*/
@@ -202,6 +202,25 @@ export class StreamDecoder {
return this.totalStreamLength > 0 && this.totalRawBytes >= (this.totalStreamLength - (this.wavHeader?.headerSize ?? 0));
}
/**
* Get the WAV header info for byte offset calculation
*/
getWavHeader(): WavHeader | null {
return this.wavHeader;
}
/**
* Calculate byte offset from a time position (in seconds)
* Returns block-aligned byte offset for clean audio
*/
calculateByteOffset(positionSeconds: number): number {
if (!this.wavHeader || this.wavHeader.byteRate <= 0) return 0;
const rawOffset = Math.floor(positionSeconds * this.wavHeader.byteRate);
// Align to block boundary for clean audio
return Math.floor(rawOffset / this.wavHeader.blockAlign) * this.wavHeader.blockAlign;
}
/**
* Reset decoder state
*/
@@ -213,4 +232,20 @@ export class StreamDecoder {
this.isFirstChunk = true;
this.totalStreamLength = 0;
}
/**
* Reinitialize for offset streaming - preserves header format knowledge
* Called when seeking beyond buffer to prepare for new stream from server
*/
reinitializeForOffset(totalStreamLength: number): void {
// Reset data state but we'll get a fresh header from the offset stream
this.rawChunks = [];
this.totalRawBytes = 0;
this.processedBytes = 0;
this.isFirstChunk = true;
this.totalStreamLength = totalStreamLength;
// wavHeader will be reparsed from the new stream (server sends fresh header)
this.wavHeader = null;
console.log(`StreamDecoder reinitialized for offset: expecting ${totalStreamLength} bytes`);
}
}
+17
View File
@@ -81,6 +81,23 @@ const DeepDrftAudio = {
return player.seek(position);
},
// New methods for seek-beyond-buffer support
getBufferedDuration: (playerId: string): number => {
const player = audioPlayers.get(playerId);
return player?.getBufferedDuration() ?? 0;
},
calculateByteOffset: (playerId: string, positionSeconds: number): number => {
const player = audioPlayers.get(playerId);
return player?.calculateByteOffset(positionSeconds) ?? 0;
},
reinitializeFromOffset: (playerId: string, totalStreamLength: number, seekPosition: number): AudioResult => {
const player = audioPlayers.get(playerId);
if (!player) return { success: false, error: 'Player not found' };
return player.reinitializeFromOffset(totalStreamLength, seekPosition);
},
setVolume: (playerId: string, volume: number): AudioResult => {
const player = audioPlayers.get(playerId);
if (!player) return { success: false, error: 'Player not found' };