Skip to main content

pryty_rustbrowser/
audio.rs

1use dioxus::prelude::*;
2use js_sys::{Promise, Uint8Array};
3use wasm_bindgen::{JsCast, JsValue, closure::Closure};
4use wasm_bindgen_futures::JsFuture;
5use web_sys::{
6    BlobEvent, MediaRecorder, MediaRecorderOptions, MediaStream, MediaStreamConstraints,
7    MediaStreamTrack,
8};
9/*
10stream = the audio data captured during recording
11chunks = stored stream that is organized
12*/
13
14#[derive(Debug, Clone, PartialEq)]
15pub enum RecordingState {
16    Idle,
17    Starting,
18    Recording,
19    Stopping,
20    Error(String),
21}
22
23#[derive(Debug, Clone)]
24pub enum RecordingError {
25    WindowUnavailable,
26    MediaDevicesUnavailable,
27    GetUserMediaFailed(String),
28    CastMediaStreamFailed,
29    RecorderCreateFailed(String),
30    RecorderStartFailed(String),
31    RecorderStopFailed(String),
32    RecorderRequestDataFailed(String),
33}
34
35impl core::fmt::Display for RecordingError {
36    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
37        match self {
38            RecordingError::WindowUnavailable => write!(f, "window unavailable"),
39            RecordingError::MediaDevicesUnavailable => write!(f, "media devices unavailable"),
40            RecordingError::GetUserMediaFailed(e) => write!(f, "getUserMedia failed: {e}"),
41            RecordingError::CastMediaStreamFailed => write!(f, "failed to cast to MediaStream"),
42            RecordingError::RecorderCreateFailed(e) => write!(f, "failed to create recorder: {e}"),
43            RecordingError::RecorderStartFailed(e) => write!(f, "failed to start recorder: {e}"),
44            RecordingError::RecorderStopFailed(e) => write!(f, "failed to stop recorder: {e}"),
45            RecordingError::RecorderRequestDataFailed(e) => {
46                write!(f, "failed to request recorder data: {e}")
47            }
48        }
49    }
50}
51
52pub struct AudioQualityConfig {
53    pub name: &'static str,
54    pub sample_rate: f64,
55    pub sample_size: u32,
56    pub channel_count: u32,
57    pub bits_per_second: u32,
58    pub mime_type: &'static str,
59}
60
61impl AudioQualityConfig {
62    pub fn low() -> Self {
63        Self {
64            name: "Low quality (voice call)",
65            sample_rate: 22050.0,
66            sample_size: 16,
67            channel_count: 1,
68            bits_per_second: 64000,
69            mime_type: "audio/webm;codecs=opus",
70        }
71    }
72
73    pub fn normal() -> Self {
74        Self {
75            name: "Standard quality",
76            sample_rate: 44100.0,
77            sample_size: 16,
78            channel_count: 1,
79            bits_per_second: 128000,
80            mime_type: "audio/webm;codecs=opus",
81        }
82    }
83
84    pub fn high() -> Self {
85        Self {
86            name: "High quality",
87            sample_rate: 48000.0,
88            sample_size: 24,
89            channel_count: 2,
90            bits_per_second: 192000,
91            mime_type: "audio/webm;codecs=opus",
92        }
93    }
94
95    pub fn studio() -> Self {
96        Self {
97            name: "Studio quality",
98            sample_rate: 96000.0,
99            sample_size: 24,
100            channel_count: 2,
101            bits_per_second: 320000,
102            mime_type: "audio/webm;codecs=opus",
103        }
104    }
105
106    pub fn lossless() -> Self {
107        Self {
108            name: "Lossless quality",
109            sample_rate: 48000.0,
110            sample_size: 32,
111            channel_count: 2,
112            bits_per_second: 0,
113            mime_type: "audio/webm;codecs=pcm",
114        }
115    }
116}
117
118pub struct Recording {
119    pub start: Callback<()>,
120    pub start_with_quality: Callback<AudioQualityConfig>,
121    pub stop: Callback<()>,
122    pub data: Signal<Option<Vec<u8>>>,
123    pub state: Signal<RecordingState>,
124    pub last_error: Signal<Option<String>>,
125}
126
127impl Recording {
128    pub fn is_active(&self) -> bool {
129        matches!(*self.state.read(), RecordingState::Recording)
130    }
131
132    pub fn is_busy(&self) -> bool {
133        matches!(
134            *self.state.read(),
135            RecordingState::Starting | RecordingState::Stopping
136        )
137    }
138}
139
140pub fn use_recording() -> Recording {
141    let data = use_signal(|| None::<Vec<u8>>);
142    let state = use_signal(|| RecordingState::Idle);
143    let last_error = use_signal(|| None::<String>);
144    let recorder = use_signal(|| None::<MediaRecorder>);
145    let stream = use_signal(|| None::<MediaStream>);
146    let chunks = use_signal(|| Vec::<Vec<u8>>::new());
147
148    let start = {
149        let state = state.clone();
150        let last_error = last_error.clone();
151        let recorder = recorder.clone();
152        let stream = stream.clone();
153        let chunks = chunks.clone();
154
155        use_callback(move |_| {
156            let mut state = state.clone();
157            let mut last_error = last_error.clone();
158            let mut recorder = recorder.clone();
159            let mut stream = stream.clone();
160            let mut chunks = chunks.clone();
161
162            spawn(async move {
163                if let Err(e) = start_recording(
164                    &mut state,
165                    &mut last_error,
166                    &mut recorder,
167                    &mut stream,
168                    &mut chunks,
169                )
170                .await
171                {
172                    // if error then change error msg
173                    let msg = e.to_string();
174                    state.set(RecordingState::Error(msg.clone()));
175                    last_error.set(Some(msg));
176                }
177            });
178        })
179    };
180
181    let start_with_quality = {
182        let state = state.clone();
183        let last_error = last_error.clone();
184        let recorder = recorder.clone();
185        let stream = stream.clone();
186        let chunks = chunks.clone();
187
188        use_callback(move |quality: AudioQualityConfig| {
189            let mut state = state.clone();
190            let mut last_error = last_error.clone();
191            let mut recorder = recorder.clone();
192            let mut stream = stream.clone();
193            let mut chunks = chunks.clone();
194
195            spawn(async move {
196                if let Err(e) = start_rec_with_quality(
197                    &mut state,
198                    &mut last_error,
199                    &mut recorder,
200                    &mut stream,
201                    &mut chunks,
202                    Some(quality),
203                )
204                .await
205                {
206                    // if error then change error msg
207                    let msg = e.to_string();
208                    state.set(RecordingState::Error(msg.clone()));
209                    last_error.set(Some(msg));
210                }
211            });
212        })
213    };
214
215    let stop = {
216        let data = data.clone();
217        let state = state.clone();
218        let last_error = last_error.clone();
219        let recorder = recorder.clone();
220        let stream = stream.clone();
221        let chunks = chunks.clone();
222
223        use_callback(move |_| {
224            let mut data = data.clone();
225            let mut state = state.clone();
226            let mut last_error = last_error.clone();
227            let mut recorder = recorder.clone();
228            let mut stream = stream.clone();
229            let mut chunks = chunks.clone();
230
231            if let Err(e) = stop_recording(
232                &mut data,
233                &mut state,
234                &mut last_error,
235                &mut recorder,
236                &mut stream,
237                &mut chunks,
238            ) {
239                let msg = e.to_string();
240                state.set(RecordingState::Error(msg.clone()));
241                last_error.set(Some(msg));
242            }
243        })
244    };
245
246    Recording {
247        start,
248        start_with_quality,
249        stop,
250        data,
251        state,
252        last_error,
253    }
254}
255
256async fn start_recording(
257    state: &mut Signal<RecordingState>,
258    last_error: &mut Signal<Option<String>>,
259    recorder: &mut Signal<Option<MediaRecorder>>,
260    stream: &mut Signal<Option<MediaStream>>,
261    chunks: &mut Signal<Vec<Vec<u8>>>,
262) -> Result<(), RecordingError> {
263    start_rec_with_quality(
264        state,
265        last_error,
266        recorder,
267        stream,
268        chunks,
269        None,
270    )
271    .await
272}
273
274pub async fn start_rec_with_quality(
275    state: &mut Signal<RecordingState>,
276    last_error: &mut Signal<Option<String>>,
277    recorder: &mut Signal<Option<MediaRecorder>>,
278    stream: &mut Signal<Option<MediaStream>>,
279    chunks: &mut Signal<Vec<Vec<u8>>>,
280    quality: Option<AudioQualityConfig>,
281) -> Result<(), RecordingError> {
282    state.set(RecordingState::Starting);
283    if let Some(s) = stream.read().as_ref() {
284        let tracks = s.get_tracks();
285        for i in 0..tracks.length() {
286            if let Ok(track) = tracks.get(i).dyn_into::<MediaStreamTrack>() {
287                track.stop();
288            }
289        }
290    }
291    last_error.set(None);
292    chunks.write().clear();
293    stream.set(None);
294    let window = web_sys::window().ok_or(RecordingError::WindowUnavailable)?;
295    let devices = window
296        .navigator()
297        .media_devices()
298        .map_err(|_| RecordingError::MediaDevicesUnavailable)?;
299
300    let constraints = MediaStreamConstraints::new();
301    constraints.set_audio(&true.into());
302
303    let promise:Promise = devices
304        .get_user_media_with_constraints(&constraints)
305        .map_err(|e| RecordingError::GetUserMediaFailed(format!("{e:?}")))?;
306
307    let js_val:JsValue = JsFuture::from(promise)
308        .await // JavascriptValue -> Rust's future value
309        .map_err(|e| RecordingError::GetUserMediaFailed(format!("{e:?}")))?;
310
311    let s: MediaStream = js_val
312        .dyn_into()
313        .map_err(|_| RecordingError::CastMediaStreamFailed)?;
314
315    stream.set(Some(s.clone()));
316
317    let options = MediaRecorderOptions::new();
318    if let Some(q) = quality {
319        options.set_mime_type(q.mime_type);
320        if q.bits_per_second > 0 {
321            options.set_audio_bits_per_second(q.bits_per_second);
322        }
323    }
324
325    let rec = MediaRecorder::new_with_media_stream_and_media_recorder_options(&s, &options)
326        .map_err(|e| RecordingError::RecorderCreateFailed(format!("{e:?}")))?;
327
328    let chunks_for_data = chunks.clone();
329    let ondata = Closure::wrap(Box::new(move |e: BlobEvent| {
330        let maybe_blob = e.data();
331        let mut chunks_inner = chunks_for_data.clone();
332
333        spawn(async move {
334            if let Some(blob) = maybe_blob {
335                if let Ok(buf) = JsFuture::from(blob.array_buffer()).await {
336                    let bytes = Uint8Array::new(&buf).to_vec(); // jsfuture -> array buffer
337                    // array buffer -> vec<u8> (array buffer cant read or write so we need vec<u8>)
338                    chunks_inner.write().push(bytes);
339                }
340            }
341        });
342    }) as Box<dyn FnMut(_)>);
343
344    rec.set_ondataavailable(Some(ondata.as_ref().unchecked_ref())); // ptr -> js ptr
345    ondata.forget(); /*If we forget to free it, Rust won't free the memory — it will just let the browser free it.
346     Otherwise, it could lead to double free or similar issues.(Bad things!☆*: .。. o(≧▽≦)o .。.:*☆)
347 */
348    rec.start_with_time_slice(1000)
349        .map_err(|e| RecordingError::RecorderStartFailed(format!("{e:?}")))?;
350
351    recorder.set(Some(rec));
352    state.set(RecordingState::Recording);
353    Ok(())
354}
355
356fn stop_recording(
357    data: &mut Signal<Option<Vec<u8>>>,
358    state: &mut Signal<RecordingState>,
359    _last_error: &mut Signal<Option<String>>,
360    recorder: &mut Signal<Option<MediaRecorder>>,
361    stream: &mut Signal<Option<MediaStream>>,
362    chunks: &mut Signal<Vec<Vec<u8>>>,
363) -> Result<(), RecordingError> {
364    state.set(RecordingState::Stopping);
365
366    if let Some(r) = recorder.read().as_ref() {
367        r.request_data() // get data
368            .map_err(|e| RecordingError::RecorderRequestDataFailed(format!("{e:?}")))?;
369        r.stop()
370            .map_err(|e| RecordingError::RecorderStopFailed(format!("{e:?}")))?;
371    }
372
373    if let Some(s) = stream.read().as_ref() {
374        let tracks = s.get_tracks();
375        for i in 0..tracks.length() {
376            if let Ok(track) = tracks.get(i).dyn_into::<MediaStreamTrack>() {
377                track.stop();
378            }
379        }
380    }
381
382    let all: Vec<u8> = chunks.read().iter().flatten().cloned().collect();
383    data.set(Some(all));
384    chunks.write().clear();
385
386    recorder.set(None);
387    stream.set(None);
388    state.set(RecordingState::Idle);
389
390    Ok(())
391}