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:
@@ -123,21 +123,40 @@ export function presentationTimeSeconds(granulePosition: number, preSkip: number
|
||||
return Math.max(0, (granulePosition - preSkip) / OPUS_SAMPLE_RATE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Result of resolving a seek time to a page-start byte offset.
|
||||
* `byteOffset` is the Range request origin; `landingTimeSeconds` is the actual presentation time of that
|
||||
* page (t_page ≤ positionSeconds). The caller uses the delta `positionSeconds - landingTimeSeconds` to
|
||||
* trim the decoded leading frames so playback lands at the requested position, not at t_page (AC9).
|
||||
*/
|
||||
export interface OpusSeekResolution {
|
||||
/** Page-start byte offset to use as the Range request origin (Ogg-sync-aligned). */
|
||||
byteOffset: number;
|
||||
/**
|
||||
* Presentation time of the resolved index page (seconds). Always ≤ positionSeconds. The decoder
|
||||
* must trim `(positionSeconds - landingTimeSeconds) * OPUS_SAMPLE_RATE` leading frames so the
|
||||
* audible start and the reported clock both land at positionSeconds, not at landingTimeSeconds.
|
||||
*/
|
||||
landingTimeSeconds: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a seek time (seconds) to a file-absolute, page-start byte offset via the precomputed index —
|
||||
* the accurate VBR-safe transfer function (§3.4a A/C). Binary-searches for the largest entry whose
|
||||
* presentation time is <= `positionSeconds` and returns its exact page-start byte offset. NOT
|
||||
* interpolation, NOT byteRate math. With an empty index it degrades to the start of audio (the offset
|
||||
* of the first audio page == the setup-header length, since the server emits [setup pages][audio pages]).
|
||||
* presentation time is <= `positionSeconds`. Returns both the page-start byte offset AND the actual
|
||||
* landing time of that page, so callers can trim leading decoded frames to land precisely at
|
||||
* `positionSeconds` (AC9 fine re-sync). NOT interpolation, NOT byteRate math.
|
||||
*
|
||||
* With an empty index it degrades to the start of audio (offset == setup-header length, landing == 0).
|
||||
*
|
||||
* This is the single source of truth for Opus seek-offset math, shared by the seek-beyond-buffer path
|
||||
* (AudioPlayer) and any byte-offset resolver. The Range fetch from this offset lands the decoder
|
||||
* Ogg-sync-aligned because every indexed offset is a real page start.
|
||||
*/
|
||||
export function resolveOpusByteOffset(sidecar: OpusSeekData, positionSeconds: number): number {
|
||||
export function resolveOpusByteOffset(sidecar: OpusSeekData, positionSeconds: number): OpusSeekResolution {
|
||||
const points = sidecar.points;
|
||||
if (points.length === 0) {
|
||||
return sidecar.setupHeaderBytes.length;
|
||||
return { byteOffset: sidecar.setupHeaderBytes.length, landingTimeSeconds: 0 };
|
||||
}
|
||||
|
||||
let lo = 0;
|
||||
@@ -153,7 +172,10 @@ export function resolveOpusByteOffset(sidecar: OpusSeekData, positionSeconds: nu
|
||||
hi = mid - 1;
|
||||
}
|
||||
}
|
||||
return points[best].byteOffset;
|
||||
return {
|
||||
byteOffset: points[best].byteOffset,
|
||||
landingTimeSeconds: presentationTimeSeconds(points[best].granulePosition, sidecar.preSkip)
|
||||
};
|
||||
}
|
||||
|
||||
function toUint8Array(input: Uint8Array | ArrayBuffer | ArrayBufferView): Uint8Array {
|
||||
|
||||
Reference in New Issue
Block a user