Files
deepdrft/DeepDrftPublic.Client/Controls/Settings/StreamQualitySetting.razor
T
daniel-c-harvey d686fe48ce 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.
2026-06-24 22:55:03 -04:00

98 lines
4.4 KiB
Plaintext

@using DeepDrftPublic.Client.Common
@using DeepDrftPublic.Client.Services
@*
The streaming-quality control (Phase 18 wave 18.6, §4) — the first occupant of the Settings menu. Binds
the listener's choice to PublicSiteSettings.StreamQuality and persists it via the cookie seam. Honest
capability gate (OQ2 / AC7): on a browser that cannot decode Ogg Opus the Low-data option still selects,
but a note tells the listener the effective stream is lossless — we never let the choice silently imply a
format that can't play.
*@
<div class="dd-setting-control">
<MudRadioGroup T="StreamQuality" Value="_quality" ValueChanged="OnQualityChanged">
<MudRadio T="StreamQuality" Value="StreamQuality.LowData" Color="Color.Primary" Dense="true">
Low-data (Opus)
</MudRadio>
<MudRadio T="StreamQuality" Value="StreamQuality.Lossless" Color="Color.Primary" Dense="true">
Lossless (WAV)
</MudRadio>
</MudRadioGroup>
@if (_opusUnavailable && _quality == StreamQuality.LowData)
{
<div class="dd-setting-note">
This browser can't decode Opus — you'll stream lossless.
</div>
}
<div class="d-flex flex-align-right">
<MudButton Disabled="!IsApplyEnabled"
Color="Color.Primary"
Variant="Variant.Filled"
OnClick="@ApplyStreamQualitySetting">
APPLY
</MudButton>
</div>
</div>
@code {
[Inject] public required PublicSiteSettings Settings { get; set; }
[Inject] public required SettingsCookieService CookieService { get; set; }
[Inject] public required AudioInteropService AudioInterop { get; set; }
// The active player, threaded in from SettingsMenu (which reads it off the AudioPlayerProvider cascade).
// It is an explicit [Parameter], NOT a [CascadingParameter], because this control renders inside the
// MudMenu panel, which MudBlazor portals to <MudPopoverProvider> — outside AudioPlayerProvider's cascade
// scope, so a cascade would land null here. Null during prerender or when no provider is present — Apply
// then just persists the preference, with no live track to restart.
[Parameter] public IStreamingPlayerService? Player { get; set; }
private StreamQuality _quality;
public bool IsApplyEnabled => _quality != Settings.StreamQuality;
// Null until the capability probe runs (post-render JS interop). false → can decode Opus; true → cannot.
private bool _opusUnavailable;
protected override void OnInitialized()
{
// Read the current preference (already seeded at prerender + bridged into WASM).
_quality = Settings.StreamQuality;
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (!firstRender) return;
// Capability probe is JS interop — only valid once interactive. Surfaces the honest note when the
// browser can't decode Ogg Opus, so a Low-data pick reads as "effectively lossless" rather than
// silently failing. The player applies the same gate independently; this is purely the UI honesty.
var canDecodeOpus = await AudioInterop.CanDecodeOggOpus();
if (canDecodeOpus == _opusUnavailable)
{
_opusUnavailable = !canDecodeOpus;
StateHasChanged();
}
}
private async Task OnQualityChanged(StreamQuality quality)
{
_quality = quality;
}
private async Task ApplyStreamQualitySetting(MouseEventArgs arg)
{
// Persist the choice first so the cookie + in-memory PublicSiteSettings.StreamQuality both reflect
// the new value BEFORE the restart: the reload re-resolves the delivery format via
// PreferenceAwareStreamingPlayerService, which reads PublicSiteSettings.StreamQuality fresh.
await CookieService.SetStreamQualityAsync(_quality);
// Switch the currently-playing track to the new format immediately, preserving the listener's
// position. No-op inside the player when nothing is loaded — the new preference then simply
// applies to the next track played. Fire-and-forget: the reload runs the streaming loop for the
// life of the track, so awaiting it would pin this handler open; UI updates flow through the
// player's state notifications, not this await.
_ = Player?.ReloadPreservingPositionAsync();
}
}