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#[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 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 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 .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(); chunks_inner.write().push(bytes);
339 }
340 }
341 });
342 }) as Box<dyn FnMut(_)>);
343
344 rec.set_ondataavailable(Some(ondata.as_ref().unchecked_ref())); ondata.forget(); 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() .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}