Streaming Seek Support
This commit is contained in:
@@ -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`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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' };
|
||||
|
||||
Reference in New Issue
Block a user