Skip to main content

dioxus_audio/
playback.rs

1//! Audio playback state and hooks.
2
3use std::fmt;
4use std::time::Duration;
5
6use dioxus::prelude::*;
7
8use crate::AudioData;
9use crate::AudioError;
10
11#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
12mod web;
13
14#[derive(Clone, Debug, PartialEq, Eq)]
15#[non_exhaustive]
16pub enum PlaybackStatus {
17    Empty,
18    Loading,
19    Ready,
20    Playing,
21    Paused,
22    Ended,
23    Failed(AudioError),
24}
25
26#[derive(Clone, Debug, PartialEq, Eq)]
27pub struct PlaybackCommandError(&'static str);
28
29impl fmt::Display for PlaybackCommandError {
30    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
31        formatter.write_str(self.0)
32    }
33}
34
35impl std::error::Error for PlaybackCommandError {}
36
37#[derive(Debug)]
38pub struct PlaybackLifecycle {
39    status: PlaybackStatus,
40}
41
42impl Default for PlaybackLifecycle {
43    fn default() -> Self {
44        Self {
45            status: PlaybackStatus::Empty,
46        }
47    }
48}
49
50impl PlaybackLifecycle {
51    pub fn status(&self) -> &PlaybackStatus {
52        &self.status
53    }
54
55    pub fn loading(&mut self) {
56        self.status = PlaybackStatus::Loading;
57    }
58
59    pub fn loaded(&mut self) {
60        self.status = PlaybackStatus::Ready;
61    }
62
63    pub fn request_play(&self) -> Result<(), PlaybackCommandError> {
64        if matches!(
65            self.status,
66            PlaybackStatus::Ready | PlaybackStatus::Paused | PlaybackStatus::Ended
67        ) {
68            Ok(())
69        } else {
70            Err(PlaybackCommandError("audio is not ready to play"))
71        }
72    }
73
74    pub fn playing(&mut self) {
75        self.status = PlaybackStatus::Playing;
76    }
77
78    pub fn paused(&mut self) {
79        if matches!(self.status, PlaybackStatus::Playing) {
80            self.status = PlaybackStatus::Paused;
81        }
82    }
83
84    pub fn ended(&mut self) {
85        self.status = PlaybackStatus::Ended;
86    }
87
88    pub fn seeked(&mut self, position: f64, duration: f64) {
89        if matches!(
90            self.status,
91            PlaybackStatus::Empty | PlaybackStatus::Loading | PlaybackStatus::Failed(_)
92        ) {
93            return;
94        }
95        if duration.is_finite() && duration > 0.0 && position >= duration {
96            self.status = PlaybackStatus::Ended;
97        } else if matches!(self.status, PlaybackStatus::Ended) {
98            self.status = PlaybackStatus::Paused;
99        }
100    }
101
102    pub fn failed(&mut self, error: AudioError) {
103        self.status = PlaybackStatus::Failed(error);
104    }
105
106    pub fn unload(&mut self) {
107        self.status = PlaybackStatus::Empty;
108    }
109}
110
111/// Clamp a requested playback position to a usable finite timeline.
112pub fn clamp_seek(position: f64, duration: f64) -> f64 {
113    if !position.is_finite() || !duration.is_finite() || duration <= 0.0 {
114        return 0.0;
115    }
116    position.clamp(0.0, duration)
117}
118
119#[derive(Clone, Copy, PartialEq)]
120pub struct AudioPlayerController {
121    status: ReadSignal<PlaybackStatus>,
122    position: ReadSignal<Duration>,
123    duration: ReadSignal<Duration>,
124    rate: ReadSignal<f64>,
125    play: Callback<(), Result<(), PlaybackCommandError>>,
126    pause: Callback<(), Result<(), PlaybackCommandError>>,
127    seek: Callback<Duration>,
128    skip: Callback<f64>,
129    set_rate: Callback<f64, Result<(), PlaybackCommandError>>,
130}
131
132impl AudioPlayerController {
133    pub fn status(self) -> ReadSignal<PlaybackStatus> {
134        self.status
135    }
136
137    pub fn position(self) -> ReadSignal<Duration> {
138        self.position
139    }
140
141    pub fn duration(self) -> ReadSignal<Duration> {
142        self.duration
143    }
144
145    pub fn rate(self) -> ReadSignal<f64> {
146        self.rate
147    }
148
149    pub fn play(self) -> Result<(), PlaybackCommandError> {
150        self.play.call(())
151    }
152
153    pub fn pause(self) -> Result<(), PlaybackCommandError> {
154        self.pause.call(())
155    }
156
157    pub fn seek(self, position: Duration) {
158        self.seek.call(position);
159    }
160
161    pub fn skip(self, seconds: f64) {
162        self.skip.call(seconds);
163    }
164
165    pub fn set_rate(self, rate: f64) -> Result<(), PlaybackCommandError> {
166        self.set_rate.call(rate)
167    }
168}
169
170pub fn use_audio_player(
171    source: ReadSignal<Option<AudioData>>,
172    initial_duration: Duration,
173) -> AudioPlayerController {
174    #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
175    {
176        web::use_web_audio_player(source, initial_duration)
177    }
178
179    #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
180    {
181        let _ = source;
182        use_unsupported_audio_player(initial_duration)
183    }
184}
185
186#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
187fn use_unsupported_audio_player(initial_duration: Duration) -> AudioPlayerController {
188    let mut status = use_signal(|| PlaybackStatus::Empty);
189    let position = use_signal(|| Duration::ZERO);
190    let mut duration = use_signal(|| initial_duration);
191    let rate = use_signal(|| 1.0);
192    let initial_duration = use_memo(use_reactive!(|(initial_duration,)| initial_duration));
193    use_effect(move || {
194        duration.set(initial_duration());
195        status.set(PlaybackStatus::Failed(AudioError::unsupported()));
196    });
197    let unsupported: Callback<(), Result<(), PlaybackCommandError>> =
198        use_callback(|()| Err(PlaybackCommandError("audio playback is unsupported")));
199    let seek = use_callback(|_: Duration| {});
200    let skip = use_callback(|_: f64| {});
201    let set_rate: Callback<f64, Result<(), PlaybackCommandError>> =
202        use_callback(|_: f64| Err(PlaybackCommandError("audio playback is unsupported")));
203    AudioPlayerController {
204        status: status.into(),
205        position: position.into(),
206        duration: duration.into(),
207        rate: rate.into(),
208        play: unsupported,
209        pause: unsupported,
210        seek,
211        skip,
212        set_rate,
213    }
214}