From 93569e30d1ad11439bfd74bade0f977141cf277d Mon Sep 17 00:00:00 2001 From: daniel-c-harvey Date: Wed, 3 Jun 2026 14:31:51 -0400 Subject: [PATCH] docs: document StateChanged pattern, readiness guard, dead path removal; clear resolved TODOs --- DeepDrftPublic.Client/CLAUDE.md | 11 ++++++----- TODO.md | 16 +--------------- 2 files changed, 7 insertions(+), 20 deletions(-) diff --git a/DeepDrftPublic.Client/CLAUDE.md b/DeepDrftPublic.Client/CLAUDE.md index e2fa7ef..d223180 100644 --- a/DeepDrftPublic.Client/CLAUDE.md +++ b/DeepDrftPublic.Client/CLAUDE.md @@ -48,11 +48,11 @@ Both are configured with JSON serializer settings (case-insensitive property mat ## Audio player stack (deepest part of the codebase) ### Contracts -- `IPlayerService`: Initialize, SelectTrack, Play, Pause, Stop, Seek, SetVolume. Sync interface. +- `IPlayerService`: Initialize, SelectTrack, Play, Pause, Stop, Seek, SetVolume. Sync interface. Owns `EventCallback? OnStateChanged` (single, provider-owned) and `event Action? StateChanged` (multicast, for cascade consumers). - `IStreamingPlayerService`: Extends above. SelectTrackStreaming(track) starts the chunked stream flow. ### Implementation -- `AudioPlayerService` (abstract base): Lifecycle. Stores current track, playback state, volume. Derived classes implement `SelectTrackStreaming` / `SelectTrackImmediate`. +- `AudioPlayerService` (abstract base): Lifecycle. Stores current track, playback state, volume. `SelectTrack` throws `NotSupportedException` (buffered path is dead); derived classes override `SelectTrackStreaming`. - `StreamingAudioPlayerService` (production): Constructor takes `TrackMediaClient`, `AudioInteropService`, logger. `SelectTrackStreaming`: 1. Calls `TrackMediaClient.GetAudioStreamAsync(trackId, offset: 0)`. 2. `StreamingAudioPlayerService.StreamAudioAsync` reads chunks (16–64 KB adaptive), pushes each via `AudioInteropService.ProcessStreamingChunkAsync` (JS interop call). @@ -62,14 +62,15 @@ Both are configured with JSON serializer settings (case-insensitive property mat 6. **Seek beyond buffer**: if seek target is past the decoded range, `Seek(position)` calls `TrackMediaClient.GetAudioStreamAsync(trackId, offset: byteOffset)`. Server's `WavOffsetService` synthesises a new 44-byte WAV header and streams from the offset. Player tears down and re-initialises decoder for the new stream. ### Interop bridge +- `AudioInteropService.CreatePlayerAsync` polls `DeepDrftAudio.isReady()` before proceeding; `index.ts` sets `ready = true` after attaching the API to `window`. This guards against slow WASM boot / cache misses. - `AudioInteropService.ProcessStreamingChunkAsync(chunk)` calls JS `window.DeepDrftAudio.processStreamingChunk(chunk)` and awaits the Promise. - `AudioInteropService` also manages callback registrations for progress (fired by `PlaybackScheduler`), end-of-playback (fired by `PlaybackScheduler`), and spectrum data (fired by `SpectrumAnalyzer`). Each callback is a `DotNetObjectReference` to a delegate. ### Component integration -- `AudioPlayerProvider.razor` is the cascading host. It injects `IStreamingPlayerService` (resolved to `StreamingAudioPlayerService` in DI), stores it in a cascade, and keeps it alive across navigation. -- `AudioPlayerBar.razor` is the dock UI. It cascades the player, binds buttons to `Play()` / `Pause()` / `Seek()` / `SetVolume()`, and displays current time / duration / progress bar. +- `AudioPlayerProvider.razor` is the cascading host. It injects `IStreamingPlayerService` (resolved to `StreamingAudioPlayerService` in DI), stores it in a cascade with `IsFixed="true"`, and keeps it alive across navigation. +- `AudioPlayerBar.razor` is the dock UI. It cascades the player, binds buttons to `Play()` / `Pause()` / `Seek()` / `SetVolume()`, and displays current time / duration / progress bar. Subscribes to `IPlayerService.StateChanged` in `OnParametersSet` (reference-guarded, idempotent) and unsubscribes on dispose to re-render itself when the cascade updates. - `SpectrumVisualizer.razor` calls `AudioInteropService.GetSpectrumData()` on a timer, receives bar heights, renders via MudBlazor `MudChart` or custom canvas. -- `TracksView.razor` injects `TracksViewModel` + cascaded `IStreamingPlayerService`. `PlayTrack(track)` calls `PlayerService.SelectTrack(track)` (which resolves to `StreamingAudioPlayerService.SelectTrackStreaming(track)`). +- `TracksView.razor` injects `TracksViewModel` + cascaded `IStreamingPlayerService`. `PlayTrack(track)` calls `PlayerService.SelectTrackStreaming(track)`. Subscribes to `IPlayerService.StateChanged` in `OnParametersSet` and clears `_selectedTrack` when `!PlayerService.IsLoaded` (covers Stop, Unload, and end-of-track). ## Dark-mode plumbing diff --git a/TODO.md b/TODO.md index 93a6a93..d4aa849 100644 --- a/TODO.md +++ b/TODO.md @@ -1,17 +1,3 @@ # TODO.md — Known issues and bugs -Pre-existing bugs and known issues not yet triaged into the roadmap. Items here are waiting for scheduling or architectural clarity. - ---- - -## Player stack — adjacent correctness/hygiene issues - -Surfaced by the same 2026-06-03 analysis (`PLAYER_ANALYSIS.md §4`). Distinct from the cascade bug. - -- **No JS-module-readiness guard.** `AudioInteropService.CreatePlayerAsync` assumes `window.DeepDrftAudio` is present; a slow WASM boot / cache miss produces a spurious init failure. Add a readiness guard or load-order guarantee. -- **Dead legacy buffered path is reachable and silent.** The base `AudioPlayerService.SelectTrack` (non-streaming) path calls `InitializeBufferedPlayerAsync` / `AppendAudioBlockAsync` / `FinalizeAudioBufferAsync`, which are **no-ops** in `index.ts`. Any non-streaming player reports success and plays silence. Remove the dead path or make it fail loudly. -- **Dead TypeScript.** `Interop/audiobuffermanager.ts` (orphaned, missing the live scheduler's `isActive_` / `playbackOffset` fixes) and `Interop/webaudio.ts` (legacy re-export shim) are not part of the `audio/` engine. Delete after confirming no build reference. -- **Duplicated UI helpers.** `GetPlayIcon` and `FormatTime` are each implemented twice across `AudioPlayerBar` and child controls — drift risk. Consolidate. -- **Misleading minimized-dock affordance.** The minimized button shows a play icon but `OnClick` toggles minimize, not playback. - ---- +No open bugs.