Skip to main content

pryty_rustbrowser/
audio.rs

1use dioxus::prelude::*;
2use js_sys::{Promise, Uint8Array};
3use std::convert::TryFrom;
4use wasm_bindgen::{closure::Closure, JsCast, JsValue};
5use wasm_bindgen_futures::JsFuture;
6use web_sys::{
7    BlobEvent, MediaRecorder, MediaRecorderOptions, MediaStream, MediaStreamConstraints,
8    MediaStreamTrack,
9};
10/*
11stream = the audio data captured during recording
12chunks = stored stream that is organized
13*/
14
15#[derive(Debug, Clone, PartialEq)]
16pub enum RecordingState {
17    Idle,
18    Starting,
19    Recording,
20    Paused,
21    Stopping,
22    Error(String),
23}
24
25#[derive(Debug, Clone)]
26pub enum RecordingError {
27    WindowUnavailable,
28    MediaDevicesUnavailable,
29    GetUserMediaFailed(String),
30    CastMediaStreamFailed,
31    RecorderCreateFailed(String),
32    RecorderStartFailed(String),
33    RecorderStopFailed(String),
34    RecorderRequestDataFailed(String),
35    RecorderPauseFailed(String),
36    RecorderResumeFailed(String),
37}
38
39impl core::fmt::Display for RecordingError {
40    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
41        match self {
42            RecordingError::WindowUnavailable => write!(f, "window unavailable"),
43            RecordingError::MediaDevicesUnavailable => write!(f, "media devices unavailable"),
44            RecordingError::GetUserMediaFailed(e) => write!(f, "getUserMedia failed: {e}"),
45            RecordingError::CastMediaStreamFailed => write!(f, "failed to cast to MediaStream"),
46            RecordingError::RecorderCreateFailed(e) => write!(f, "failed to create recorder: {e}"),
47            RecordingError::RecorderStartFailed(e) => write!(f, "failed to start recorder: {e}"),
48            RecordingError::RecorderStopFailed(e) => write!(f, "failed to stop recorder: {e}"),
49            RecordingError::RecorderRequestDataFailed(e) => {
50                write!(f, "failed to request recorder data: {e}")
51            }
52            RecordingError::RecorderPauseFailed(e) => write!(f, "failed to pause recorder: {e}"),
53            RecordingError::RecorderResumeFailed(e) => write!(f, "failed to resume recorder: {e}"),
54        }
55    }
56}
57
58pub struct AudioQualityConfig {
59    pub name: &'static str,
60    pub sample_rate: f64,
61    pub sample_size: u32,
62    pub channel_count: u32,
63    pub bits_per_second: u32,
64    pub mime_type: &'static str,
65}
66
67impl AudioQualityConfig {
68    pub fn low() -> Self {
69        Self {
70            name: "Low quality (voice call)",
71            sample_rate: 22050.0,
72            sample_size: 16,
73            channel_count: 1,
74            bits_per_second: 64000,
75            mime_type: "audio/webm;codecs=opus",
76        }
77    }
78
79    pub fn normal() -> Self {
80        Self {
81            name: "Standard quality",
82            sample_rate: 44100.0,
83            sample_size: 16,
84            channel_count: 1,
85            bits_per_second: 128000,
86            mime_type: "audio/webm;codecs=opus",
87        }
88    }
89
90    pub fn high() -> Self {
91        Self {
92            name: "High quality",
93            sample_rate: 48000.0,
94            sample_size: 24,
95            channel_count: 2,
96            bits_per_second: 192000,
97            mime_type: "audio/webm;codecs=opus",
98        }
99    }
100
101    pub fn studio() -> Self {
102        Self {
103            name: "Studio quality",
104            sample_rate: 96000.0,
105            sample_size: 24,
106            channel_count: 2,
107            bits_per_second: 320000,
108            mime_type: "audio/webm;codecs=opus",
109        }
110    }
111
112    pub fn lossless() -> Self {
113        Self {
114            name: "Lossless quality",
115            sample_rate: 96000.0,
116            sample_size: 32,
117            channel_count: 2,
118            bits_per_second: 0,
119            mime_type: "audio/webm;codecs=pcm",
120        }
121    }
122}
123
124// 建议增加的功能
125#[derive(Debug, Clone, Copy, Default)]
126pub struct RecordingConfig {
127    pub time_slice: Option<u32>,  // 可配置数据块间隔
128    pub max_duration: Option<u32>, // 最大录音时长
129}
130
131pub struct Recording {
132    pub start: Callback<()>,
133    pub start_with_quality: Callback<AudioQualityConfig>,
134    pub start_with_config: Callback<RecordingConfig>,
135    pub start_with_quality_and_config: Callback<(AudioQualityConfig, RecordingConfig)>,
136    pub pause: Callback<()>,
137    pub resume: Callback<()>,
138    pub stop: Callback<()>,
139    pub data: Signal<Option<Vec<u8>>>,
140    pub state: Signal<RecordingState>,
141    pub last_error: Signal<Option<String>>,
142    recorder: Signal<Option<MediaRecorder>>,
143    stream: Signal<Option<MediaStream>>,
144    chunks: Signal<Vec<Vec<u8>>>,
145}
146
147impl Recording {
148    pub fn is_active(&self) -> bool {
149        matches!(*self.state.read(), RecordingState::Recording)
150    }
151
152    pub fn is_paused(&self) -> bool {
153        matches!(*self.state.read(), RecordingState::Paused)
154    }
155
156    pub fn is_busy(&self) -> bool {
157        matches!(
158            *self.state.read(),
159            RecordingState::Starting | RecordingState::Stopping
160        )
161    }
162}
163
164// 更好的资源清理
165impl Drop for Recording {
166    fn drop(&mut self) {
167        // 增加手动drop减少内存问题
168        let _ = stop_recording(
169            &mut self.data,
170            &mut self.state,
171            &mut self.last_error,
172            &mut self.recorder,
173            &mut self.stream,
174            &mut self.chunks,
175        );
176    }
177}
178
179pub fn use_recording() -> Recording {
180    let data = use_signal(|| None::<Vec<u8>>);
181    let state = use_signal(|| RecordingState::Idle);
182    let last_error = use_signal(|| None::<String>);
183    let recorder = use_signal(|| None::<MediaRecorder>);
184    let stream = use_signal(|| None::<MediaStream>);
185    let chunks = use_signal(|| Vec::<Vec<u8>>::new());
186
187    let start = {
188        let state = state.clone();
189        let last_error = last_error.clone();
190        let recorder = recorder.clone();
191        let stream = stream.clone();
192        let chunks = chunks.clone();
193
194        use_callback(move |_| {
195            let mut state = state.clone();
196            let mut last_error = last_error.clone();
197            let mut recorder = recorder.clone();
198            let mut stream = stream.clone();
199            let mut chunks = chunks.clone();
200
201            spawn(async move {
202                if let Err(e) =
203                    start_recording(&mut state, &mut last_error, &mut recorder, &mut stream, &mut chunks)
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 start_with_quality = {
216        let state = state.clone();
217        let last_error = last_error.clone();
218        let recorder = recorder.clone();
219        let stream = stream.clone();
220        let chunks = chunks.clone();
221
222        use_callback(move |quality: AudioQualityConfig| {
223            let mut state = state.clone();
224            let mut last_error = last_error.clone();
225            let mut recorder = recorder.clone();
226            let mut stream = stream.clone();
227            let mut chunks = chunks.clone();
228
229            spawn(async move {
230                if let Err(e) = start_rec_with_quality_and_config(
231                    &mut state,
232                    &mut last_error,
233                    &mut recorder,
234                    &mut stream,
235                    &mut chunks,
236                    Some(quality),
237                    None,
238                )
239                .await
240                {
241                    // if error then change error msg
242                    let msg = e.to_string();
243                    state.set(RecordingState::Error(msg.clone()));
244                    last_error.set(Some(msg));
245                }
246            });
247        })
248    };
249
250    let start_with_config = {
251        let state = state.clone();
252        let last_error = last_error.clone();
253        let recorder = recorder.clone();
254        let stream = stream.clone();
255        let chunks = chunks.clone();
256
257        use_callback(move |config: RecordingConfig| {
258            let mut state = state.clone();
259            let mut last_error = last_error.clone();
260            let mut recorder = recorder.clone();
261            let mut stream = stream.clone();
262            let mut chunks = chunks.clone();
263
264            spawn(async move {
265                if let Err(e) = start_rec_with_quality_and_config(
266                    &mut state,
267                    &mut last_error,
268                    &mut recorder,
269                    &mut stream,
270                    &mut chunks,
271                    None,
272                    Some(config),
273                )
274                .await
275                {
276                    // if error then change error msg
277                    let msg = e.to_string();
278                    state.set(RecordingState::Error(msg.clone()));
279                    last_error.set(Some(msg));
280                }
281            });
282        })
283    };
284
285    let start_with_quality_and_config = {
286        let state = state.clone();
287        let last_error = last_error.clone();
288        let recorder = recorder.clone();
289        let stream = stream.clone();
290        let chunks = chunks.clone();
291
292        use_callback(move |(quality, config): (AudioQualityConfig, RecordingConfig)| {
293            let mut state = state.clone();
294            let mut last_error = last_error.clone();
295            let mut recorder = recorder.clone();
296            let mut stream = stream.clone();
297            let mut chunks = chunks.clone();
298
299            spawn(async move {
300                if let Err(e) = start_rec_with_quality_and_config(
301                    &mut state,
302                    &mut last_error,
303                    &mut recorder,
304                    &mut stream,
305                    &mut chunks,
306                    Some(quality),
307                    Some(config),
308                )
309                .await
310                {
311                    // if error then change error msg
312                    let msg = e.to_string();
313                    state.set(RecordingState::Error(msg.clone()));
314                    last_error.set(Some(msg));
315                }
316            });
317        })
318    };
319
320    let pause = {
321        let state = state.clone();
322        let last_error = last_error.clone();
323        let recorder = recorder.clone();
324
325        use_callback(move |_| {
326            let mut state = state.clone();
327            let mut last_error = last_error.clone();
328            let mut recorder = recorder.clone();
329
330            if let Err(e) = pause_recording(&mut state, &mut last_error, &mut recorder) {
331                let msg = e.to_string();
332                state.set(RecordingState::Error(msg.clone()));
333                last_error.set(Some(msg));
334            }
335        })
336    };
337
338    let resume = {
339        let state = state.clone();
340        let last_error = last_error.clone();
341        let recorder = recorder.clone();
342
343        use_callback(move |_| {
344            let mut state = state.clone();
345            let mut last_error = last_error.clone();
346            let mut recorder = recorder.clone();
347
348            if let Err(e) = resume_recording(&mut state, &mut last_error, &mut recorder) {
349                let msg = e.to_string();
350                state.set(RecordingState::Error(msg.clone()));
351                last_error.set(Some(msg));
352            }
353        })
354    };
355
356    let stop = {
357        let data = data.clone();
358        let state = state.clone();
359        let last_error = last_error.clone();
360        let recorder = recorder.clone();
361        let stream = stream.clone();
362        let chunks = chunks.clone();
363
364        use_callback(move |_| {
365            let mut data = data.clone();
366            let mut state = state.clone();
367            let mut last_error = last_error.clone();
368            let mut recorder = recorder.clone();
369            let mut stream = stream.clone();
370            let mut chunks = chunks.clone();
371
372            if let Err(e) = stop_recording(
373                &mut data,
374                &mut state,
375                &mut last_error,
376                &mut recorder,
377                &mut stream,
378                &mut chunks,
379            ) {
380                let msg = e.to_string();
381                state.set(RecordingState::Error(msg.clone()));
382                last_error.set(Some(msg));
383            }
384        })
385    };
386
387    Recording {
388        start,
389        start_with_quality,
390        start_with_config,
391        start_with_quality_and_config,
392        pause,
393        resume,
394        stop,
395        data,
396        state,
397        last_error,
398        recorder,
399        stream,
400        chunks,
401    }
402}
403
404async fn start_recording(
405    state: &mut Signal<RecordingState>,
406    last_error: &mut Signal<Option<String>>,
407    recorder: &mut Signal<Option<MediaRecorder>>,
408    stream: &mut Signal<Option<MediaStream>>,
409    chunks: &mut Signal<Vec<Vec<u8>>>,
410) -> Result<(), RecordingError> {
411    start_rec_with_quality_and_config(state, last_error, recorder, stream, chunks, None, None).await
412}
413
414pub async fn start_rec_with_quality(
415    state: &mut Signal<RecordingState>,
416    last_error: &mut Signal<Option<String>>,
417    recorder: &mut Signal<Option<MediaRecorder>>,
418    stream: &mut Signal<Option<MediaStream>>,
419    chunks: &mut Signal<Vec<Vec<u8>>>,
420    quality: Option<AudioQualityConfig>,
421) -> Result<(), RecordingError> {
422    start_rec_with_quality_and_config(state, last_error, recorder, stream, chunks, quality, None).await
423}
424
425pub async fn start_rec_with_quality_and_config(
426    state: &mut Signal<RecordingState>,
427    last_error: &mut Signal<Option<String>>,
428    recorder: &mut Signal<Option<MediaRecorder>>,
429    stream: &mut Signal<Option<MediaStream>>,
430    chunks: &mut Signal<Vec<Vec<u8>>>,
431    quality: Option<AudioQualityConfig>,
432    config: Option<RecordingConfig>,
433) -> Result<(), RecordingError> {
434    if matches!(
435        *state.read(),
436        RecordingState::Starting | RecordingState::Recording | RecordingState::Paused
437    ) {
438        return Ok(());
439    }
440
441    state.set(RecordingState::Starting);
442    if let Some(s) = stream.read().as_ref() {
443        let tracks = s.get_tracks();
444        for i in 0..tracks.length() {
445            if let Ok(track) = tracks.get(i).dyn_into::<MediaStreamTrack>() {
446                track.stop();
447            }
448        }
449    }
450    last_error.set(None);
451    chunks.write().clear();
452    stream.set(None);
453    let window = web_sys::window().ok_or(RecordingError::WindowUnavailable)?;
454    let devices = window
455        .navigator()
456        .media_devices()
457        .map_err(|_| RecordingError::MediaDevicesUnavailable)?;
458
459    let constraints = MediaStreamConstraints::new();
460    constraints.set_audio(&true.into());
461
462    let promise: Promise = devices
463        .get_user_media_with_constraints(&constraints)
464        .map_err(|e| RecordingError::GetUserMediaFailed(format!("{e:?}")))?;
465
466    let js_val: JsValue = JsFuture::from(promise)
467        .await
468        .map_err(|e| RecordingError::GetUserMediaFailed(format!("{e:?}")))?;
469
470    let s: MediaStream = js_val
471        .dyn_into()
472        .map_err(|_| RecordingError::CastMediaStreamFailed)?;
473
474    stream.set(Some(s.clone()));
475
476    let options = MediaRecorderOptions::new();
477    if let Some(q) = quality {
478        options.set_mime_type(q.mime_type);
479        if q.bits_per_second > 0 {
480            options.set_audio_bits_per_second(q.bits_per_second);
481        }
482    }
483
484    let rec = MediaRecorder::new_with_media_stream_and_media_recorder_options(&s, &options)
485        .map_err(|e| RecordingError::RecorderCreateFailed(format!("{e:?}")))?;
486
487    let chunks_for_data = chunks.clone();
488    let ondata = Closure::wrap(Box::new(move |e: BlobEvent| {
489        let maybe_blob = e.data();
490        let mut chunks_inner = chunks_for_data.clone();
491
492        spawn(async move {
493            if let Some(blob) = maybe_blob {
494                if let Ok(buf) = JsFuture::from(blob.array_buffer()).await {
495                    let bytes = Uint8Array::new(&buf).to_vec();
496                    chunks_inner.write().push(bytes);
497                }
498            }
499        });
500    }) as Box<dyn FnMut(_)>);
501
502    rec.set_ondataavailable(Some(ondata.as_ref().unchecked_ref()));
503    ondata.forget(); /*If we forget to free it, Rust won't free the memory — it will just let the browser free it.
504     Otherwise, it could lead to double free or similar issues.(Bad things!☆*: .。. o(≧▽≦)o .。.:*☆)
505 */
506
507    let time_slice_u32 = config.and_then(|c| c.time_slice).unwrap_or(1000);
508    let time_slice = i32::try_from(time_slice_u32)
509        .map_err(|_| RecordingError::RecorderStartFailed("time_slice out of i32 range".to_string()))?;
510    rec.start_with_time_slice(time_slice)
511        .map_err(|e| RecordingError::RecorderStartFailed(format!("{e:?}")))?;
512
513    recorder.set(Some(rec));
514    state.set(RecordingState::Recording);
515
516    if let Some(max_duration) = config.and_then(|c| c.max_duration) {
517        let mut data_for_timeout = use_signal(|| None::<Vec<u8>>);
518        let mut state_for_timeout = state.clone();
519        let mut last_error_for_timeout = last_error.clone();
520        let mut recorder_for_timeout = recorder.clone();
521        let mut stream_for_timeout = stream.clone();
522        let mut chunks_for_timeout = chunks.clone();
523
524        let timeout_cb = Closure::once_into_js(move || {
525            let _ = stop_recording(
526                &mut data_for_timeout,
527                &mut state_for_timeout,
528                &mut last_error_for_timeout,
529                &mut recorder_for_timeout,
530                &mut stream_for_timeout,
531                &mut chunks_for_timeout,
532            );
533        });
534
535        let window = web_sys::window().ok_or(RecordingError::WindowUnavailable)?;
536        let timeout_i32 = i32::try_from(max_duration)
537            .map_err(|_| RecordingError::RecorderStartFailed("max_duration out of i32 range".to_string()))?;
538
539        window
540            .set_timeout_with_callback_and_timeout_and_arguments_0(
541                timeout_cb.as_ref().unchecked_ref(),
542                timeout_i32,
543            )
544            .map_err(|e| RecordingError::RecorderStartFailed(format!("{e:?}")))?;
545    }
546
547    Ok(())
548}
549
550fn pause_recording(
551    state: &mut Signal<RecordingState>,
552    _last_error: &mut Signal<Option<String>>,
553    recorder: &mut Signal<Option<MediaRecorder>>,
554) -> Result<(), RecordingError> {
555    if !matches!(*state.read(), RecordingState::Recording) {
556        return Ok(());
557    }
558
559    if let Some(r) = recorder.read().as_ref() {
560        r.request_data()
561            .map_err(|e| RecordingError::RecorderRequestDataFailed(format!("{e:?}")))?;
562        r.pause()
563            .map_err(|e| RecordingError::RecorderPauseFailed(format!("{e:?}")))?;
564        state.set(RecordingState::Paused);
565    }
566
567    Ok(())
568}
569
570fn resume_recording(
571    state: &mut Signal<RecordingState>,
572    _last_error: &mut Signal<Option<String>>,
573    recorder: &mut Signal<Option<MediaRecorder>>,
574) -> Result<(), RecordingError> {
575    if !matches!(*state.read(), RecordingState::Paused) {
576        return Ok(());
577    }
578
579    if let Some(r) = recorder.read().as_ref() {
580        r.resume()
581            .map_err(|e| RecordingError::RecorderResumeFailed(format!("{e:?}")))?;
582        state.set(RecordingState::Recording);
583    }
584
585    Ok(())
586}
587
588fn stop_recording(
589    data: &mut Signal<Option<Vec<u8>>>,
590    state: &mut Signal<RecordingState>,
591    _last_error: &mut Signal<Option<String>>,
592    recorder: &mut Signal<Option<MediaRecorder>>,
593    stream: &mut Signal<Option<MediaStream>>,
594    chunks: &mut Signal<Vec<Vec<u8>>>,
595) -> Result<(), RecordingError> {
596    if matches!(*state.read(), RecordingState::Idle) {
597        return Ok(());
598    }
599
600    state.set(RecordingState::Stopping);
601
602    if let Some(r) = recorder.read().as_ref() {
603        r.request_data()
604            .map_err(|e| RecordingError::RecorderRequestDataFailed(format!("{e:?}")))?;
605        r.stop()
606            .map_err(|e| RecordingError::RecorderStopFailed(format!("{e:?}")))?;
607    }
608
609    if let Some(s) = stream.read().as_ref() {
610        let tracks = s.get_tracks();
611        for i in 0..tracks.length() {
612            if let Ok(track) = tracks.get(i).dyn_into::<MediaStreamTrack>() {
613                track.stop();
614            }
615        }
616    }
617
618    let all: Vec<u8> = chunks.read().iter().flatten().cloned().collect();
619    data.set(Some(all));
620    chunks.write().clear();
621
622    recorder.set(None);
623    stream.set(None);
624    state.set(RecordingState::Idle);
625
626    Ok(())
627}