feature: Phase 18.6 Track A — public Settings menu + streaming-quality toggle
This commit is contained in:
@@ -76,7 +76,7 @@ public class AudioInteropService : IAsyncDisposable
|
||||
/// stays on the universal lossless path (AC7 — no listener ever gets silence over a codec gap). Probe
|
||||
/// failures degrade to <c>false</c> (assume incapable) so an interop error can never silence playback.
|
||||
/// </summary>
|
||||
public async Task<bool> CanDecodeOggOpus(string playerId)
|
||||
public async Task<bool> CanDecodeOggOpus()
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
using DeepDrftModels.Enums;
|
||||
using DeepDrftPublic.Client.Clients;
|
||||
using DeepDrftPublic.Client.Common;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace DeepDrftPublic.Client.Services;
|
||||
|
||||
/// <summary>
|
||||
/// The production player that honours the listener's streaming-quality preference (Phase 18 wave 18.6).
|
||||
/// Extends <see cref="StreamingAudioPlayerService"/> through the single deliberately-overridable seam,
|
||||
/// <see cref="StreamingAudioPlayerService.ResolveStreamFormatAsync"/>, so the rest of the streaming stack
|
||||
/// (seek, telemetry, the seek-beyond-buffer format reuse) is inherited verbatim.
|
||||
/// <para>
|
||||
/// The override is one branch: a <see cref="StreamQuality.Lossless"/> preference returns
|
||||
/// <see cref="AudioFormat.Lossless"/> immediately; anything else falls through to <c>base</c>, which keeps
|
||||
/// the 18.5 invariants intact — the capability gate (AC7: a browser that can't decode Ogg Opus gets lossless)
|
||||
/// and the sidecar-absent → lossless fallback (C2: a legacy / un-backfilled / failed-transcode track gets
|
||||
/// lossless). So a Lossless pick always yields lossless; a Low-data pick yields Opus only when it can
|
||||
/// actually play, and lossless otherwise. No path produces an unplayable stream.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public class PreferenceAwareStreamingPlayerService : StreamingAudioPlayerService
|
||||
{
|
||||
private readonly PublicSiteSettings _settings;
|
||||
|
||||
public PreferenceAwareStreamingPlayerService(
|
||||
AudioInteropService audioInterop,
|
||||
TrackMediaClient trackMediaClient,
|
||||
ILogger<StreamingAudioPlayerService> logger,
|
||||
PublicSiteSettings settings)
|
||||
: base(audioInterop, trackMediaClient, logger)
|
||||
{
|
||||
_settings = settings;
|
||||
}
|
||||
|
||||
protected override async Task<AudioFormat> ResolveStreamFormatAsync(string entryKey, CancellationToken cancellationToken)
|
||||
{
|
||||
// Listener explicitly chose lossless — request it directly, no Opus probe / sidecar fetch needed.
|
||||
if (_settings.StreamQuality == StreamQuality.Lossless)
|
||||
{
|
||||
return AudioFormat.Lossless;
|
||||
}
|
||||
|
||||
// Low-data preference: defer to the base capability-gated resolution, which probes Opus support and
|
||||
// the sidecar's presence and degrades to lossless when either is missing. Both 18.5 invariants are
|
||||
// inherited here, not re-implemented.
|
||||
return await base.ResolveStreamFormatAsync(entryKey, cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
using DeepDrftPublic.Client.Common;
|
||||
using Microsoft.JSInterop;
|
||||
|
||||
namespace DeepDrftPublic.Client.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Client-side runtime writer for public-site settings (Phase 18 wave 18.6), the analogue of
|
||||
/// <see cref="DarkModeCookieService"/>. Reads the current preference off the in-memory
|
||||
/// <see cref="PublicSiteSettings"/> (already seeded at prerender and bridged into WASM), and writes a
|
||||
/// 365-day cookie via <c>document.cookie</c> interop when the listener changes it in the Settings menu —
|
||||
/// the same durable-truth seam dark mode uses, so the choice survives the session and seeds the next visit's
|
||||
/// prerender (no flash).
|
||||
/// </summary>
|
||||
public class SettingsCookieService(PublicSiteSettings settings, IJSRuntime js) : SettingsServiceBase
|
||||
{
|
||||
private const int ExpiryDays = 365;
|
||||
|
||||
public StreamQuality GetStreamQuality() => settings.StreamQuality;
|
||||
|
||||
public async ValueTask SetStreamQualityAsync(StreamQuality quality)
|
||||
{
|
||||
if (settings.StreamQuality == quality) return;
|
||||
|
||||
await WriteCookieAsync(StreamQualityCookieName, FormatStreamQuality(quality));
|
||||
settings.StreamQuality = quality;
|
||||
}
|
||||
|
||||
private async ValueTask WriteCookieAsync(string name, string value)
|
||||
{
|
||||
var expires = DateTime.UtcNow.AddDays(ExpiryDays).ToString("R");
|
||||
await js.InvokeVoidAsync("eval",
|
||||
$"document.cookie = '{name}={value}; expires={expires}; path=/; SameSite=Lax'");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using DeepDrftPublic.Client.Common;
|
||||
|
||||
namespace DeepDrftPublic.Client.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Shared cookie contract for the public-site settings seam (Phase 18 wave 18.6), the analogue of
|
||||
/// <see cref="DarkModeServiceBase"/>. Holds the cookie names and the (de)serialization for each preference
|
||||
/// so the server prerender-read service and the client cookie-write service agree on one wire format —
|
||||
/// the load-bearing reason this is shared rather than duplicated. Each new preference adds its cookie name
|
||||
/// and a parse/format pair here, keeping the round-trip in one place.
|
||||
/// </summary>
|
||||
public abstract class SettingsServiceBase
|
||||
{
|
||||
protected const string StreamQualityCookieName = "streamQuality";
|
||||
|
||||
/// <summary>
|
||||
/// Parses the <c>streamQuality</c> cookie value into <see cref="StreamQuality"/>, defaulting to
|
||||
/// <see cref="StreamQuality.LowData"/> (the OQ2 default) for an absent, empty, or unrecognized value so
|
||||
/// a missing/garbled cookie never produces a surprising preference.
|
||||
/// </summary>
|
||||
protected static StreamQuality ParseStreamQuality(string? cookieValue) =>
|
||||
Enum.TryParse<StreamQuality>(cookieValue, ignoreCase: true, out var parsed)
|
||||
? parsed
|
||||
: StreamQuality.LowData;
|
||||
|
||||
/// <summary>Formats a <see cref="StreamQuality"/> for cookie storage (round-trips with <see cref="ParseStreamQuality"/>).</summary>
|
||||
protected static string FormatStreamQuality(StreamQuality quality) => quality.ToString();
|
||||
}
|
||||
@@ -280,7 +280,7 @@ public class StreamingAudioPlayerService : AudioPlayerService, IStreamingPlayerS
|
||||
protected virtual async Task<AudioFormat> ResolveStreamFormatAsync(string entryKey, CancellationToken cancellationToken)
|
||||
{
|
||||
// Capability gate first (AC7): never hand Ogg Opus to a browser that cannot decode it.
|
||||
if (!await _audioInterop.CanDecodeOggOpus(PlayerId))
|
||||
if (!await _audioInterop.CanDecodeOggOpus())
|
||||
{
|
||||
return AudioFormat.Lossless;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user