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#[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#[derive(Debug, Clone, Copy, Default)]
126pub struct RecordingConfig {
127 pub time_slice: Option<u32>, pub max_duration: Option<u32>, }
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
164impl Drop for Recording {
166 fn drop(&mut self) {
167 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 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 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 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 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(); 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}