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 Logger { 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. _audioPlayerService = new StreamingAudioPlayerService(AudioInterop, TrackMediaClient, Logger); // 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); } /// /// 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). /// 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; } }