d686fe48ce
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.
98 lines
4.4 KiB
Plaintext
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();
|
|
}
|
|
|
|
}
|