Add whole-release embeds to FramePlayer and un-gate the release embed share affordance

The queue gains an armed-but-idle state (Arm/Start) so a release embed stages track 0 prerender-safe, then queues the full release on first play and auto-advances.
This commit is contained in:
daniel-c-harvey
2026-06-19 12:05:35 -04:00
parent 1931574ad4
commit 912256d99a
12 changed files with 560 additions and 47 deletions
+48 -11
View File
@@ -2,6 +2,7 @@
@using DeepDrftPublic.Client.Controls.AudioPlayerBar
@using DeepDrftPublic.Client.Layout
@using DeepDrftPublic.Client.Services
@using DeepDrftPublic.Client.ViewModels
@page "/FramePlayer"
@layout EmbedLayout
@@ -10,29 +11,65 @@
@code {
[CascadingParameter] public IStreamingPlayerService? PlayerService { get; set; }
[CascadingParameter] public IQueueService? Queue { get; set; }
// Two mutually-exclusive embed targets. ReleaseEntryKey wins if both are somehow supplied — a
// release embed is the richer surface, and the single-track path would otherwise mask it.
[SupplyParameterFromQuery] public string? ReleaseEntryKey { get; set; }
[SupplyParameterFromQuery] public string? TrackEntryKey { get; set; }
[Inject] public required ITrackDataService TrackDataService { get; set; }
[Inject] public required FramePlayerViewModel ViewModel { get; set; }
private string? _stagedKey;
protected override async Task OnParametersSetAsync()
{
if (PlayerService is null || string.IsNullOrWhiteSpace(TrackEntryKey)) return;
if (PlayerService is null) return;
// OnParametersSetAsync can fire repeatedly (and once per render pass); only act when the
// key actually changes so we don't re-fetch on every parameter set.
if (TrackEntryKey == _stagedKey) return;
_stagedKey = TrackEntryKey;
if (!string.IsNullOrWhiteSpace(ReleaseEntryKey))
{
await StageRelease(ReleaseEntryKey);
}
else if (!string.IsNullOrWhiteSpace(TrackEntryKey))
{
await StageSingleTrack(TrackEntryKey);
}
}
var result = await TrackDataService.GetTrack(TrackEntryKey);
// Release embed: resolve the release's ordered tracks, stage the first so the bar shows the release
// ready, and arm the queue with the whole list. No JS interop here (StageTrack and Arm are both
// interop-free), so this runs identically during prerender and after WASM boot. The first play
// click (handled in AudioPlayerBar) routes through Queue.PlayRelease, queueing the full release.
private async Task StageRelease(string releaseEntryKey)
{
// OnParametersSetAsync can fire repeatedly; only act when the key actually changes.
if (releaseEntryKey == _stagedKey) return;
_stagedKey = releaseEntryKey;
await ViewModel.Load(releaseEntryKey);
if (ViewModel.Tracks.Count == 0) return; // No tracks: leave the bar idle.
await PlayerService!.StageTrack(ViewModel.Tracks[0]);
Queue?.Arm(ViewModel.Tracks);
}
// Single-track embed: unchanged behaviour — stage exactly the requested track. The first play
// click streams it directly (the queue stays empty/disarmed).
private async Task StageSingleTrack(string trackEntryKey)
{
if (trackEntryKey == _stagedKey) return;
_stagedKey = trackEntryKey;
var result = await TrackDataService.GetTrack(trackEntryKey);
if (result.Success && result.Value is not null)
{
// Stage only — no audio context, no streaming. The browser blocks audio until a user
// gesture, so the embed shows the track ready and the first play click (handled in
// AudioPlayerBar) calls SelectTrackStreaming. This also keeps this pass free of JS
// interop, so it works whether it runs during prerender or after WASM is interactive.
await PlayerService.StageTrack(result.Value);
// gesture, so the embed shows the track ready and the first play click calls
// SelectTrackStreaming. This keeps this pass free of JS interop, so it works whether it
// runs during prerender or after WASM is interactive.
await PlayerService!.StageTrack(result.Value);
}
// On failure, leave the bar idle; a stream-level error surfaces via PlayerService.ErrorMessage.
}
}
}