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    pub fn loaded(&mut self) {
1032        self.status = PlaybackStatus::Ready;
1033        self.snapshot.source = PlaybackSourceLifecycle::Playable;
1034        self.snapshot.readiness = PlaybackReadiness::Metadata;
1035        self.snapshot.bounded_event = None;
1036        self.clear_source_failures();
1037    }
1038
1039    pub fn metadata_loaded(&mut self) {
1040        if self.snapshot.source == PlaybackSourceLifecycle::Loading {
1041            self.snapshot.readiness = PlaybackReadiness::Metadata;
1042        }
1043    }
1044
1045    #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
1046    fn loading_alternative(&mut self) {
1047        if self.snapshot.source == PlaybackSourceLifecycle::Loading {
1048            self.status = PlaybackStatus::Loading;
1049            self.snapshot.readiness = PlaybackReadiness::LoadingMetadata;
1050            self.clear_source_failures();
1051            self.clear_attempt_observations();
1052        }
1053    }
1054
1055    #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
1056    fn source_attempt_started(&mut self, url_addressable: bool) {
1057        if self.snapshot.source == PlaybackSourceLifecycle::Loading {
1058            self.snapshot.network = if url_addressable {
1059                PlaybackNetworkActivity::Unknown
1060            } else {
1061                PlaybackNetworkActivity::Inactive
1062            };
1063            self.snapshot.buffered = Arc::from([]);
1064            self.snapshot.seekable = Arc::from([]);
1065        }
1066    }
1067
1068    pub fn url_playable(&mut self, alternative: PlaybackSourceAlternative) {
1069        if self.snapshot.source == PlaybackSourceLifecycle::Loading {
1070            self.status = PlaybackStatus::Ready;
1071            self.snapshot.source = PlaybackSourceLifecycle::Playable;
1072            self.snapshot.readiness = PlaybackReadiness::Playable;
1073            self.snapshot.selected_alternative = Some(alternative);
1074            self.snapshot.bounded_event = None;
1075            self.clear_source_failures();
1076        }
1077    }
1078
1079    pub fn request_play(&mut self) -> Result<(), PlaybackCommandError> {
1080        if !matches!(
1081            self.snapshot.transport,
1082            PlaybackTransport::Idle | PlaybackTransport::Paused | PlaybackTransport::Ended
1083        ) {
1084            return Err(PlaybackCommandError("audio is not ready to play"));
1085        }
1086
1087        match self.snapshot.source {
1088            PlaybackSourceLifecycle::Dormant => {
1089                self.status = PlaybackStatus::Loading;
1090                self.snapshot.source = PlaybackSourceLifecycle::Loading;
1091                self.snapshot.readiness = PlaybackReadiness::LoadingMetadata;
1092            }
1093            PlaybackSourceLifecycle::Loading => {
1094                self.status = PlaybackStatus::Loading;
1095            }
1096            PlaybackSourceLifecycle::Playable => {
1097                if self.snapshot.play_failure.is_some() {
1098                    self.status = PlaybackStatus::Ready;
1099                }
1100            }
1101            PlaybackSourceLifecycle::Empty | PlaybackSourceLifecycle::Failed => {
1102                return Err(PlaybackCommandError("audio is not ready to play"));
1103            }
1104        }
1105        self.snapshot.play_failure = None;
1106        self.snapshot.bounded_event = None;
1107        if self.snapshot.graph == PlaybackGraphState::InteractionRequired {
1108            self.snapshot.graph = PlaybackGraphState::Suspended;
1109        }
1110        self.snapshot.transport = PlaybackTransport::PlayPending;
1111        Ok(())
1112    }
1113
1114    pub fn playing(&mut self) {
1115        if !matches!(self.snapshot.source, PlaybackSourceLifecycle::Playable)
1116            || !matches!(
1117                self.snapshot.transport,
1118                PlaybackTransport::PlayPending | PlaybackTransport::Playing
1119            )
1120        {
1121            return;
1122        }
1123        self.status = PlaybackStatus::Playing;
1124        self.snapshot.transport = PlaybackTransport::Playing;
1125        self.snapshot.readiness = PlaybackReadiness::Playable;
1126        self.snapshot.play_failure = None;
1127        self.snapshot.bounded_event = None;
1128    }
1129
1130    pub fn waiting(&mut self) {
1131        if matches!(self.snapshot.source, PlaybackSourceLifecycle::Playable)
1132            && matches!(
1133                self.snapshot.transport,
1134                PlaybackTransport::PlayPending | PlaybackTransport::Playing
1135            )
1136        {
1137            self.snapshot.readiness = PlaybackReadiness::Waiting;
1138        }
1139    }
1140
1141    pub fn playable(&mut self) {
1142        if matches!(self.snapshot.source, PlaybackSourceLifecycle::Playable) {
1143            self.snapshot.readiness = PlaybackReadiness::Playable;
1144        }
1145    }
1146
1147    pub fn network_observed(&mut self, activity: PlaybackNetworkActivity) {
1148        if matches!(
1149            self.snapshot.source,
1150            PlaybackSourceLifecycle::Loading | PlaybackSourceLifecycle::Playable
1151        ) {
1152            self.snapshot.network = activity;
1153        }
1154    }
1155
1156    /// Replace buffered and seekable observations for the current source attempt.
1157    ///
1158    /// Ranges are sorted and overlapping or touching ranges are merged. A later
1159    /// observation replaces the whole snapshot, so either collection may shrink.
1160    pub fn ranges_changed(
1161        &mut self,
1162        buffered: impl IntoIterator<Item = PlaybackTimeRange>,
1163        seekable: impl IntoIterator<Item = PlaybackTimeRange>,
1164    ) {
1165        if matches!(
1166            self.snapshot.source,
1167            PlaybackSourceLifecycle::Loading | PlaybackSourceLifecycle::Playable
1168        ) {
1169            self.snapshot.buffered = normalize_time_ranges(buffered);
1170            self.snapshot.seekable = normalize_time_ranges(seekable);
1171        }
1172    }
1173
1174    pub fn paused(&mut self) {
1175        if self.snapshot.bounded.is_none() {
1176            self.snapshot.bounded_event = None;
1177        }
1178        if matches!(
1179            self.snapshot.transport,
1180            PlaybackTransport::PlayPending | PlaybackTransport::Playing
1181        ) {
1182            if self.snapshot.source == PlaybackSourceLifecycle::Loading {
1183                self.snapshot.transport = PlaybackTransport::Idle;
1184                self.status = PlaybackStatus::Loading;
1185            } else {
1186                self.snapshot.transport = PlaybackTransport::Paused;
1187                self.status = PlaybackStatus::Paused;
1188            }
1189        }
1190    }
1191
1192    /// Return a loaded source to its ready, idle state.
1193    pub fn stop(&mut self) -> Result<(), PlaybackCommandError> {
1194        if !matches!(self.snapshot.source, PlaybackSourceLifecycle::Playable) {
1195            return Err(PlaybackCommandError("audio is not loaded"));
1196        }
1197
1198        self.status = PlaybackStatus::Ready;
1199        self.snapshot.transport = PlaybackTransport::Idle;
1200        self.snapshot.play_failure = None;
1201        self.clear_bounded_for_lifecycle_change();
1202        Ok(())
1203    }
1204
1205    pub fn ended(&mut self) {
1206        if self.snapshot.transport != PlaybackTransport::Playing {
1207            return;
1208        }
1209        self.status = PlaybackStatus::Ended;
1210        self.snapshot.transport = PlaybackTransport::Ended;
1211        self.snapshot.readiness = PlaybackReadiness::Playable;
1212    }
1213
1214    pub fn play_rejected(&mut self, failure: PlaybackPlayFailure) {
1215        if !matches!(
1216            self.snapshot.source,
1217            PlaybackSourceLifecycle::Loading | PlaybackSourceLifecycle::Playable
1218        ) || !matches!(self.snapshot.transport, PlaybackTransport::PlayPending)
1219        {
1220            return;
1221        }
1222
1223        self.status = PlaybackStatus::Failed(failure.error().clone());
1224        self.snapshot.transport = if self.snapshot.source == PlaybackSourceLifecycle::Loading {
1225            PlaybackTransport::Idle
1226        } else {
1227            PlaybackTransport::Paused
1228        };
1229        if self.snapshot.readiness == PlaybackReadiness::Waiting {
1230            self.snapshot.readiness = PlaybackReadiness::Metadata;
1231        }
1232        self.snapshot.play_failure = Some(failure);
1233    }
1234
1235    pub fn seeked(&mut self, position: f64, duration: f64) {
1236        if matches!(
1237            self.status,
1238            PlaybackStatus::Empty | PlaybackStatus::Loading | PlaybackStatus::Failed(_)
1239        ) {
1240            return;
1241        }
1242        if duration.is_finite() && duration > 0.0 && position >= duration {
1243            self.status = PlaybackStatus::Ended;
1244            self.snapshot.transport = PlaybackTransport::Ended;
1245        } else if matches!(self.status, PlaybackStatus::Ended) {
1246            self.status = PlaybackStatus::Paused;
1247            self.snapshot.transport = PlaybackTransport::Paused;
1248        }
1249    }
1250
1251    pub fn failed(&mut self, error: AudioError) {
1252        self.source_failed(PlaybackSourceFailure::Unknown(error));
1253    }
1254
1255    pub fn source_failed(&mut self, failure: PlaybackSourceFailure) {
1256        let error = failure.error().clone();
1257        self.status = PlaybackStatus::Failed(error.clone());
1258        self.snapshot.source = PlaybackSourceLifecycle::Failed;
1259        self.snapshot.transport = PlaybackTransport::Idle;
1260        self.snapshot.readiness = PlaybackReadiness::Unavailable;
1261        self.snapshot.source_failure = Some(failure);
1262        self.snapshot.alternative_failures = Arc::from([]);
1263        self.snapshot.play_failure = None;
1264        self.clear_bounded_for_lifecycle_change();
1265        self.clear_attempt_observations();
1266    }
1267
1268    #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
1269    fn source_exhausted(
1270        &mut self,
1271        failure: PlaybackSourceFailure,
1272        alternative_failures: Vec<PlaybackAlternativeFailure>,
1273    ) {
1274        self.source_failed(failure);
1275        self.snapshot.alternative_failures = alternative_failures.into();
1276    }
1277
1278    pub fn unload(&mut self) {
1279        self.status = PlaybackStatus::Empty;
1280        self.snapshot.source = PlaybackSourceLifecycle::Empty;
1281        self.snapshot.transport = PlaybackTransport::Idle;
1282        self.snapshot.readiness = PlaybackReadiness::Unavailable;
1283        self.clear_bounded_for_lifecycle_change();
1284        self.clear_source_observations();
1285        if self.graph_backed && self.snapshot.graph != PlaybackGraphState::Unavailable {
1286            self.snapshot.graph = PlaybackGraphState::AwaitingSource;
1287        }
1288    }
1289}
1290
1291fn normalize_time_ranges(
1292    ranges: impl IntoIterator<Item = PlaybackTimeRange>,
1293) -> Arc<[PlaybackTimeRange]> {
1294    let mut ranges: Vec<_> = ranges.into_iter().collect();
1295    ranges.sort_unstable_by_key(|range| range.start);
1296
1297    let mut normalized: Vec<PlaybackTimeRange> = Vec::with_capacity(ranges.len());
1298    for range in ranges {
1299        if let Some(last) = normalized.last_mut()
1300            && range.start <= last.end
1301        {
1302            last.end = last.end.max(range.end);
1303        } else {
1304            normalized.push(range);
1305        }
1306    }
1307    normalized.into()
1308}
1309
1310/// Clamp a requested playback position to a usable finite timeline.
1311pub fn clamp_seek(position: f64, duration: f64) -> f64 {
1312    if !position.is_finite() || !duration.is_finite() || duration <= 0.0 {
1313        return 0.0;
1314    }
1315    position.clamp(0.0, duration)
1316}
1317
1318#[derive(Clone, Copy, PartialEq)]
1319pub struct AudioPlayerController {
1320    status: ReadSignal<PlaybackStatus>,
1321    snapshot: ReadSignal<PlaybackSnapshot>,
1322    position: ReadSignal<Duration>,
1323    duration: ReadSignal<Duration>,
1324    rate: ReadSignal<f64>,
1325    analyser: ReadSignal<Option<AudioAnalyser>>,
1326    play: Callback<(), Result<(), PlaybackCommandError>>,
1327    pause: Callback<(), Result<(), PlaybackCommandError>>,
1328    stop: Callback<(), Result<(), PlaybackCommandError>>,
1329    seek: Callback<Duration>,
1330    skip: Callback<f64>,
1331    play_bounded:
1332        Callback<(WaveformSelection, BoundedPlaybackMode), Result<(), PlaybackCommandError>>,
1333    retarget_bounded: Callback<WaveformSelection, Result<(), PlaybackCommandError>>,
1334    cancel_bounded: Callback<()>,
1335    set_rate: Callback<f64, Result<(), PlaybackCommandError>>,
1336    set_repeat: Callback<bool>,
1337    set_muted: Callback<bool>,
1338    set_audibility_level: Callback<f64, Result<(), PlaybackCommandError>>,
1339}
1340
1341impl AudioPlayerController {
1342    pub fn status(self) -> ReadSignal<PlaybackStatus> {
1343        self.status
1344    }
1345
1346    pub fn snapshot(self) -> ReadSignal<PlaybackSnapshot> {
1347        self.snapshot
1348    }
1349
1350    pub fn position(self) -> ReadSignal<Duration> {
1351        self.position
1352    }
1353
1354    pub fn duration(self) -> ReadSignal<Duration> {
1355        self.duration
1356    }
1357
1358    pub fn rate(self) -> ReadSignal<f64> {
1359        self.rate
1360    }
1361
1362    /// The stable source-neutral Analyser for this graph-backed owner, once created.
1363    pub fn analyser(self) -> ReadSignal<Option<AudioAnalyser>> {
1364        self.analyser
1365    }
1366
1367    pub fn repeat(self) -> bool {
1368        self.snapshot.read().repeat
1369    }
1370
1371    pub fn muted(self) -> bool {
1372        self.snapshot.read().muted
1373    }
1374
1375    pub fn audibility_level(self) -> PlaybackAudibilityLevel {
1376        self.snapshot.read().audibility_level
1377    }
1378
1379    pub fn audibility_capability(self) -> PlaybackAudibilityCapability {
1380        self.snapshot.read().audibility_capability
1381    }
1382
1383    pub fn play(self) -> Result<(), PlaybackCommandError> {
1384        self.play.call(())
1385    }
1386
1387    pub fn pause(self) -> Result<(), PlaybackCommandError> {
1388        self.pause.call(())
1389    }
1390
1391    /// Stop Playback atomically and reset its observable position.
1392    pub fn stop(self) -> Result<(), PlaybackCommandError> {
1393        self.stop.call(())
1394    }
1395
1396    pub fn seek(self, position: Duration) {
1397        self.seek.call(position);
1398    }
1399
1400    pub fn skip(self, seconds: f64) {
1401        self.skip.call(seconds);
1402    }
1403
1404    /// Pause-seek to a Waveform Selection and play it once.
1405    ///
1406    /// The selection is validated against the current source's authoritative
1407    /// browser duration. Use [`Self::snapshot`] to observe the bounded lifecycle.
1408    pub fn play_bounded_once(
1409        self,
1410        selection: WaveformSelection,
1411    ) -> Result<(), PlaybackCommandError> {
1412        self.play_bounded
1413            .call((selection, BoundedPlaybackMode::Once))
1414    }
1415
1416    /// Pause-seek to a Waveform Selection and repeatedly play it.
1417    ///
1418    /// Loop wraps wait for a correlated seek to the range start before Playback
1419    /// resumes. Audible wrapping is best effort and is not gapless.
1420    pub fn play_bounded_loop(
1421        self,
1422        selection: WaveformSelection,
1423    ) -> Result<(), PlaybackCommandError> {
1424        self.play_bounded
1425            .call((selection, BoundedPlaybackMode::Loop))
1426    }
1427
1428    /// Atomically retarget an enforcing Bounded Playback operation.
1429    ///
1430    /// The current position is preserved when it remains in range. Otherwise,
1431    /// Playback pause-seeks to the replacement start before optionally resuming.
1432    pub fn retarget_bounded_playback(
1433        self,
1434        selection: WaveformSelection,
1435    ) -> Result<(), PlaybackCommandError> {
1436        self.retarget_bounded.call(selection)
1437    }
1438
1439    pub(crate) fn retarget_bounded_after_selection_commit(self, selection: WaveformSelection) {
1440        let enforcing = self
1441            .snapshot
1442            .read()
1443            .bounded
1444            .is_some_and(|bounded| bounded.phase.is_enforcing());
1445        if enforcing && self.retarget_bounded_playback(selection).is_err() {
1446            self.cancel_bounded_playback();
1447        }
1448    }
1449
1450    /// Stop enforcing the current bounded range without clearing application selection state.
1451    pub fn cancel_bounded_playback(self) {
1452        self.cancel_bounded.call(());
1453    }
1454
1455    pub fn set_rate(self, rate: f64) -> Result<(), PlaybackCommandError> {
1456        self.set_rate.call(rate)
1457    }
1458
1459    /// Set the whole-source repeat preference.
1460    pub fn set_repeat(self, repeat: bool) {
1461        self.set_repeat.call(repeat);
1462    }
1463
1464    /// Toggle the whole-source repeat preference.
1465    pub fn toggle_repeat(self) {
1466        self.set_repeat(!self.repeat());
1467    }
1468
1469    /// Set mute without changing transport, position, or the audibility level preference.
1470    pub fn set_muted(self, muted: bool) {
1471        self.set_muted.call(muted);
1472    }
1473
1474    /// Toggle mute without changing transport, position, or the audibility level preference.
1475    pub fn toggle_muted(self) {
1476        self.set_muted(!self.muted());
1477    }
1478
1479    /// Set the normalized audibility preference in the inclusive range `0.0..=1.0`.
1480    ///
1481    /// Consult [`Self::audibility_capability`] before presenting this value as effective
1482    /// output gain. Direct media-element control is best effort on some browsers.
1483    pub fn set_audibility_level(self, level: f64) -> Result<(), PlaybackCommandError> {
1484        self.set_audibility_level.call(level)
1485    }
1486}
1487
1488pub fn use_audio_player(
1489    source: ReadSignal<Option<PlaybackSource>>,
1490    initial_duration: Duration,
1491) -> AudioPlayerController {
1492    use_audio_player_with_options(source, initial_duration, PlaybackOptions::default())
1493}
1494
1495/// Create a Playback owner with immutable owner-level options.
1496pub fn use_audio_player_with_options(
1497    source: ReadSignal<Option<PlaybackSource>>,
1498    initial_duration: Duration,
1499    options: PlaybackOptions,
1500) -> AudioPlayerController {
1501    #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
1502    {
1503        web::use_web_audio_player(source, initial_duration, options)
1504    }
1505
1506    #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
1507    {
1508        let _ = source;
1509        use_unsupported_audio_player(initial_duration, options)
1510    }
1511}
1512
1513#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
1514fn use_unsupported_audio_player(
1515    initial_duration: Duration,
1516    options: PlaybackOptions,
1517) -> AudioPlayerController {
1518    let initial_lifecycle = PlaybackLifecycle::new(options);
1519    let mut status = use_signal(|| PlaybackStatus::Empty);
1520    let mut snapshot = use_signal(|| initial_lifecycle.snapshot().clone());
1521    let position = use_signal(|| Duration::ZERO);
1522    let mut duration = use_signal(|| initial_duration);
1523    let rate = use_signal(|| 1.0);
1524    let analyser = use_signal(|| None::<AudioAnalyser>);
1525    let initial_duration = use_memo(use_reactive!(|(initial_duration,)| initial_duration));
1526    use_effect(move || {
1527        duration.set(initial_duration());
1528        let error = AudioError::unsupported();
1529        let preferences = snapshot.peek().clone();
1530        status.set(PlaybackStatus::Failed(error.clone()));
1531        snapshot.set(PlaybackSnapshot {
1532            source: PlaybackSourceLifecycle::Failed,
1533            transport: PlaybackTransport::Idle,
1534            readiness: PlaybackReadiness::Unavailable,
1535            network: PlaybackNetworkActivity::Inactive,
1536            buffered: Arc::from([]),
1537            seekable: Arc::from([]),
1538            selected_alternative: None,
1539            source_failure: Some(PlaybackSourceFailure::Unsupported(error)),
1540            alternative_failures: Arc::from([]),
1541            play_failure: None,
1542            bounded: None,
1543            bounded_event: None,
1544            repeat: preferences.repeat,
1545            muted: preferences.muted,
1546            audibility_level: preferences.audibility_level,
1547            audibility_capability: PlaybackAudibilityCapability::Unavailable,
1548            graph: if preferences.graph == PlaybackGraphState::NotRequested {
1549                PlaybackGraphState::NotRequested
1550            } else {
1551                PlaybackGraphState::Unavailable
1552            },
1553        });
1554    });
1555    let unsupported: Callback<(), Result<(), PlaybackCommandError>> =
1556        use_callback(|()| Err(PlaybackCommandError("audio playback is unsupported")));
1557    let seek = use_callback(|_: Duration| {});
1558    let skip = use_callback(|_: f64| {});
1559    let play_bounded: Callback<
1560        (WaveformSelection, BoundedPlaybackMode),
1561        Result<(), PlaybackCommandError>,
1562    > = use_callback(|_: (WaveformSelection, BoundedPlaybackMode)| {
1563        Err(PlaybackCommandError("audio playback is unsupported"))
1564    });
1565    let retarget_bounded: Callback<WaveformSelection, Result<(), PlaybackCommandError>> =
1566        use_callback(|_: WaveformSelection| {
1567            Err(PlaybackCommandError("audio playback is unsupported"))
1568        });
1569    let cancel_bounded = use_callback(|()| {});
1570    let set_rate: Callback<f64, Result<(), PlaybackCommandError>> =
1571        use_callback(|_: f64| Err(PlaybackCommandError("audio playback is unsupported")));
1572    let mut snapshot_for_repeat = snapshot;
1573    let set_repeat = use_callback(move |repeat: bool| {
1574        snapshot_for_repeat.write().repeat = repeat;
1575    });
1576    let mut snapshot_for_muted = snapshot;
1577    let set_muted = use_callback(move |muted: bool| {
1578        snapshot_for_muted.write().muted = muted;
1579    });
1580    let set_audibility_level: Callback<f64, Result<(), PlaybackCommandError>> =
1581        use_callback(|_: f64| Err(PlaybackCommandError("audio playback is unsupported")));
1582    AudioPlayerController {
1583        status: status.into(),
1584        snapshot: snapshot.into(),
1585        position: position.into(),
1586        duration: duration.into(),
1587        rate: rate.into(),
1588        analyser: analyser.into(),
1589        play: unsupported,
1590        pause: unsupported,
1591        stop: unsupported,
1592        seek,
1593        skip,
1594        play_bounded,
1595        retarget_bounded,
1596        cancel_bounded,
1597        set_rate,
1598        set_repeat,
1599        set_muted,
1600        set_audibility_level,
1601    }
1602}