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:
daniel-c-harvey
2026-06-24 22:55:03 -04:00
parent 7adc35dd5d
commit d686fe48ce
8 changed files with 284 additions and 12 deletions
+10
View File
@@ -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.