web_audio_api/media_recorder/
mod.rs

1//! Primitives of the Media Recorder API
2//!
3//! The MediaRecorder interface of the MediaStream Recording API provides functionality to easily
4//! record media.
5//!
6//! <https://developer.mozilla.org/en-US/docs/Web/API/MediaRecorder>
7
8use crate::media_streams::MediaStream;
9use crate::{AudioBuffer, ErrorEvent, Event};
10use std::error::Error;
11
12use std::io::Write;
13use std::sync::atomic::{AtomicBool, Ordering};
14use std::sync::{Arc, Mutex};
15use std::time::Instant;
16
17type EventCallback = Box<dyn FnOnce(Event) + Send + 'static>;
18type BlobEventCallback = Box<dyn FnMut(BlobEvent) + Send + 'static>;
19type ErrorEventCallback = Box<dyn FnOnce(ErrorEvent) + Send + 'static>;
20
21struct RecordedData {
22    blob: Vec<u8>,
23    start_timecode: Instant,
24    current_timecode: Instant,
25}
26
27impl RecordedData {
28    fn new(blob: Vec<u8>) -> Self {
29        let now = Instant::now();
30
31        Self {
32            blob,
33            start_timecode: now,
34            current_timecode: now,
35        }
36    }
37
38    /// Start encoding audio into the blob buffer
39    fn encode_first(&mut self, buf: AudioBuffer) {
40        let spec = hound::WavSpec {
41            channels: buf.number_of_channels() as u16,
42            sample_rate: buf.sample_rate() as u32,
43            bits_per_sample: 32,
44            sample_format: hound::SampleFormat::Float,
45        };
46        let v = spec.into_header_for_infinite_file();
47        self.blob.write_all(&v).unwrap();
48        self.encode_next(buf);
49    }
50
51    /// Encode subsequent buffers into the blob buffer
52    fn encode_next(&mut self, buf: AudioBuffer) {
53        for i in 0..buf.length() {
54            for c in 0..buf.number_of_channels() {
55                let v = buf.get_channel_data(c)[i];
56                hound::Sample::write(v, &mut self.blob, 32).unwrap();
57            }
58        }
59    }
60}
61
62struct MediaRecorderInner {
63    stream: MediaStream,
64    active: AtomicBool,
65    recorded_data: Mutex<RecordedData>,
66    data_available_callback: Mutex<Option<BlobEventCallback>>,
67    stop_callback: Mutex<Option<EventCallback>>,
68    error_callback: Mutex<Option<ErrorEventCallback>>,
69}
70
71impl MediaRecorderInner {
72    fn record(&self, buf: AudioBuffer) {
73        let mut recorded_data = self.recorded_data.lock().unwrap();
74
75        recorded_data.encode_next(buf);
76
77        if recorded_data.blob.len() > 128 * 1024 {
78            drop(recorded_data);
79            self.flush();
80        }
81    }
82
83    fn handle_error(&self, error: Box<dyn Error + Send + Sync>) {
84        self.flush();
85
86        if let Some(f) = self.error_callback.lock().unwrap().take() {
87            (f)(ErrorEvent {
88                message: error.to_string(),
89                error: Box::new(error),
90                event: Event {
91                    type_: "ErrorEvent",
92                },
93            })
94        }
95
96        self.stop();
97    }
98
99    fn flush(&self) {
100        let mut recorded_data = self.recorded_data.lock().unwrap();
101
102        let timecode = recorded_data
103            .current_timecode
104            .duration_since(recorded_data.start_timecode)
105            .as_secs_f64();
106
107        let data = std::mem::replace(&mut recorded_data.blob, Vec::with_capacity(128 * 1024));
108        if let Some(f) = self.data_available_callback.lock().unwrap().as_mut() {
109            let blob = Blob {
110                data,
111                type_: "audio/wav",
112            };
113            let event = BlobEvent {
114                blob,
115                timecode,
116                event: Event { type_: "BlobEvent" },
117            };
118            (f)(event)
119        }
120
121        recorded_data.current_timecode = Instant::now();
122    }
123
124    fn stop(&self) {
125        self.active.store(false, Ordering::SeqCst);
126
127        if let Some(f) = self.stop_callback.lock().unwrap().take() {
128            (f)(Event { type_: "StopEvent" })
129        }
130    }
131}
132
133#[non_exhaustive]
134#[derive(Debug, Clone, Default)]
135/// Dictionary with media recorder options
136pub struct MediaRecorderOptions {
137    /// The container and codec format(s) for the recording, which may include any parameters that
138    /// are defined for the format. Defaults to `""` which means any suitable mimeType is picked.
139    pub mime_type: String,
140}
141
142/// Record and encode media
143///
144/// ```no_run
145/// use web_audio_api::context::AudioContext;
146/// use web_audio_api::media_recorder::{MediaRecorder, MediaRecorderOptions};
147///
148/// let context = AudioContext::default();
149/// let output = context.create_media_stream_destination();
150///
151/// let options = MediaRecorderOptions::default(); // default to audio/wav
152/// let recorder = MediaRecorder::new(output.stream(), options);
153/// recorder.set_ondataavailable(|event| {
154///     println!("Received {} bytes of data", event.blob.size());
155/// });
156/// recorder.start();
157/// ```
158///
159/// # Examples
160///
161/// - `cargo run --release --example recorder`
162pub struct MediaRecorder {
163    inner: Arc<MediaRecorderInner>,
164}
165
166impl std::fmt::Debug for MediaRecorder {
167    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
168        f.debug_struct("MediaRecorder")
169            .field("stream", &self.inner.stream)
170            .field("active", &self.inner.active)
171            .finish_non_exhaustive()
172    }
173}
174
175impl MediaRecorder {
176    /// A static method which returns a true or false value indicating if the given MIME media type
177    /// is supported.
178    pub fn is_type_supported(mime_type: &str) -> bool {
179        // only WAV supported for now #508
180        match mime_type {
181            "" => true, // we are free to pick a supported mime type
182            "audio/wav" => true,
183            _ => false,
184        }
185    }
186
187    /// Creates a new `MediaRecorder` object, given a [`MediaStream`] to record.
188    ///
189    /// Only supports WAV file format currently, so `options.mime_type` should be set to
190    /// `audio/wav` or left empty.
191    ///
192    /// # Panics
193    ///
194    /// This function will panic with a `NotSupportedError` when the provided mime type is not
195    /// supported. Be sure to check [`Self::is_type_supported`] before calling this constructor.
196    pub fn new(stream: &MediaStream, options: MediaRecorderOptions) -> Self {
197        assert!(
198            Self::is_type_supported(&options.mime_type),
199            "NotSupportedError - the provided mime type is not supported"
200        );
201        // TODO #508 actually use options.mime_type
202
203        let inner = MediaRecorderInner {
204            stream: stream.clone(),
205            active: AtomicBool::new(false),
206            recorded_data: Mutex::new(RecordedData::new(vec![])),
207            data_available_callback: Mutex::new(None),
208            stop_callback: Mutex::new(None),
209            error_callback: Mutex::new(None),
210        };
211
212        Self {
213            inner: Arc::new(inner),
214        }
215    }
216
217    #[allow(clippy::missing_panics_doc)]
218    pub fn set_ondataavailable<F: FnMut(BlobEvent) + Send + 'static>(&self, callback: F) {
219        *self.inner.data_available_callback.lock().unwrap() = Some(Box::new(callback));
220    }
221
222    #[allow(clippy::missing_panics_doc)]
223    pub fn clear_ondataavailable(&self) {
224        *self.inner.data_available_callback.lock().unwrap() = None;
225    }
226
227    #[allow(clippy::missing_panics_doc)]
228    pub fn set_onstop<F: FnOnce(Event) + Send + 'static>(&self, callback: F) {
229        *self.inner.stop_callback.lock().unwrap() = Some(Box::new(callback));
230    }
231
232    #[allow(clippy::missing_panics_doc)]
233    pub fn clear_onstop(&self) {
234        *self.inner.stop_callback.lock().unwrap() = None;
235    }
236
237    #[allow(clippy::missing_panics_doc)]
238    pub fn set_onerror<F: FnOnce(ErrorEvent) + Send + 'static>(&self, callback: F) {
239        *self.inner.error_callback.lock().unwrap() = Some(Box::new(callback));
240    }
241
242    #[allow(clippy::missing_panics_doc)]
243    pub fn clear_onerror(&self) {
244        *self.inner.error_callback.lock().unwrap() = None;
245    }
246
247    /// Begin recording media
248    ///
249    /// # Panics
250    ///
251    /// Will panic when the recorder has already started
252    pub fn start(&self) {
253        let prev_active = self.inner.active.swap(true, Ordering::Relaxed);
254        assert!(
255            !prev_active,
256            "InvalidStateError - recorder has already started"
257        );
258
259        let inner = Arc::clone(&self.inner);
260        let blob = Vec::with_capacity(128 * 1024);
261
262        std::thread::spawn(move || {
263            // for now, only record single track
264            let mut stream_iter = inner.stream.get_tracks()[0].iter();
265            let buf = match stream_iter.next() {
266                None => return,
267                Some(Err(error)) => {
268                    inner.handle_error(error);
269                    return;
270                }
271                Some(Ok(first)) => first,
272            };
273
274            let mut recorded_data = RecordedData::new(blob);
275            recorded_data.encode_first(buf);
276            *inner.recorded_data.lock().unwrap() = recorded_data;
277
278            for item in stream_iter {
279                if !inner.active.load(Ordering::Relaxed) {
280                    return; // recording has stopped
281                }
282
283                let buf = match item {
284                    Ok(buf) => buf,
285                    Err(error) => {
286                        inner.handle_error(error);
287                        return;
288                    }
289                };
290
291                inner.record(buf);
292            }
293
294            inner.flush();
295            inner.stop();
296        });
297    }
298
299    pub fn stop(&self) {
300        self.inner.flush();
301        self.inner.stop();
302    }
303}
304
305/// Interface for the `dataavailable` event, containing the recorded data
306#[non_exhaustive]
307#[derive(Debug)]
308pub struct BlobEvent {
309    /// The encoded Blob whose type attribute indicates the encoding of the blob data.
310    pub blob: Blob,
311    /// The difference between the timestamp of the first chunk in data and the timestamp of the
312    /// first chunk in the first BlobEvent produced by this recorder
313    pub timecode: f64,
314    /// Inherits from this base Event
315    pub event: Event,
316}
317
318#[derive(Debug)]
319pub struct Blob {
320    /// Byte sequence of this blob
321    pub data: Vec<u8>,
322    type_: &'static str,
323}
324
325impl Blob {
326    /// Returns the size of the byte sequence in number of bytes
327    pub fn size(&self) -> usize {
328        self.data.len()
329    }
330
331    /// The ASCII-encoded string in lower case representing the media type
332    pub fn type_(&self) -> &str {
333        self.type_
334    }
335}
336
337#[cfg(test)]
338mod tests {
339    use crate::context::{BaseAudioContext, OfflineAudioContext};
340    use crate::media_streams::MediaStreamTrack;
341    use float_eq::assert_float_eq;
342    use std::io::Cursor;
343
344    use super::*;
345
346    #[test]
347    fn test_record() {
348        let data_received = Arc::new(AtomicBool::new(false));
349
350        let buffers = vec![
351            Ok(AudioBuffer::from(vec![vec![1.; 1024]], 48000.)),
352            Ok(AudioBuffer::from(vec![vec![2.; 1024]], 48000.)),
353            Ok(AudioBuffer::from(vec![vec![3.; 1024]], 48000.)),
354        ];
355        let track = MediaStreamTrack::from_iter(buffers);
356        let stream = MediaStream::from_tracks(vec![track]);
357        let recorder = MediaRecorder::new(&stream, Default::default());
358
359        {
360            let data_received = Arc::clone(&data_received);
361            recorder.set_ondataavailable(move |_| data_received.store(true, Ordering::Relaxed));
362        }
363
364        // setup channel to await recorder completion
365        let (send, recv) = crossbeam_channel::bounded(1);
366        recorder.set_onstop(move |_| {
367            let _ = send.send(());
368        });
369
370        recorder.start();
371
372        let _ = recv.recv();
373        assert!(data_received.load(Ordering::Relaxed));
374    }
375
376    #[test]
377    fn test_error() {
378        let data_received = Arc::new(AtomicBool::new(false));
379        let error_received = Arc::new(AtomicBool::new(false));
380
381        let buffers = vec![
382            Ok(AudioBuffer::from(vec![vec![1.; 1024]], 48000.)),
383            Err(String::from("error").into()),
384            Ok(AudioBuffer::from(vec![vec![3.; 1024]], 48000.)),
385        ];
386        let track = MediaStreamTrack::from_iter(buffers);
387        let stream = MediaStream::from_tracks(vec![track]);
388        let recorder = MediaRecorder::new(&stream, Default::default());
389
390        {
391            let data_received = Arc::clone(&data_received);
392            recorder.set_ondataavailable(move |_| data_received.store(true, Ordering::Relaxed));
393        }
394        {
395            let error_received = Arc::clone(&error_received);
396            recorder.set_onerror(move |_| error_received.store(true, Ordering::Relaxed));
397        }
398
399        // setup channel to await recorder completion
400        let (send, recv) = crossbeam_channel::bounded(1);
401        recorder.set_onstop(move |_| {
402            let _ = send.send(());
403        });
404
405        recorder.start();
406
407        let _ = recv.recv();
408        assert!(data_received.load(Ordering::Relaxed));
409        assert!(error_received.load(Ordering::Relaxed));
410    }
411
412    #[test]
413    fn test_encode_decode() {
414        let buffers = vec![Ok(AudioBuffer::from(
415            vec![vec![1.; 1024], vec![-1.; 1024]],
416            48000.,
417        ))];
418        let track = MediaStreamTrack::from_iter(buffers);
419        let stream = MediaStream::from_tracks(vec![track]);
420        let recorder = MediaRecorder::new(&stream, Default::default());
421
422        let samples: Arc<Mutex<Vec<u8>>> = Default::default();
423        {
424            let samples = Arc::clone(&samples);
425            recorder.set_ondataavailable(move |e| {
426                samples.lock().unwrap().extend_from_slice(&e.blob.data);
427            });
428        }
429
430        // setup channel to await recorder completion
431        let (send, recv) = crossbeam_channel::bounded(1);
432        recorder.set_onstop(move |_| {
433            let _ = send.send(());
434        });
435
436        recorder.start();
437        let _ = recv.recv();
438
439        let samples = samples.lock().unwrap().clone();
440
441        let ctx = OfflineAudioContext::new(1, 128, 48000.);
442        let buf = ctx.decode_audio_data_sync(Cursor::new(samples)).unwrap();
443        assert_eq!(buf.number_of_channels(), 2);
444        assert_eq!(buf.length(), 1024);
445        assert_float_eq!(buf.get_channel_data(0), &[1.; 1024][..], abs_all <= 0.);
446        assert_float_eq!(buf.get_channel_data(1), &[-1.; 1024][..], abs_all <= 0.);
447    }
448}