using DeepDrftModels.DTOs; using Microsoft.AspNetCore.Components; using NetBlocks.Models; namespace DeepDrftPublic.Client.Services; public interface IPlayerService { // State properties bool IsInitialized { get; } bool IsLoaded { get; } bool IsLoading { get; } bool IsPlaying { get; } bool IsPaused { get; } double CurrentTime { get; } double? Duration { get; } double Volume { get; } double LoadProgress { get; } string? ErrorMessage { get; } TrackDto? CurrentTrack { get; } /// /// Normalized loudness profile for the current track, each value in [0, 1], or null when no /// profile is available (no track loaded, or the track has no stored profile). The seek zone /// renders this as a waveform; a null profile drives the flat-but-seekable fallback. Fetched on /// track select and cleared on unload/stop; fires once it arrives. /// double[]? WaveformProfile { get; } // Events for UI updates EventCallback? OnStateChanged { get; set; } EventCallback? OnTrackSelected { get; set; } /// /// Multicast side-channel for state changes. The provider owns the single /// EventCallback (it drives the provider re-render); /// cascade consumers that read state directly off this service — and so are not /// re-rendered by the provider's render when the cascade is IsFixed — /// subscribe here to re-render themselves. Fires on the same cadence as /// (throttled to ~10/s during streaming). /// event Action? StateChanged; /// /// Raised once when the current track reaches its natural end of playback (the JS /// end-of-stream callback), distinct from a stop/unload/track-switch. This is the single /// hook the play-queue subscribes to in order to auto-advance to the next track. It does /// NOT fire when playback is stopped, the track is switched, or the player is unloaded — /// only on organic completion — so an orchestrator can treat it as "advance the queue." /// event Action? TrackEnded; // Control methods Task InitializeAsync(); Task SelectTrack(TrackDto track); Task Stop(); Task Unload(); Task TogglePlayPause(); Task Seek(double position); Task SetVolume(double volume); Task ClearError(); } public interface IStreamingPlayerService : IPlayerService { // Streaming state properties bool IsStreamingMode { get; } bool CanStartStreaming { get; } bool HeaderParsed { get; } int BufferedChunks { get; } // Streaming control methods Task SelectTrackStreaming(TrackDto track); /// /// Initializes the player (if needed) and resumes the AudioContext. Call this synchronously at /// the very start of a user-gesture handler — before any await on network I/O — so the /// gesture is still "active" when the context resumes. Safari refuses to start a suspended /// AudioContext once the originating gesture has been consumed by an intervening await /// (e.g. fetching which track to play), so warming here and streaming after is load-bearing. /// also resumes the context, but only after its own internal /// awaits — too late for a handler that must first fetch the track to play. /// Task WarmAudioContext(); /// /// Stages a track as the current track without touching the audio context or starting the /// stream. Used by the embed player, where there is no user gesture on initial load: the track /// is shown as ready, and the first play click (a genuine gesture) calls /// so the browser allows the AudioContext to start. Sets /// and notifies; performs no JS interop. /// Task StageTrack(TrackDto track); /// /// Re-streams the current track in the freshly-resolved delivery format while preserving the /// listener's playback position (Phase 18 wave 18.6 — the Settings "Apply" restart). The format is /// re-resolved on the new load via the ResolveStreamFormatAsync seam, so this picks up a just- /// changed streaming-quality preference. A cross-format byte offset can only be resolved once the NEW /// format's decoder has parsed its header, so the reload runs a fresh load from byte 0 to initialize /// that decoder, then seeks back to the saved position through the existing seek-beyond-buffer path. /// No-op when no track is loaded (nothing playing to switch). Safe to call while a track is playing; /// a track switch during the brief restore window abandons the position restore. /// Task ReloadPreservingPositionAsync(); }