1use 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
111pub 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}