94 lines
4.9 KiB
C#
94 lines
4.9 KiB
C#
using DeepDrftPublic.Client.Common;
|
|
using DeepDrftPublic.Client.Services;
|
|
using DeepDrftPublic.Client.Clients;
|
|
using Microsoft.AspNetCore.Components;
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
namespace DeepDrftPublic.Client.Controls;
|
|
|
|
public partial class AudioPlayerProvider : ComponentBase, IAsyncDisposable
|
|
{
|
|
[Inject] public required AudioInteropService AudioInterop { get; set; }
|
|
[Inject] public required TrackMediaClient TrackMediaClient { get; set; }
|
|
[Inject] public required ILogger<StreamingAudioPlayerService> Logger { get; set; }
|
|
[Inject] public required BeaconInterop Beacon { get; set; }
|
|
[Inject] public required IPlayEventSink PlayEventSink { get; set; }
|
|
[Inject] public required IAnonIdProvider AnonId { get; set; }
|
|
[Inject] public required PublicSiteSettings Settings { get; set; }
|
|
|
|
private IStreamingPlayerService? _audioPlayerService;
|
|
private QueueService? _queueService;
|
|
|
|
[Parameter] public RenderFragment? ChildContent { get; set; }
|
|
|
|
protected override void OnInitialized()
|
|
{
|
|
// Create the service immediately (but don't initialize yet).
|
|
// The base class lazily initializes on first track selection via
|
|
// EnsureInitializedAsync — that path is correct because audio contexts
|
|
// require a user gesture anyway. Initializing eagerly here causes 4+
|
|
// SignalR round-trips before any content is stable.
|
|
// Construct the preference-aware player (Phase 18 wave 18.6): it honours the listener's streaming-
|
|
// quality choice via the ResolveStreamFormatAsync seam while inheriting the 18.5 capability gate and
|
|
// C2 fallback. PublicSiteSettings is scoped data (already prerender-seeded + WASM-bridged), so passing
|
|
// it through the constructor is cheap and carries no lifecycle — the telemetry tracker still binds
|
|
// post-construction below, exactly as before.
|
|
var player = new PreferenceAwareStreamingPlayerService(AudioInterop, TrackMediaClient, Logger, Settings);
|
|
|
|
// Phase 16: bind the play-session tracker to the player after construction, the same way the
|
|
// queue binds — the player is built with `new`, not DI, so threading telemetry through its
|
|
// constructor would force the provider to over-resolve. The tracker owns the floor/bucket logic
|
|
// and emits via the injected sink (the beacon in production); the beacon also drives the
|
|
// page-unload close so a mid-play tab-close still records the listen. Attached on the concrete
|
|
// type before it is exposed through the IStreamingPlayerService field.
|
|
player.AttachTracker(new PlayTracker(PlayEventSink), Beacon);
|
|
_audioPlayerService = player;
|
|
|
|
// Provider is the SOLE owner of OnStateChanged. When the service fires,
|
|
// the provider re-renders, which cascades to its children automatically.
|
|
// Children must not wrap or replace this callback.
|
|
_audioPlayerService.OnStateChanged = new EventCallback(this, () => InvokeAsync(StateHasChanged));
|
|
// OnTrackSelected will be set by individual child components that need it
|
|
|
|
// The queue orchestrates above the single-slot player. The player is not DI-registered
|
|
// (constructed here), so the queue binds to it via Attach rather than constructor injection —
|
|
// no construction cycle, no IServiceProvider. Cascaded alongside the player so the bar and a
|
|
// future up-next panel both read it.
|
|
_queueService = new QueueService();
|
|
_queueService.Attach(_audioPlayerService);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Warm the anon-id cache once the provider is interactive (Phase 16 wave 16.3). Done here, after the
|
|
/// first render, because the localStorage read is JS interop — not available during prerender. By the
|
|
/// time any play session closes and the sink reads <c>AnonId.Current</c>, the cache is populated; a
|
|
/// play that somehow closes before this completes simply sends no anonId (acceptable over-count). The
|
|
/// provider is the natural warm point: it is mounted in MainLayout, so it goes interactive on every
|
|
/// page the player can play from.
|
|
/// </summary>
|
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
|
{
|
|
if (firstRender)
|
|
await AnonId.EnsureLoadedAsync();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Dispose the player on unmount so the JS setInterval driving progress
|
|
/// callbacks no longer holds a DotNetObjectReference into a destroyed
|
|
/// component (otherwise it throws every 100ms after navigation away).
|
|
/// </summary>
|
|
public async ValueTask DisposeAsync()
|
|
{
|
|
// Dispose the queue first so it unsubscribes from the player's TrackEnded before the
|
|
// player tears down.
|
|
_queueService?.Dispose();
|
|
_queueService = null;
|
|
|
|
if (_audioPlayerService is IAsyncDisposable disposable)
|
|
{
|
|
await disposable.DisposeAsync();
|
|
}
|
|
_audioPlayerService = null;
|
|
}
|
|
}
|