fix: AC9 seek fine re-sync + deterministic decoder drain (WebCodecs Opus)
Seek now trims the lead-in so playback lands at the requested time, not the page start; decoder drain polls decodeQueueSize (bounded) instead of a single timeout. Minor cleanups.
This commit is contained in:
@@ -16,7 +16,7 @@ import { WavFormatDecoder } from './WavFormatDecoder.js';
|
||||
import { Mp3FormatDecoder } from './Mp3FormatDecoder.js';
|
||||
import { FlacFormatDecoder } from './FlacFormatDecoder.js';
|
||||
import { OpusStreamDecoder } from './OpusStreamDecoder.js';
|
||||
import { OpusSeekData, parseSidecar, resolveOpusByteOffset } from './OpusSidecar.js';
|
||||
import { OpusSeekData, parseSidecar, resolveOpusByteOffset, OpusSeekResolution } from './OpusSidecar.js';
|
||||
|
||||
export interface AudioResult {
|
||||
success: boolean;
|
||||
@@ -56,6 +56,10 @@ export class AudioPlayer {
|
||||
// The sidecar in effect for the active Opus stream (its seek index resolves byte offsets). Distinct
|
||||
// from pendingOpusSidecar, which is the one set for the NEXT stream init.
|
||||
private activeOpusSidecar: OpusSeekData | null = null;
|
||||
// The landing time of the most recent seek-beyond-buffer page resolution (seconds). Set by
|
||||
// seekBeyondBuffer, consumed by reinitializeFromOffset to trim leading decoded frames so the
|
||||
// audible position matches the requested seek target (AC9 fine re-sync, §3.4a step 4).
|
||||
private _seekLandingTime: number = 0;
|
||||
|
||||
// Playback state
|
||||
private isPlaying: boolean = false;
|
||||
@@ -453,15 +457,19 @@ export class AudioPlayer {
|
||||
try {
|
||||
// Opus: resolve the offset from the precomputed seek index (the accurate VBR-safe transfer
|
||||
// function). The returned offset is a real page start, so the Range continuation lands the
|
||||
// demuxer/decoder Ogg-sync-aligned.
|
||||
// demuxer/decoder Ogg-sync-aligned. Also capture the landing time (t_page ≤ position) so
|
||||
// reinitializeFromOffset can trim the leading decoded frames and land precisely at `position`
|
||||
// (AC9 fine re-sync, §3.4a step 4).
|
||||
if (this.opusDecoder) {
|
||||
if (!this.activeOpusSidecar) {
|
||||
return { success: false, error: 'Cannot calculate byte offset' };
|
||||
}
|
||||
const resolution: OpusSeekResolution = resolveOpusByteOffset(this.activeOpusSidecar, position);
|
||||
this._seekLandingTime = resolution.landingTimeSeconds;
|
||||
return {
|
||||
success: true,
|
||||
seekBeyondBuffer: true,
|
||||
byteOffset: resolveOpusByteOffset(this.activeOpusSidecar, position)
|
||||
byteOffset: resolution.byteOffset
|
||||
};
|
||||
}
|
||||
|
||||
@@ -499,7 +507,7 @@ export class AudioPlayer {
|
||||
calculateByteOffset(positionSeconds: number): number {
|
||||
if (this.opusDecoder) {
|
||||
return this.activeOpusSidecar
|
||||
? resolveOpusByteOffset(this.activeOpusSidecar, positionSeconds)
|
||||
? resolveOpusByteOffset(this.activeOpusSidecar, positionSeconds).byteOffset
|
||||
: 0;
|
||||
}
|
||||
if (!this.streamDecoder.getFormatInfo()) return 0;
|
||||
@@ -521,10 +529,11 @@ export class AudioPlayer {
|
||||
this.scheduler.setPlaybackOffset(seekPosition);
|
||||
|
||||
// Reinitialize the active decoder for the Range-continuation stream (206 body, no header/
|
||||
// setup pages). Opus resets demux + codec state (keeping the cached config); the
|
||||
// setup pages). Opus resets demux + codec state (keeping the cached config) and arms the
|
||||
// lead-trim so decoded audio starts at `seekPosition`, not at the page boundary (AC9). The
|
||||
// StreamDecoder path uses totalStreamLength (the 206 Content-Length) to detect completion.
|
||||
if (this.opusDecoder) {
|
||||
this.opusDecoder.reinitializeForRangeContinuation();
|
||||
this.opusDecoder.reinitializeForRangeContinuation(this._seekLandingTime, seekPosition);
|
||||
} else {
|
||||
this.streamDecoder.reinitializeForRangeContinuation(totalStreamLength);
|
||||
}
|
||||
@@ -643,6 +652,7 @@ export class AudioPlayer {
|
||||
this.isStreamingMode = false;
|
||||
this.streamingStarted = false;
|
||||
this.streamingCompleted = false;
|
||||
this._seekLandingTime = 0;
|
||||
}
|
||||
|
||||
private handlePlaybackEnded(): void {
|
||||
|
||||
Reference in New Issue
Block a user