Skip to main content

dioxus_audio/
playback.rs

1//! Audio playback state and hooks.
2
3use std::fmt;
4use std::sync::Arc;
5use std::time::Duration;
6
7use dioxus::prelude::*;
8
9use crate::AudioData;
10use crate::AudioError;
11use crate::AudioErrorKind;
12use crate::analysis::AudioAnalyser;
13use crate::analysis::WaveformSelection;
14
15#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
16mod web;
17
18/// When Playback attaches the current source to its media resource.
19#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
20#[non_exhaustive]
21pub enum PlaybackLoadingPolicy {
22    /// Begin acquiring the source as soon as it becomes current.
23    #[default]
24    Eager,
25    /// Keep the source dormant until Playback is requested.
26    OnPlay,
27}
28
29/// The cross-origin request policy for one URL-addressable Playback Source alternative.
30#[derive(Clone, Copy, Debug, PartialEq, Eq)]
31#[non_exhaustive]
32pub enum PlaybackSourceCrossOrigin {
33    /// Fetch without credentials and require CORS authorization.
34    Anonymous,
35    /// Fetch with credentials and require credentialed CORS authorization.
36    UseCredentials,
37}
38
39/// One validated URL-addressable Playback Source alternative.
40#[derive(Clone, Debug, PartialEq, Eq)]
41pub struct PlaybackSourceAlternative {
42    url: String,
43    media_type: Option<String>,
44    cross_origin: Option<PlaybackSourceCrossOrigin>,
45}
46
47impl PlaybackSourceAlternative {
48    /// Validate a browser-resolvable URL reference.
49    ///
50    /// Relative references are accepted. Validation does not claim that the
51    /// resource exists or that the browser can decode it.
52    pub fn new(url: impl Into<String>) -> Result<Self, AudioError> {
53        let url = url.into();
54        if url.trim().is_empty() || url.chars().any(char::is_control) {
55            return Err(AudioError::new(
56                AudioErrorKind::InvalidConfiguration,
57                "Playback Source URL must be non-empty and contain no control characters",
58            ));
59        }
60
61        Ok(Self {
62            url,
63            media_type: None,
64            cross_origin: None,
65        })
66    }
67
68    /// Add an advisory media-type hint.
69    pub fn with_media_type(mut self, media_type: impl Into<String>) -> Result<Self, AudioError> {
70        let media_type = media_type.into();
71        if media_type.trim().is_empty() || media_type.chars().any(char::is_control) {
72            return Err(AudioError::new(
73                AudioErrorKind::InvalidConfiguration,
74                "Playback Source media type must be non-empty and contain no control characters",
75            ));
76        }
77        self.media_type = Some(media_type);
78        Ok(self)
79    }
80
81    pub fn url(&self) -> &str {
82        &self.url
83    }
84
85    pub fn media_type(&self) -> Option<&str> {
86        self.media_type.as_deref()
87    }
88
89    /// Configure how the browser requests this cross-origin alternative.
90    ///
91    /// Alternatives without a policy remain direct-only. Anonymous CORS is
92    /// required before a URL alternative can be attached to a Playback graph.
93    pub fn with_cross_origin(mut self, cross_origin: PlaybackSourceCrossOrigin) -> Self {
94        self.cross_origin = Some(cross_origin);
95        self
96    }
97
98    pub fn cross_origin(&self) -> Option<PlaybackSourceCrossOrigin> {
99        self.cross_origin
100    }
101
102    /// Whether this alternative declares the anonymous-CORS intent required
103    /// for graph-backed Playback.
104    pub fn is_graph_eligible(&self) -> bool {
105        self.cross_origin == Some(PlaybackSourceCrossOrigin::Anonymous)
106    }
107}
108
109#[derive(Clone, Debug, PartialEq, Eq)]
110enum PlaybackSourceInput {
111    AudioData(Arc<AudioData>),
112    Url(Arc<[PlaybackSourceAlternative]>),
113}
114
115/// One owned Playback input and its loading policy.
116#[derive(Clone, Debug, PartialEq, Eq)]
117pub struct PlaybackSource {
118    input: PlaybackSourceInput,
119    loading_policy: PlaybackLoadingPolicy,
120}
121
122impl PlaybackSource {
123    pub fn audio_data(audio: AudioData) -> Self {
124        Self {
125            input: PlaybackSourceInput::AudioData(Arc::new(audio)),
126            loading_policy: PlaybackLoadingPolicy::Eager,
127        }
128    }
129
130    pub fn url(alternative: PlaybackSourceAlternative) -> Self {
131        Self {
132            input: PlaybackSourceInput::Url(Arc::from([alternative])),
133            loading_policy: PlaybackLoadingPolicy::Eager,
134        }
135    }
136
137    /// Build a URL-backed Playback Source from ordered alternatives.
138    pub fn url_alternatives(
139        alternatives: impl IntoIterator<Item = PlaybackSourceAlternative>,
140    ) -> Result<Self, AudioError> {
141        let alternatives: Arc<[PlaybackSourceAlternative]> = alternatives.into_iter().collect();
142        if alternatives.is_empty() {
143            return Err(AudioError::new(
144                AudioErrorKind::InvalidConfiguration,
145                "a URL Playback Source must contain at least one alternative",
146            ));
147        }
148
149        Ok(Self {
150            input: PlaybackSourceInput::Url(alternatives),
151            loading_policy: PlaybackLoadingPolicy::Eager,
152        })
153    }
154
155    /// Return the ordered URL alternatives, or `None` for Audio Data.
156    pub fn alternatives(&self) -> Option<&[PlaybackSourceAlternative]> {
157        match &self.input {
158            PlaybackSourceInput::AudioData(_) => None,
159            PlaybackSourceInput::Url(alternatives) => Some(alternatives),
160        }
161    }
162
163    pub fn with_loading_policy(mut self, loading_policy: PlaybackLoadingPolicy) -> Self {
164        self.loading_policy = loading_policy;
165        self
166    }
167
168    pub fn loading_policy(&self) -> PlaybackLoadingPolicy {
169        self.loading_policy
170    }
171}
172
173impl From<AudioData> for PlaybackSource {
174    fn from(audio: AudioData) -> Self {
175        Self::audio_data(audio)
176    }
177}
178
179#[derive(Clone, Debug, PartialEq, Eq)]
180#[non_exhaustive]
181pub enum PlaybackStatus {
182    Empty,
183    Loading,
184    Ready,
185    Playing,
186    Paused,
187    Ended,
188    Failed(AudioError),
189}
190
191/// The lifecycle of the current Playback Source, independent of transport.
192#[derive(Clone, Debug, PartialEq, Eq)]
193#[non_exhaustive]
194pub enum PlaybackSourceLifecycle {
195    Empty,
196    Dormant,
197    Loading,
198    Playable,
199    Failed,
200}
201
202/// The requested or confirmed transport state of Playback.
203#[derive(Clone, Copy, Debug, PartialEq, Eq)]
204#[non_exhaustive]
205pub enum PlaybackTransport {
206    Idle,
207    PlayPending,
208    Playing,
209    Paused,
210    Ended,
211}
212
213/// How ready the current source is to advance Playback.
214#[derive(Clone, Copy, Debug, PartialEq, Eq)]
215#[non_exhaustive]
216pub enum PlaybackReadiness {
217    Unavailable,
218    LoadingMetadata,
219    Metadata,
220    Playable,
221    Waiting,
222}
223
224/// Coarse network activity for the current Playback Source attempt.
225#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
226#[non_exhaustive]
227pub enum PlaybackNetworkActivity {
228    /// No URL-addressable Playback Source is attached.
229    #[default]
230    Inactive,
231    /// A URL-addressable Playback Source is attached but has no reported activity yet.
232    Unknown,
233    /// The browser reports that it is acquiring media data.
234    Loading,
235    /// The browser is not currently acquiring media data.
236    Idle,
237    /// Media acquisition has stopped unexpectedly without becoming a terminal failure.
238    Stalled,
239}
240
241/// One immutable half-open source-time observation.
242///
243/// Buffered and seekable ranges are UI guidance from the browser. They may
244/// disappear or shrink and do not guarantee that a future seek will succeed.
245#[derive(Clone, Copy, Debug, PartialEq, Eq)]
246pub struct PlaybackTimeRange {
247    start: Duration,
248    end: Duration,
249}
250
251impl PlaybackTimeRange {
252    pub fn new(start: Duration, end: Duration) -> Result<Self, PlaybackCommandError> {
253        if end <= start {
254            return Err(PlaybackCommandError(
255                "Playback time range end must be after its start",
256            ));
257        }
258
259        Ok(Self { start, end })
260    }
261
262    pub fn start(self) -> Duration {
263        self.start
264    }
265
266    pub fn end(self) -> Duration {
267        self.end
268    }
269}
270
271/// A portable terminal failure of the current Playback Source.
272#[derive(Clone, Debug, PartialEq, Eq)]
273#[non_exhaustive]
274pub enum PlaybackSourceFailure {
275    /// The source cannot participate in an owner-requested Playback graph.
276    GraphIneligible(AudioError),
277    Unsupported(AudioError),
278    Network(AudioError),
279    Decode(AudioError),
280    Unknown(AudioError),
281}
282
283impl PlaybackSourceFailure {
284    pub fn error(&self) -> &AudioError {
285        match self {
286            Self::GraphIneligible(error)
287            | Self::Unsupported(error)
288            | Self::Network(error)
289            | Self::Decode(error)
290            | Self::Unknown(error) => error,
291        }
292    }
293
294    pub fn kind(&self) -> PlaybackSourceFailureKind {
295        match self {
296            Self::GraphIneligible(_) => PlaybackSourceFailureKind::GraphIneligible,
297            Self::Unsupported(_) => PlaybackSourceFailureKind::Unsupported,
298            Self::Network(_) => PlaybackSourceFailureKind::Network,
299            Self::Decode(_) => PlaybackSourceFailureKind::Decode,
300            Self::Unknown(_) => PlaybackSourceFailureKind::Unknown,
301        }
302    }
303}
304
305/// A portable kind for one URL alternative's initial load failure.
306#[derive(Clone, Copy, Debug, PartialEq, Eq)]
307#[non_exhaustive]
308pub enum PlaybackSourceFailureKind {
309    GraphIneligible,
310    Unsupported,
311    Network,
312    Decode,
313    Unknown,
314}
315
316/// The coarse failure of one URL alternative during initial selection.
317#[derive(Clone, Debug, PartialEq, Eq)]
318pub struct PlaybackAlternativeFailure {
319    alternative: PlaybackSourceAlternative,
320    kind: PlaybackSourceFailureKind,
321}
322
323impl PlaybackAlternativeFailure {
324    #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
325    fn new(alternative: PlaybackSourceAlternative, failure: &PlaybackSourceFailure) -> Self {
326        Self {
327            alternative,
328            kind: failure.kind(),
329        }
330    }
331
332    pub fn alternative(&self) -> &PlaybackSourceAlternative {
333        &self.alternative
334    }
335
336    pub fn kind(&self) -> PlaybackSourceFailureKind {
337        self.kind
338    }
339}
340
341/// A play request failure that leaves the current source usable for retry.
342#[derive(Clone, Debug, PartialEq, Eq)]
343#[non_exhaustive]
344pub enum PlaybackPlayFailure {
345    InteractionRequired(AudioError),
346    Unknown(AudioError),
347}
348
349/// The mode of one Bounded Playback operation.
350#[derive(Clone, Copy, Debug, PartialEq, Eq)]
351#[non_exhaustive]
352pub enum BoundedPlaybackMode {
353    /// Play the selected source-time range once.
354    Once,
355    /// Repeatedly play the selected source-time range.
356    Loop,
357}
358
359/// A failure scoped to one Bounded Playback operation.
360#[derive(Clone, Copy, Debug, PartialEq, Eq)]
361#[non_exhaustive]
362pub enum BoundedPlaybackFailure {
363    /// The browser did not confirm the seek to the selected start.
364    SeekTimedOut,
365    /// The browser rejected activation after the bounded seek completed.
366    ActivationRejected,
367    /// The browser rejected a pause required by the bounded operation.
368    PauseRejected,
369}
370
371/// The observable lifecycle of one Bounded Playback operation.
372#[derive(Clone, Copy, Debug, PartialEq, Eq)]
373#[non_exhaustive]
374pub enum BoundedPlaybackPhase {
375    /// Playback is paused while seeking to the selected start.
376    Seeking,
377    /// The seek completed and Playback is awaiting browser play confirmation.
378    Activating,
379    /// The browser confirmed Playback and the selected end is being enforced.
380    Active,
381    /// Playback is paused while retaining the selected range for resume.
382    Paused,
383    /// A committed selection edit requires a seek to the new range start.
384    Retargeting,
385    /// A loop boundary is seeking back to the selected range start.
386    Wrapping,
387    /// The one-shot operation stopped and clamped observable position to its end.
388    Completed,
389    /// The application explicitly cancelled the bounded operation.
390    Cancelled,
391    /// The bounded operation failed without failing its Playback Source.
392    Failed(BoundedPlaybackFailure),
393}
394
395/// A discrete Bounded Playback transition suitable for accessible presentation.
396#[derive(Clone, Copy, Debug, PartialEq, Eq)]
397#[non_exhaustive]
398pub enum BoundedPlaybackEvent {
399    Started(BoundedPlaybackMode),
400    Completed,
401    Cancelled,
402    Wrapping,
403    Failed,
404}
405
406/// The Controller action required after atomically replacing a bounded range.
407#[derive(Clone, Copy, Debug, PartialEq, Eq)]
408#[non_exhaustive]
409pub enum BoundedPlaybackRetarget {
410    PreservePosition,
411    SeekToStart,
412}
413
414impl BoundedPlaybackPhase {
415    /// Whether this phase still owns future Playback transport behavior.
416    pub fn is_enforcing(self) -> bool {
417        matches!(
418            self,
419            Self::Seeking
420                | Self::Activating
421                | Self::Active
422                | Self::Paused
423                | Self::Retargeting
424                | Self::Wrapping
425        )
426    }
427}
428
429/// One coherent observation of a Controller-owned Bounded Playback operation.
430#[derive(Clone, Copy, Debug, PartialEq, Eq)]
431pub struct BoundedPlaybackSnapshot {
432    pub range: WaveformSelection,
433    pub mode: BoundedPlaybackMode,
434    pub phase: BoundedPlaybackPhase,
435}
436
437impl PlaybackPlayFailure {
438    pub fn error(&self) -> &AudioError {
439        match self {
440            Self::InteractionRequired(error) | Self::Unknown(error) => error,
441        }
442    }
443}
444
445/// How directly setting Playback's audibility level affects output.
446#[derive(Clone, Copy, Debug, PartialEq, Eq)]
447#[non_exhaustive]
448pub enum PlaybackAudibilityCapability {
449    /// An owned Playback graph applies effective gain.
450    EffectiveGraphGain,
451    /// The media element accepts the level, but some browsers may not apply it audibly.
452    BestEffortMediaElement,
453    /// This Playback owner cannot set an audibility level.
454    Unavailable,
455}
456
457/// The state of an opt-in owner-lifetime Playback graph.
458#[derive(Clone, Copy, Debug, PartialEq, Eq)]
459#[non_exhaustive]
460pub enum PlaybackGraphState {
461    /// This Playback owner uses ordinary direct media playback.
462    NotRequested,
463    /// The graph is waiting for an eligible Playback Source.
464    AwaitingSource,
465    /// The graph is being created or attached to the current source.
466    Preparing,
467    /// The graph exists but its audio context is suspended.
468    Suspended,
469    /// The graph is running with the current source attached.
470    Running,
471    /// Playback needs another user interaction before graph output can resume.
472    InteractionRequired,
473    /// Graph setup failed permanently for this owner; transport degraded to direct Playback.
474    Unavailable,
475}
476
477/// Immutable configuration for one Playback owner.
478#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
479pub struct PlaybackOptions {
480    graph_backed: bool,
481}
482
483impl PlaybackOptions {
484    /// Request owner-lifetime graph-backed Playback for eligible Playback Sources.
485    pub const fn graph_backed() -> Self {
486        Self { graph_backed: true }
487    }
488}
489
490/// A finite normalized Playback audibility preference in the inclusive range `0.0..=1.0`.
491#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
492pub struct PlaybackAudibilityLevel(f64);
493
494impl PlaybackAudibilityLevel {
495    pub const SILENT: Self = Self(0.0);
496    pub const FULL: Self = Self(1.0);
497
498    /// Validate a normalized audibility level.
499    pub fn new(value: f64) -> Result<Self, PlaybackCommandError> {
500        if !value.is_finite() || !(0.0..=1.0).contains(&value) {
501            return Err(PlaybackCommandError(
502                "audibility level must be finite and between 0 and 1",
503            ));
504        }
505
506        Ok(Self(value))
507    }
508
509    pub fn value(self) -> f64 {
510        self.0
511    }
512}
513
514impl Default for PlaybackAudibilityLevel {
515    fn default() -> Self {
516        Self::FULL
517    }
518}
519
520// Construction excludes NaN, so the value has reflexive equality.
521impl Eq for PlaybackAudibilityLevel {}
522
523/// One coherent observation of Playback's independent state facets.
524#[derive(Clone, Debug, PartialEq, Eq)]
525#[non_exhaustive]
526pub struct PlaybackSnapshot {
527    pub source: PlaybackSourceLifecycle,
528    pub transport: PlaybackTransport,
529    pub readiness: PlaybackReadiness,
530    /// Network activity observed independently from readiness and transport.
531    pub network: PlaybackNetworkActivity,
532    /// Normalized buffered observations for the current source attempt.
533    pub buffered: Arc<[PlaybackTimeRange]>,
534    /// Normalized seekable observations for the current source attempt.
535    pub seekable: Arc<[PlaybackTimeRange]>,
536    /// The selected URL alternative, if the current source is URL-addressable.
537    pub selected_alternative: Option<PlaybackSourceAlternative>,
538    /// A terminal failure, separate from recoverable play-policy rejection.
539    pub source_failure: Option<PlaybackSourceFailure>,
540    /// Ordered initial URL failures, populated when no alternative becomes playable.
541    pub alternative_failures: Arc<[PlaybackAlternativeFailure]>,
542    pub play_failure: Option<PlaybackPlayFailure>,
543    /// A one-operation source-time bound, independent from ordinary Playback state.
544    pub bounded: Option<BoundedPlaybackSnapshot>,
545    /// The latest discrete Bounded Playback transition, cleared by its next steady phase.
546    pub bounded_event: Option<BoundedPlaybackEvent>,
547    /// Whole-source repeat preference, retained across source replacement and unload.
548    pub repeat: bool,
549    /// Mute preference, retained independently from transport and audibility level.
550    pub muted: bool,
551    /// Normalized audibility preference, retained across source replacement and unload.
552    pub audibility_level: PlaybackAudibilityLevel,
553    /// The effectiveness contract for setting [`Self::audibility_level`].
554    pub audibility_capability: PlaybackAudibilityCapability,
555    /// Owner-lifetime graph state, independent from source and transport state.
556    pub graph: PlaybackGraphState,
557}
558
559impl Default for PlaybackSnapshot {
560    fn default() -> Self {
561        Self {
562            source: PlaybackSourceLifecycle::Empty,
563            transport: PlaybackTransport::Idle,
564            readiness: PlaybackReadiness::Unavailable,
565            network: PlaybackNetworkActivity::Inactive,
566            buffered: Arc::from([]),
567            seekable: Arc::from([]),
568            selected_alternative: None,
569            source_failure: None,
570            alternative_failures: Arc::from([]),
571            play_failure: None,
572            bounded: None,
573            bounded_event: None,
574            repeat: false,
575            muted: false,
576            audibility_level: PlaybackAudibilityLevel::FULL,
577            audibility_capability: PlaybackAudibilityCapability::BestEffortMediaElement,
578            graph: PlaybackGraphState::NotRequested,
579        }
580    }
581}
582
583#[derive(Clone, Debug, PartialEq, Eq)]
584pub struct PlaybackCommandError(&'static str);
585
586impl fmt::Display for PlaybackCommandError {
587    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
588        formatter.write_str(self.0)
589    }
590}
591
592impl std::error::Error for PlaybackCommandError {}
593
594#[derive(Debug)]
595pub struct PlaybackLifecycle {
596    status: PlaybackStatus,
597    snapshot: PlaybackSnapshot,
598    graph_backed: bool,
599}
600
601impl Default for PlaybackLifecycle {
602    fn default() -> Self {
603        Self::new(PlaybackOptions::default())
604    }
605}
606
607impl PlaybackLifecycle {
608    pub fn new(options: PlaybackOptions) -> Self {
609        let mut snapshot = PlaybackSnapshot::default();
610        if options.graph_backed {
611            snapshot.graph = PlaybackGraphState::AwaitingSource;
612            snapshot.audibility_capability = PlaybackAudibilityCapability::EffectiveGraphGain;
613        }
614        Self {
615            status: PlaybackStatus::Empty,
616            snapshot,
617            graph_backed: options.graph_backed,
618        }
619    }
620
621    pub fn status(&self) -> &PlaybackStatus {
622        &self.status
623    }
624
625    pub fn snapshot(&self) -> &PlaybackSnapshot {
626        &self.snapshot
627    }
628
629    pub fn source(&self) -> &PlaybackSourceLifecycle {
630        &self.snapshot.source
631    }
632
633    pub fn transport(&self) -> PlaybackTransport {
634        self.snapshot.transport
635    }
636
637    pub fn readiness(&self) -> PlaybackReadiness {
638        self.snapshot.readiness
639    }
640
641    pub fn network_activity(&self) -> PlaybackNetworkActivity {
642        self.snapshot.network
643    }
644
645    pub fn play_failure(&self) -> Option<&PlaybackPlayFailure> {
646        self.snapshot.play_failure.as_ref()
647    }
648
649    pub fn bounded(&self) -> Option<&BoundedPlaybackSnapshot> {
650        self.snapshot.bounded.as_ref()
651    }
652
653    /// Begin one Bounded Playback run after validating its authoritative timeline.
654    pub fn start_bounded_once(
655        &mut self,
656        range: WaveformSelection,
657        duration_secs: f64,
658    ) -> Result<(), PlaybackCommandError> {
659        self.start_bounded(range, duration_secs, BoundedPlaybackMode::Once)
660    }
661
662    /// Begin one repeating Bounded Playback run after validating its authoritative timeline.
663    pub fn start_bounded_loop(
664        &mut self,
665        range: WaveformSelection,
666        duration_secs: f64,
667    ) -> Result<(), PlaybackCommandError> {
668        self.start_bounded(range, duration_secs, BoundedPlaybackMode::Loop)
669    }
670
671    fn start_bounded(
672        &mut self,
673        range: WaveformSelection,
674        duration_secs: f64,
675        mode: BoundedPlaybackMode,
676    ) -> Result<(), PlaybackCommandError> {
677        self.validate_bounded_range(range, duration_secs)?;
678
679        self.snapshot.bounded = Some(BoundedPlaybackSnapshot {
680            range,
681            mode,
682            phase: BoundedPlaybackPhase::Seeking,
683        });
684        self.snapshot.bounded_event = Some(BoundedPlaybackEvent::Started(mode));
685        Ok(())
686    }
687
688    fn validate_bounded_range(
689        &self,
690        range: WaveformSelection,
691        duration_secs: f64,
692    ) -> Result<(), PlaybackCommandError> {
693        if self.snapshot.source != PlaybackSourceLifecycle::Playable {
694            return Err(PlaybackCommandError("audio is not loaded"));
695        }
696        if !duration_secs.is_finite() || duration_secs <= 0.0 {
697            return Err(PlaybackCommandError(
698                "authoritative audio duration is unavailable",
699            ));
700        }
701        if range.is_collapsed() {
702            return Err(PlaybackCommandError("Waveform Selection is collapsed"));
703        }
704        if !range.is_playable_within(duration_secs) {
705            return Err(PlaybackCommandError(
706                "Waveform Selection is outside the Playback duration",
707            ));
708        }
709        Ok(())
710    }
711
712    /// Atomically replace the range of an enforcing Bounded Playback operation.
713    ///
714    /// Returns whether the platform owner can preserve `current_position` or
715    /// must pause and seek. Out-of-range retargets enter
716    /// [`BoundedPlaybackPhase::Retargeting`] before optionally resuming.
717    pub fn retarget_bounded(
718        &mut self,
719        range: WaveformSelection,
720        duration_secs: f64,
721        current_position: f64,
722    ) -> Result<BoundedPlaybackRetarget, PlaybackCommandError> {
723        let previous = self
724            .snapshot
725            .bounded
726            .filter(|bounded| bounded.phase.is_enforcing())
727            .ok_or(PlaybackCommandError("Bounded Playback is not active"))?;
728        self.validate_bounded_range(range, duration_secs)?;
729
730        let preserved = current_position.is_finite()
731            && current_position >= range.start()
732            && current_position < range.end();
733        let phase = if preserved {
734            match self.snapshot.transport {
735                PlaybackTransport::Playing => BoundedPlaybackPhase::Active,
736                PlaybackTransport::PlayPending => BoundedPlaybackPhase::Activating,
737                _ => BoundedPlaybackPhase::Paused,
738            }
739        } else {
740            BoundedPlaybackPhase::Retargeting
741        };
742        self.snapshot.bounded = Some(BoundedPlaybackSnapshot {
743            range,
744            mode: previous.mode,
745            phase,
746        });
747        self.snapshot.bounded_event = None;
748        Ok(if preserved {
749            BoundedPlaybackRetarget::PreservePosition
750        } else {
751            BoundedPlaybackRetarget::SeekToStart
752        })
753    }
754
755    pub fn bounded_seeking(&mut self) {
756        if let Some(bounded) = self.snapshot.bounded.as_mut()
757            && bounded.phase.is_enforcing()
758        {
759            bounded.phase = BoundedPlaybackPhase::Seeking;
760        }
761    }
762
763    pub fn bounded_activating(&mut self) {
764        if let Some(bounded) = self.snapshot.bounded.as_mut()
765            && bounded.phase.is_enforcing()
766        {
767            bounded.phase = BoundedPlaybackPhase::Activating;
768            self.snapshot.bounded_event = None;
769        }
770    }
771
772    pub fn bounded_active(&mut self) {
773        if let Some(bounded) = self.snapshot.bounded.as_mut()
774            && bounded.phase == BoundedPlaybackPhase::Activating
775        {
776            bounded.phase = BoundedPlaybackPhase::Active;
777            self.snapshot.bounded_event = None;
778        }
779    }
780
781    pub fn bounded_paused(&mut self) {
782        if let Some(bounded) = self.snapshot.bounded.as_mut()
783            && bounded.phase.is_enforcing()
784        {
785            bounded.phase = BoundedPlaybackPhase::Paused;
786            self.snapshot.bounded_event = None;
787        }
788    }
789
790    pub fn bounded_retargeting(&mut self) {
791        if let Some(bounded) = self.snapshot.bounded.as_mut()
792            && bounded.phase.is_enforcing()
793        {
794            bounded.phase = BoundedPlaybackPhase::Retargeting;
795            self.snapshot.bounded_event = None;
796        }
797    }
798
799    pub fn bounded_wrapping(&mut self) {
800        if let Some(bounded) = self.snapshot.bounded.as_mut()
801            && bounded.mode == BoundedPlaybackMode::Loop
802            && bounded.phase.is_enforcing()
803        {
804            bounded.phase = BoundedPlaybackPhase::Wrapping;
805            self.snapshot.bounded_event = Some(BoundedPlaybackEvent::Wrapping);
806        }
807    }
808
809    pub fn bounded_completed(&mut self) {
810        if let Some(bounded) = self.snapshot.bounded.as_mut()
811            && bounded.mode == BoundedPlaybackMode::Once
812            && bounded.phase.is_enforcing()
813        {
814            bounded.phase = BoundedPlaybackPhase::Completed;
815            self.status = PlaybackStatus::Paused;
816            self.snapshot.transport = PlaybackTransport::Paused;
817            self.snapshot.play_failure = None;
818            self.snapshot.bounded_event = Some(BoundedPlaybackEvent::Completed);
819        }
820    }
821
822    pub fn bounded_failed(&mut self, failure: BoundedPlaybackFailure) {
823        if let Some(bounded) = self.snapshot.bounded.as_mut()
824            && bounded.phase.is_enforcing()
825        {
826            bounded.phase = BoundedPlaybackPhase::Failed(failure);
827            if self.snapshot.transport != PlaybackTransport::Playing {
828                self.snapshot.transport = PlaybackTransport::Paused;
829                self.status = PlaybackStatus::Paused;
830            }
831            self.snapshot.play_failure = None;
832            self.snapshot.bounded_event = Some(BoundedPlaybackEvent::Failed);
833        }
834    }
835
836    pub fn cancel_bounded(&mut self) {
837        let cancelled = self
838            .snapshot
839            .bounded
840            .is_some_and(|bounded| bounded.phase.is_enforcing());
841        self.snapshot.bounded = None;
842        self.snapshot.bounded_event = cancelled.then_some(BoundedPlaybackEvent::Cancelled);
843    }
844
845    pub fn bounded_cancelled(&mut self) {
846        if let Some(bounded) = self.snapshot.bounded.as_mut()
847            && bounded.phase.is_enforcing()
848        {
849            bounded.phase = BoundedPlaybackPhase::Cancelled;
850            self.snapshot.bounded_event = Some(BoundedPlaybackEvent::Cancelled);
851        }
852    }
853
854    pub fn selected_alternative(&self) -> Option<&PlaybackSourceAlternative> {
855        self.snapshot.selected_alternative.as_ref()
856    }
857
858    pub fn source_failure(&self) -> Option<&PlaybackSourceFailure> {
859        self.snapshot.source_failure.as_ref()
860    }
861
862    pub fn repeat(&self) -> bool {
863        self.snapshot.repeat
864    }
865
866    pub fn set_repeat(&mut self, repeat: bool) {
867        self.snapshot.repeat = repeat;
868    }
869
870    pub fn toggle_repeat(&mut self) {
871        self.snapshot.repeat = !self.snapshot.repeat;
872    }
873
874    pub fn muted(&self) -> bool {
875        self.snapshot.muted
876    }
877
878    pub fn set_muted(&mut self, muted: bool) {
879        self.snapshot.muted = muted;
880    }
881
882    pub fn toggle_muted(&mut self) {
883        self.snapshot.muted = !self.snapshot.muted;
884    }
885
886    pub fn audibility_level(&self) -> PlaybackAudibilityLevel {
887        self.snapshot.audibility_level
888    }
889
890    pub fn set_audibility_level(&mut self, level: f64) -> Result<(), PlaybackCommandError> {
891        self.set_validated_audibility_level(PlaybackAudibilityLevel::new(level)?);
892        Ok(())
893    }
894
895    fn set_validated_audibility_level(&mut self, level: PlaybackAudibilityLevel) {
896        self.snapshot.audibility_level = level;
897    }
898
899    pub fn audibility_capability(&self) -> PlaybackAudibilityCapability {
900        self.snapshot.audibility_capability
901    }
902
903    pub fn graph_state(&self) -> PlaybackGraphState {
904        self.snapshot.graph
905    }
906
907    pub fn graph_preparing(&mut self) {
908        if self.graph_backed && self.snapshot.graph != PlaybackGraphState::Unavailable {
909            self.snapshot.graph = PlaybackGraphState::Preparing;
910            self.snapshot.audibility_capability = PlaybackAudibilityCapability::EffectiveGraphGain;
911        }
912    }
913
914    pub fn graph_awaiting_source(&mut self) {
915        if self.graph_backed && self.snapshot.graph != PlaybackGraphState::Unavailable {
916            self.snapshot.graph = PlaybackGraphState::AwaitingSource;
917            self.snapshot.audibility_capability = PlaybackAudibilityCapability::EffectiveGraphGain;
918        }
919    }
920
921    pub fn direct_audibility(&mut self) {
922        if self.snapshot.graph != PlaybackGraphState::Unavailable {
923            self.snapshot.audibility_capability =
924                PlaybackAudibilityCapability::BestEffortMediaElement;
925        }
926    }
927
928    pub fn graph_suspended(&mut self) {
929        if self.graph_backed
930            && !matches!(
931                self.snapshot.graph,
932                PlaybackGraphState::InteractionRequired | PlaybackGraphState::Unavailable
933            )
934        {
935            self.snapshot.graph = PlaybackGraphState::Suspended;
936        }
937    }
938
939    pub fn graph_running(&mut self) {
940        if self.graph_backed
941            && !matches!(
942                self.snapshot.graph,
943                PlaybackGraphState::InteractionRequired | PlaybackGraphState::Unavailable
944            )
945        {
946            self.snapshot.graph = PlaybackGraphState::Running;
947        }
948    }
949
950    pub fn graph_interaction_required(&mut self, error: AudioError) {
951        if !self.graph_backed
952            || self.snapshot.graph == PlaybackGraphState::Unavailable
953            || !matches!(
954                self.snapshot.source,
955                PlaybackSourceLifecycle::Loading | PlaybackSourceLifecycle::Playable
956            )
957            || !matches!(
958                self.snapshot.transport,
959                PlaybackTransport::PlayPending | PlaybackTransport::Playing
960            )
961        {
962            return;
963        }
964
965        self.status = PlaybackStatus::Failed(error.clone());
966        self.snapshot.transport = if self.snapshot.source == PlaybackSourceLifecycle::Loading {
967            PlaybackTransport::Idle
968        } else {
969            PlaybackTransport::Paused
970        };
971        if self.snapshot.readiness == PlaybackReadiness::Waiting {
972            self.snapshot.readiness = PlaybackReadiness::Metadata;
973        }
974        self.snapshot.play_failure = Some(PlaybackPlayFailure::InteractionRequired(error));
975        self.snapshot.graph = PlaybackGraphState::InteractionRequired;
976    }
977
978    pub fn graph_unavailable(&mut self) {
979        if self.graph_backed {
980            self.snapshot.graph = PlaybackGraphState::Unavailable;
981            self.snapshot.audibility_capability =
982                PlaybackAudibilityCapability::BestEffortMediaElement;
983        }
984    }
985
986    fn clear_source_failures(&mut self) {
987        self.snapshot.source_failure = None;
988        self.snapshot.alternative_failures = Arc::from([]);
989    }
990
991    fn clear_source_observations(&mut self) {
992        self.snapshot.selected_alternative = None;
993        self.clear_source_failures();
994        self.snapshot.play_failure = None;
995        self.clear_attempt_observations();
996    }
997
998    fn clear_attempt_observations(&mut self) {
999        self.snapshot.network = PlaybackNetworkActivity::Inactive;
1000        self.snapshot.buffered = Arc::from([]);
1001        self.snapshot.seekable = Arc::from([]);
1002    }
1003
1004    fn clear_bounded_for_lifecycle_change(&mut self) {
1005        if let Some(bounded) = self.snapshot.bounded.take() {
1006            self.snapshot.bounded_event = bounded
1007                .phase
1008                .is_enforcing()
1009                .then_some(BoundedPlaybackEvent::Cancelled);
1010        }
1011    }
1012
1013    pub fn loading(&mut self) {
1014        self.status = PlaybackStatus::Loading;
1015        self.snapshot.source = PlaybackSourceLifecycle::Loading;
1016        self.snapshot.transport = PlaybackTransport::Idle;
1017        self.snapshot.readiness = PlaybackReadiness::LoadingMetadata;
1018        self.clear_bounded_for_lifecycle_change();
1019        self.clear_source_observations();
1020    }
1021
1022    pub fn dormant(&mut self) {
1023        self.status = PlaybackStatus::Ready;
1024        self.snapshot.source = PlaybackSourceLifecycle::Dormant;
1025        self.snapshot.transport = PlaybackTransport::Idle;
1026        self.snapshot.readiness = PlaybackReadiness::Unavailable;
1027        self.clear_bounded_for_lifecycle_change();
1028        self.clear_source_observations();
1029    }
1030
1031    /// Report Playback as ready unless a rejected play attempt is still
1032    /// awaiting a retry.
1033    ///
1034    /// A play attempt may be rejected while its Playback Source is still
1035    /// loading, so the attempt that becomes playable afterwards must report
1036    /// the rejection rather than replace it.
1037    fn mark_ready_unless_play_rejected(&mut self) {
1038        self.status = match self.snapshot.play_failure.as_ref() {
1039            Some(failure) => PlaybackStatus::Failed(failure.error().clone()),
1040            None => PlaybackStatus::Ready,
1041        };
1042    }
1043
1044    pub fn loaded(&mut self) {
1045        self.mark_ready_unless_play_rejected();
1046        self.snapshot.source = PlaybackSourceLifecycle::Playable;
1047        self.snapshot.readiness = PlaybackReadiness::Metadata;
1048        self.snapshot.bounded_event = None;
1049        self.clear_source_failures();
1050    }
1051
1052    pub fn metadata_loaded(&mut self) {
1053        if self.snapshot.source == PlaybackSourceLifecycle::Loading {
1054            self.snapshot.readiness = PlaybackReadiness::Metadata;
1055        }
1056    }
1057
1058    #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
1059    fn loading_alternative(&mut self) {
1060        if self.snapshot.source == PlaybackSourceLifecycle::Loading {
1061            self.status = PlaybackStatus::Loading;
1062            self.snapshot.readiness = PlaybackReadiness::LoadingMetadata;
1063            self.clear_source_failures();
1064            self.clear_attempt_observations();
1065        }
1066    }
1067
1068    #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
1069    fn source_attempt_started(&mut self, url_addressable: bool) {
1070        if self.snapshot.source == PlaybackSourceLifecycle::Loading {
1071            self.snapshot.network = if url_addressable {
1072                PlaybackNetworkActivity::Unknown
1073            } else {
1074                PlaybackNetworkActivity::Inactive
1075            };
1076            self.snapshot.buffered = Arc::from([]);
1077            self.snapshot.seekable = Arc::from([]);
1078        }
1079    }
1080
1081    pub fn url_playable(&mut self, alternative: PlaybackSourceAlternative) {
1082        if self.snapshot.source == PlaybackSourceLifecycle::Loading {
1083            self.mark_ready_unless_play_rejected();
1084            self.snapshot.source = PlaybackSourceLifecycle::Playable;
1085            self.snapshot.readiness = PlaybackReadiness::Playable;
1086            self.snapshot.selected_alternative = Some(alternative);
1087            self.snapshot.bounded_event = None;
1088            self.clear_source_failures();
1089        }
1090    }
1091
1092    pub fn request_play(&mut self) -> Result<(), PlaybackCommandError> {
1093        if !matches!(
1094            self.snapshot.transport,
1095            PlaybackTransport::Idle | PlaybackTransport::Paused | PlaybackTransport::Ended
1096        ) {
1097            return Err(PlaybackCommandError("audio is not ready to play"));
1098        }
1099
1100        match self.snapshot.source {
1101            PlaybackSourceLifecycle::Dormant => {
1102                self.status = PlaybackStatus::Loading;
1103                self.snapshot.source = PlaybackSourceLifecycle::Loading;
1104                self.snapshot.readiness = PlaybackReadiness::LoadingMetadata;
1105            }
1106            PlaybackSourceLifecycle::Loading => {
1107                self.status = PlaybackStatus::Loading;
1108            }
1109            PlaybackSourceLifecycle::Playable => {
1110                if self.snapshot.play_failure.is_some() {
1111                    self.status = PlaybackStatus::Ready;
1112                }
1113            }
1114            PlaybackSourceLifecycle::Empty | PlaybackSourceLifecycle::Failed => {
1115                return Err(PlaybackCommandError("audio is not ready to play"));
1116            }
1117        }
1118        self.snapshot.play_failure = None;
1119        self.snapshot.bounded_event = None;
1120        if self.snapshot.graph == PlaybackGraphState::InteractionRequired {
1121            self.snapshot.graph = PlaybackGraphState::Suspended;
1122        }
1123        self.snapshot.transport = PlaybackTransport::PlayPending;
1124        Ok(())
1125    }
1126
1127    pub fn playing(&mut self) {
1128        if !matches!(self.snapshot.source, PlaybackSourceLifecycle::Playable)
1129            || !matches!(
1130                self.snapshot.transport,
1131                PlaybackTransport::PlayPending | PlaybackTransport::Playing
1132            )
1133        {
1134            return;
1135        }
1136        self.status = PlaybackStatus::Playing;
1137        self.snapshot.transport = PlaybackTransport::Playing;
1138        self.snapshot.readiness = PlaybackReadiness::Playable;
1139        self.snapshot.play_failure = None;
1140        self.snapshot.bounded_event = None;
1141    }
1142
1143    pub fn waiting(&mut self) {
1144        if matches!(self.snapshot.source, PlaybackSourceLifecycle::Playable)
1145            && matches!(
1146                self.snapshot.transport,
1147                PlaybackTransport::PlayPending | PlaybackTransport::Playing
1148            )
1149        {
1150            self.snapshot.readiness = PlaybackReadiness::Waiting;
1151        }
1152    }
1153
1154    pub fn playable(&mut self) {
1155        if matches!(self.snapshot.source, PlaybackSourceLifecycle::Playable) {
1156            self.snapshot.readiness = PlaybackReadiness::Playable;
1157        }
1158    }
1159
1160    pub fn network_observed(&mut self, activity: PlaybackNetworkActivity) {
1161        if matches!(
1162            self.snapshot.source,
1163            PlaybackSourceLifecycle::Loading | PlaybackSourceLifecycle::Playable
1164        ) {
1165            self.snapshot.network = activity;
1166        }
1167    }
1168
1169    /// Replace buffered and seekable observations for the current source attempt.
1170    ///
1171    /// Ranges are sorted and overlapping or touching ranges are merged. A later
1172    /// observation replaces the whole snapshot, so either collection may shrink.
1173    pub fn ranges_changed(
1174        &mut self,
1175        buffered: impl IntoIterator<Item = PlaybackTimeRange>,
1176        seekable: impl IntoIterator<Item = PlaybackTimeRange>,
1177    ) {
1178        if matches!(
1179            self.snapshot.source,
1180            PlaybackSourceLifecycle::Loading | PlaybackSourceLifecycle::Playable
1181        ) {
1182            self.snapshot.buffered = normalize_time_ranges(buffered);
1183            self.snapshot.seekable = normalize_time_ranges(seekable);
1184        }
1185    }
1186
1187    pub fn paused(&mut self) {
1188        if self.snapshot.bounded.is_none() {
1189            self.snapshot.bounded_event = None;
1190        }
1191        if matches!(
1192            self.snapshot.transport,
1193            PlaybackTransport::PlayPending | PlaybackTransport::Playing
1194        ) {
1195            if self.snapshot.source == PlaybackSourceLifecycle::Loading {
1196                self.snapshot.transport = PlaybackTransport::Idle;
1197                self.status = PlaybackStatus::Loading;
1198            } else {
1199                self.snapshot.transport = PlaybackTransport::Paused;
1200                self.status = PlaybackStatus::Paused;
1201            }
1202        }
1203    }
1204
1205    /// Return a loaded source to its ready, idle state.
1206    pub fn stop(&mut self) -> Result<(), PlaybackCommandError> {
1207        if !matches!(self.snapshot.source, PlaybackSourceLifecycle::Playable) {
1208            return Err(PlaybackCommandError("audio is not loaded"));
1209        }
1210
1211        self.status = PlaybackStatus::Ready;
1212        self.snapshot.transport = PlaybackTransport::Idle;
1213        self.snapshot.play_failure = None;
1214        self.clear_bounded_for_lifecycle_change();
1215        Ok(())
1216    }
1217
1218    pub fn ended(&mut self) {
1219        if self.snapshot.transport != PlaybackTransport::Playing {
1220            return;
1221        }
1222        self.status = PlaybackStatus::Ended;
1223        self.snapshot.transport = PlaybackTransport::Ended;
1224        self.snapshot.readiness = PlaybackReadiness::Playable;
1225    }
1226
1227    pub fn play_rejected(&mut self, failure: PlaybackPlayFailure) {
1228        if !matches!(
1229            self.snapshot.source,
1230            PlaybackSourceLifecycle::Loading | PlaybackSourceLifecycle::Playable
1231        ) || !matches!(self.snapshot.transport, PlaybackTransport::PlayPending)
1232        {
1233            return;
1234        }
1235
1236        self.status = PlaybackStatus::Failed(failure.error().clone());
1237        self.snapshot.transport = if self.snapshot.source == PlaybackSourceLifecycle::Loading {
1238            PlaybackTransport::Idle
1239        } else {
1240            PlaybackTransport::Paused
1241        };
1242        if self.snapshot.readiness == PlaybackReadiness::Waiting {
1243            self.snapshot.readiness = PlaybackReadiness::Metadata;
1244        }
1245        self.snapshot.play_failure = Some(failure);
1246    }
1247
1248    pub fn seeked(&mut self, position: f64, duration: f64) {
1249        if matches!(
1250            self.status,
1251            PlaybackStatus::Empty | PlaybackStatus::Loading | PlaybackStatus::Failed(_)
1252        ) {
1253            return;
1254        }
1255        if duration.is_finite() && duration > 0.0 && position >= duration {
1256            self.status = PlaybackStatus::Ended;
1257            self.snapshot.transport = PlaybackTransport::Ended;
1258        } else if matches!(self.status, PlaybackStatus::Ended) {
1259            self.status = PlaybackStatus::Paused;
1260            self.snapshot.transport = PlaybackTransport::Paused;
1261        }
1262    }
1263
1264    pub fn failed(&mut self, error: AudioError) {
1265        self.source_failed(PlaybackSourceFailure::Unknown(error));
1266    }
1267
1268    pub fn source_failed(&mut self, failure: PlaybackSourceFailure) {
1269        let error = failure.error().clone();
1270        self.status = PlaybackStatus::Failed(error.clone());
1271        self.snapshot.source = PlaybackSourceLifecycle::Failed;
1272        self.snapshot.transport = PlaybackTransport::Idle;
1273        self.snapshot.readiness = PlaybackReadiness::Unavailable;
1274        self.snapshot.source_failure = Some(failure);
1275        self.snapshot.alternative_failures = Arc::from([]);
1276        self.snapshot.play_failure = None;
1277        self.clear_bounded_for_lifecycle_change();
1278        self.clear_attempt_observations();
1279    }
1280
1281    #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
1282    fn source_exhausted(
1283        &mut self,
1284        failure: PlaybackSourceFailure,
1285        alternative_failures: Vec<PlaybackAlternativeFailure>,
1286    ) {
1287        self.source_failed(failure);
1288        self.snapshot.alternative_failures = alternative_failures.into();
1289    }
1290
1291    pub fn unload(&mut self) {
1292        self.status = PlaybackStatus::Empty;
1293        self.snapshot.source = PlaybackSourceLifecycle::Empty;
1294        self.snapshot.transport = PlaybackTransport::Idle;
1295        self.snapshot.readiness = PlaybackReadiness::Unavailable;
1296        self.clear_bounded_for_lifecycle_change();
1297        self.clear_source_observations();
1298        if self.graph_backed && self.snapshot.graph != PlaybackGraphState::Unavailable {
1299            self.snapshot.graph = PlaybackGraphState::AwaitingSource;
1300        }
1301    }
1302}
1303
1304fn normalize_time_ranges(
1305    ranges: impl IntoIterator<Item = PlaybackTimeRange>,
1306) -> Arc<[PlaybackTimeRange]> {
1307    let mut ranges: Vec<_> = ranges.into_iter().collect();
1308    ranges.sort_unstable_by_key(|range| range.start);
1309
1310    let mut normalized: Vec<PlaybackTimeRange> = Vec::with_capacity(ranges.len());
1311    for range in ranges {
1312        if let Some(last) = normalized.last_mut()
1313            && range.start <= last.end
1314        {
1315            last.end = last.end.max(range.end);
1316        } else {
1317            normalized.push(range);
1318        }
1319    }
1320    normalized.into()
1321}
1322
1323/// Clamp a requested playback position to a usable finite timeline.
1324pub fn clamp_seek(position: f64, duration: f64) -> f64 {
1325    if !position.is_finite() || !duration.is_finite() || duration <= 0.0 {
1326        return 0.0;
1327    }
1328    position.clamp(0.0, duration)
1329}
1330
1331#[derive(Clone, Copy, PartialEq)]
1332pub struct AudioPlayerController {
1333    status: ReadSignal<PlaybackStatus>,
1334    snapshot: ReadSignal<PlaybackSnapshot>,
1335    position: ReadSignal<Duration>,
1336    duration: ReadSignal<Duration>,
1337    rate: ReadSignal<f64>,
1338    analyser: ReadSignal<Option<AudioAnalyser>>,
1339    play: Callback<(), Result<(), PlaybackCommandError>>,
1340    pause: Callback<(), Result<(), PlaybackCommandError>>,
1341    stop: Callback<(), Result<(), PlaybackCommandError>>,
1342    seek: Callback<Duration>,
1343    skip: Callback<f64>,
1344    play_bounded:
1345        Callback<(WaveformSelection, BoundedPlaybackMode), Result<(), PlaybackCommandError>>,
1346    retarget_bounded: Callback<WaveformSelection, Result<(), PlaybackCommandError>>,
1347    cancel_bounded: Callback<()>,
1348    set_rate: Callback<f64, Result<(), PlaybackCommandError>>,
1349    set_repeat: Callback<bool>,
1350    set_muted: Callback<bool>,
1351    set_audibility_level: Callback<f64, Result<(), PlaybackCommandError>>,
1352}
1353
1354impl AudioPlayerController {
1355    pub fn status(self) -> ReadSignal<PlaybackStatus> {
1356        self.status
1357    }
1358
1359    pub fn snapshot(self) -> ReadSignal<PlaybackSnapshot> {
1360        self.snapshot
1361    }
1362
1363    pub fn position(self) -> ReadSignal<Duration> {
1364        self.position
1365    }
1366
1367    pub fn duration(self) -> ReadSignal<Duration> {
1368        self.duration
1369    }
1370
1371    pub fn rate(self) -> ReadSignal<f64> {
1372        self.rate
1373    }
1374
1375    /// The stable source-neutral Analyser for this graph-backed owner, once created.
1376    pub fn analyser(self) -> ReadSignal<Option<AudioAnalyser>> {
1377        self.analyser
1378    }
1379
1380    pub fn repeat(self) -> bool {
1381        self.snapshot.read().repeat
1382    }
1383
1384    pub fn muted(self) -> bool {
1385        self.snapshot.read().muted
1386    }
1387
1388    pub fn audibility_level(self) -> PlaybackAudibilityLevel {
1389        self.snapshot.read().audibility_level
1390    }
1391
1392    pub fn audibility_capability(self) -> PlaybackAudibilityCapability {
1393        self.snapshot.read().audibility_capability
1394    }
1395
1396    pub fn play(self) -> Result<(), PlaybackCommandError> {
1397        self.play.call(())
1398    }
1399
1400    pub fn pause(self) -> Result<(), PlaybackCommandError> {
1401        self.pause.call(())
1402    }
1403
1404    /// Stop Playback atomically and reset its observable position.
1405    pub fn stop(self) -> Result<(), PlaybackCommandError> {
1406        self.stop.call(())
1407    }
1408
1409    pub fn seek(self, position: Duration) {
1410        self.seek.call(position);
1411    }
1412
1413    pub fn skip(self, seconds: f64) {
1414        self.skip.call(seconds);
1415    }
1416
1417    /// Pause-seek to a Waveform Selection and play it once.
1418    ///
1419    /// The selection is validated against the current source's authoritative
1420    /// browser duration. Use [`Self::snapshot`] to observe the bounded lifecycle.
1421    pub fn play_bounded_once(
1422        self,
1423        selection: WaveformSelection,
1424    ) -> Result<(), PlaybackCommandError> {
1425        self.play_bounded
1426            .call((selection, BoundedPlaybackMode::Once))
1427    }
1428
1429    /// Pause-seek to a Waveform Selection and repeatedly play it.
1430    ///
1431    /// Loop wraps wait for a correlated seek to the range start before Playback
1432    /// resumes. Audible wrapping is best effort and is not gapless.
1433    pub fn play_bounded_loop(
1434        self,
1435        selection: WaveformSelection,
1436    ) -> Result<(), PlaybackCommandError> {
1437        self.play_bounded
1438            .call((selection, BoundedPlaybackMode::Loop))
1439    }
1440
1441    /// Atomically retarget an enforcing Bounded Playback operation.
1442    ///
1443    /// The current position is preserved when it remains in range. Otherwise,
1444    /// Playback pause-seeks to the replacement start before optionally resuming.
1445    pub fn retarget_bounded_playback(
1446        self,
1447        selection: WaveformSelection,
1448    ) -> Result<(), PlaybackCommandError> {
1449        self.retarget_bounded.call(selection)
1450    }
1451
1452    pub(crate) fn retarget_bounded_after_selection_commit(self, selection: WaveformSelection) {
1453        let enforcing = self
1454            .snapshot
1455            .read()
1456            .bounded
1457            .is_some_and(|bounded| bounded.phase.is_enforcing());
1458        if enforcing && self.retarget_bounded_playback(selection).is_err() {
1459            self.cancel_bounded_playback();
1460        }
1461    }
1462
1463    /// Stop enforcing the current bounded range without clearing application selection state.
1464    pub fn cancel_bounded_playback(self) {
1465        self.cancel_bounded.call(());
1466    }
1467
1468    pub fn set_rate(self, rate: f64) -> Result<(), PlaybackCommandError> {
1469        self.set_rate.call(rate)
1470    }
1471
1472    /// Set the whole-source repeat preference.
1473    pub fn set_repeat(self, repeat: bool) {
1474        self.set_repeat.call(repeat);
1475    }
1476
1477    /// Toggle the whole-source repeat preference.
1478    pub fn toggle_repeat(self) {
1479        self.set_repeat(!self.repeat());
1480    }
1481
1482    /// Set mute without changing transport, position, or the audibility level preference.
1483    pub fn set_muted(self, muted: bool) {
1484        self.set_muted.call(muted);
1485    }
1486
1487    /// Toggle mute without changing transport, position, or the audibility level preference.
1488    pub fn toggle_muted(self) {
1489        self.set_muted(!self.muted());
1490    }
1491
1492    /// Set the normalized audibility preference in the inclusive range `0.0..=1.0`.
1493    ///
1494    /// Consult [`Self::audibility_capability`] before presenting this value as effective
1495    /// output gain. Direct media-element control is best effort on some browsers.
1496    pub fn set_audibility_level(self, level: f64) -> Result<(), PlaybackCommandError> {
1497        self.set_audibility_level.call(level)
1498    }
1499}
1500
1501pub fn use_audio_player(
1502    source: ReadSignal<Option<PlaybackSource>>,
1503    initial_duration: Duration,
1504) -> AudioPlayerController {
1505    use_audio_player_with_options(source, initial_duration, PlaybackOptions::default())
1506}
1507
1508/// Create a Playback owner with immutable owner-level options.
1509pub fn use_audio_player_with_options(
1510    source: ReadSignal<Option<PlaybackSource>>,
1511    initial_duration: Duration,
1512    options: PlaybackOptions,
1513) -> AudioPlayerController {
1514    #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
1515    {
1516        web::use_web_audio_player(source, initial_duration, options)
1517    }
1518
1519    #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
1520    {
1521        let _ = source;
1522        use_unsupported_audio_player(initial_duration, options)
1523    }
1524}
1525
1526#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
1527fn use_unsupported_audio_player(
1528    initial_duration: Duration,
1529    options: PlaybackOptions,
1530) -> AudioPlayerController {
1531    let initial_lifecycle = PlaybackLifecycle::new(options);
1532    let mut status = use_signal(|| PlaybackStatus::Empty);
1533    let mut snapshot = use_signal(|| initial_lifecycle.snapshot().clone());
1534    let position = use_signal(|| Duration::ZERO);
1535    let mut duration = use_signal(|| initial_duration);
1536    let rate = use_signal(|| 1.0);
1537    let analyser = use_signal(|| None::<AudioAnalyser>);
1538    let initial_duration = use_memo(use_reactive!(|(initial_duration,)| initial_duration));
1539    use_effect(move || {
1540        duration.set(initial_duration());
1541        let error = AudioError::unsupported();
1542        let preferences = snapshot.peek().clone();
1543        status.set(PlaybackStatus::Failed(error.clone()));
1544        snapshot.set(PlaybackSnapshot {
1545            source: PlaybackSourceLifecycle::Failed,
1546            transport: PlaybackTransport::Idle,
1547            readiness: PlaybackReadiness::Unavailable,
1548            network: PlaybackNetworkActivity::Inactive,
1549            buffered: Arc::from([]),
1550            seekable: Arc::from([]),
1551            selected_alternative: None,
1552            source_failure: Some(PlaybackSourceFailure::Unsupported(error)),
1553            alternative_failures: Arc::from([]),
1554            play_failure: None,
1555            bounded: None,
1556            bounded_event: None,
1557            repeat: preferences.repeat,
1558            muted: preferences.muted,
1559            audibility_level: preferences.audibility_level,
1560            audibility_capability: PlaybackAudibilityCapability::Unavailable,
1561            graph: if preferences.graph == PlaybackGraphState::NotRequested {
1562                PlaybackGraphState::NotRequested
1563            } else {
1564                PlaybackGraphState::Unavailable
1565            },
1566        });
1567    });
1568    let unsupported: Callback<(), Result<(), PlaybackCommandError>> =
1569        use_callback(|()| Err(PlaybackCommandError("audio playback is unsupported")));
1570    let seek = use_callback(|_: Duration| {});
1571    let skip = use_callback(|_: f64| {});
1572    let play_bounded: Callback<
1573        (WaveformSelection, BoundedPlaybackMode),
1574        Result<(), PlaybackCommandError>,
1575    > = use_callback(|_: (WaveformSelection, BoundedPlaybackMode)| {
1576        Err(PlaybackCommandError("audio playback is unsupported"))
1577    });
1578    let retarget_bounded: Callback<WaveformSelection, Result<(), PlaybackCommandError>> =
1579        use_callback(|_: WaveformSelection| {
1580            Err(PlaybackCommandError("audio playback is unsupported"))
1581        });
1582    let cancel_bounded = use_callback(|()| {});
1583    let set_rate: Callback<f64, Result<(), PlaybackCommandError>> =
1584        use_callback(|_: f64| Err(PlaybackCommandError("audio playback is unsupported")));
1585    let mut snapshot_for_repeat = snapshot;
1586    let set_repeat = use_callback(move |repeat: bool| {
1587        snapshot_for_repeat.write().repeat = repeat;
1588    });
1589    let mut snapshot_for_muted = snapshot;
1590    let set_muted = use_callback(move |muted: bool| {
1591        snapshot_for_muted.write().muted = muted;
1592    });
1593    let set_audibility_level: Callback<f64, Result<(), PlaybackCommandError>> =
1594        use_callback(|_: f64| Err(PlaybackCommandError("audio playback is unsupported")));
1595    AudioPlayerController {
1596        status: status.into(),
1597        snapshot: snapshot.into(),
1598        position: position.into(),
1599        duration: duration.into(),
1600        rate: rate.into(),
1601        analyser: analyser.into(),
1602        play: unsupported,
1603        pause: unsupported,
1604        stop: unsupported,
1605        seek,
1606        skip,
1607        play_bounded,
1608        retarget_bounded,
1609        cancel_bounded,
1610        set_rate,
1611        set_repeat,
1612        set_muted,
1613        set_audibility_level,
1614    }
1615}