Apply stream-quality change live by reloading at current position
Finish the Settings "Apply" behavior so changing streaming quality mid-track
switches format immediately instead of only persisting the cookie for the next
play.
- SettingsMenu reads the AudioPlayerProvider cascade and threads the player into
StreamQualitySetting as an explicit parameter (the MudMenu panel portals to
MudPopoverProvider, outside the cascade scope, so a [CascadingParameter] there
lands null). StreamQualitySetting's Apply persists the cookie, then asks the
player to reload preserving position.
- Add a "load at timestamp" path to the player rather than restart-from-0-then-
seek (which audibly played the start and raced the just-started scheduler into
a crash). ReloadPreservingPositionAsync loads the track in the newly-resolved
format beginning DIRECTLY at the saved position:
* new JS resolveStreamOffset(position) resolves the file-absolute byte offset
with no playback/buffer state (Opus from its sidecar immediately; WAV after
a header probe),
* StartFromPositionAsync converges onto the existing seek/refill loop
(RunSegmentedStreamAsync with a non-null seekPosition) so the decoder
reinitializes for a header-less Range continuation and starts playback at
the target,
* ProbeHeaderAsync feeds the byte-0 segment to the decoder WITHOUT starting
playback until the WAV header parses (bounded by 256 KB); the probe buffers
are dropped by the continuation's clearForSeek, so nothing is audible.
- IStreamingPlayerService gains ReloadPreservingPositionAsync; the QueueService
test fake implements it.
This commit is contained in:
@@ -532,6 +532,22 @@ export class AudioPlayer {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the file-absolute byte offset to begin a stream at `position`, WITHOUT requiring active
|
||||
* playback or buffered audio (the "load at timestamp" entry point — Phase 18 wave 18.6 format switch).
|
||||
* Unlike seek(), it has no duration guard and never routes to the within-buffer path: a fresh load has
|
||||
* no scheduler window, so the answer is always "start the byte stream here". For Opus the sidecar
|
||||
* resolves the offset (and captures the page landing time for the lead-trim) immediately after init; for
|
||||
* WAV the header must already be parsed (feed the byte-0 segment first). Returns success:false when the
|
||||
* decoder cannot yet resolve an offset (no header / no sidecar), so the caller can probe and retry.
|
||||
*/
|
||||
resolveStreamOffset(position: number): AudioResult {
|
||||
if (!this.isStreamingMode) {
|
||||
return { success: false, error: 'Not in streaming mode' };
|
||||
}
|
||||
return this.seekBeyondBuffer(position);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the total buffered duration (for C# to check if seek is within buffer)
|
||||
*/
|
||||
|
||||
@@ -117,6 +117,16 @@ const DeepDrftAudio = {
|
||||
return player?.calculateByteOffset(positionSeconds) ?? 0;
|
||||
},
|
||||
|
||||
// "Load at timestamp" seam (Phase 18 wave 18.6 format switch). Resolve the file-absolute byte offset
|
||||
// to begin a stream at `position` with no playback/buffer state — the C# load-from-position path calls
|
||||
// this after initializeStreaming (Opus: sidecar resolves immediately; WAV: after a header probe) and
|
||||
// then streams from the returned offset via the seek/refill loop. seekBeyondBuffer:true + byteOffset.
|
||||
resolveStreamOffset: (playerId: string, position: number): AudioResult => {
|
||||
const player = audioPlayers.get(playerId);
|
||||
if (!player) return { success: false, error: 'Player not found' };
|
||||
return player.resolveStreamOffset(position);
|
||||
},
|
||||
|
||||
// Phase 21.2a back-pressure poll: the C# read loop calls this WHILE throttled to learn when
|
||||
// the scheduler has drained below low-water and reading may resume. A missing player reads as
|
||||
// "not paused" so a torn-down player never wedges a loop that is already exiting.
|
||||
|
||||
Reference in New Issue
Block a user