Skip to main content

web_audio_api/media_devices/
mod.rs

1//! Primitives of the MediaDevices API
2//!
3//! The MediaDevices interface provides access to connected media input devices like microphones.
4//!
5//! <https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices>
6
7use std::collections::hash_map::DefaultHasher;
8use std::hash::{Hash, Hasher};
9
10use crate::context::{AudioContextLatencyCategory, AudioContextOptions};
11use crate::io::BackendResult;
12use crate::media_streams::MediaStream;
13
14/// List the available media output devices, such as speakers, headsets, loopbacks, etc
15///
16/// The media device_id can be used to specify the [`sink_id` of the `AudioContext`](crate::context::AudioContextOptions::sink_id)
17///
18/// ```no_run
19/// use web_audio_api::media_devices::{enumerate_devices_sync, MediaDeviceInfoKind};
20///
21/// let devices = enumerate_devices_sync();
22/// assert_eq!(devices[0].device_id(), "1");
23/// assert_eq!(devices[0].group_id(), None);
24/// assert_eq!(devices[0].kind(), MediaDeviceInfoKind::AudioOutput);
25/// assert_eq!(devices[0].label(), "Macbook Pro Builtin Speakers");
26/// ```
27pub fn enumerate_devices_sync() -> Vec<MediaDeviceInfo> {
28    try_enumerate_devices_sync().unwrap_or_else(|e| {
29        log::error!("Unable to enumerate media devices: {e}");
30        vec![]
31    })
32}
33
34pub(crate) fn try_enumerate_devices_sync() -> BackendResult<Vec<MediaDeviceInfo>> {
35    crate::io::enumerate_devices_sync()
36}
37
38// Internal struct to derive a stable id for a given input / output device
39// cf. https://github.com/orottier/web-audio-api-rs/issues/356
40#[derive(Hash)]
41pub(crate) struct DeviceId {
42    kind: MediaDeviceInfoKind,
43    host: String,
44    device_name: String,
45    num_channels: u16,
46    index: u8,
47}
48
49impl DeviceId {
50    pub(crate) fn as_string(
51        kind: MediaDeviceInfoKind,
52        host: String,
53        device_name: String,
54        num_channels: u16,
55        index: u8,
56    ) -> String {
57        let device_info = Self {
58            kind,
59            host,
60            device_name,
61            num_channels,
62            index,
63        };
64
65        let mut hasher = DefaultHasher::new();
66        device_info.hash(&mut hasher);
67        format!("{}", hasher.finish())
68    }
69}
70
71/// Describes input/output type of a media device
72#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
73pub enum MediaDeviceInfoKind {
74    VideoInput,
75    AudioInput,
76    AudioOutput,
77}
78
79/// Describes a single media input or output device
80///
81/// Call [`enumerate_devices_sync`] to obtain a list of devices for your hardware.
82#[derive(Debug)]
83pub struct MediaDeviceInfo {
84    device_id: String,
85    group_id: Option<String>,
86    kind: MediaDeviceInfoKind,
87    label: String,
88    device: Box<dyn std::any::Any>,
89}
90
91impl MediaDeviceInfo {
92    pub(crate) fn new(
93        device_id: String,
94        group_id: Option<String>,
95        kind: MediaDeviceInfoKind,
96        label: String,
97        device: Box<dyn std::any::Any>,
98    ) -> Self {
99        Self {
100            device_id,
101            group_id,
102            kind,
103            label,
104            device,
105        }
106    }
107
108    /// Identifier for the represented device
109    ///
110    /// The current implementation is not stable across sessions so you should not persist this
111    /// value
112    pub fn device_id(&self) -> &str {
113        &self.device_id
114    }
115
116    /// Two devices have the same group identifier if they belong to the same physical device
117    pub fn group_id(&self) -> Option<&str> {
118        self.group_id.as_deref()
119    }
120
121    /// Enumerated value that is either "videoinput", "audioinput" or "audiooutput".
122    pub fn kind(&self) -> MediaDeviceInfoKind {
123        self.kind
124    }
125
126    /// Friendly label describing this device
127    pub fn label(&self) -> &str {
128        &self.label
129    }
130
131    pub(crate) fn device(self) -> Box<dyn std::any::Any> {
132        self.device
133    }
134}
135
136/// Dictionary used to instruct what sort of tracks to include in the [`MediaStream`] returned by
137/// [`get_user_media_sync`]
138#[derive(Clone, Debug)]
139pub enum MediaStreamConstraints {
140    Audio,
141    AudioWithConstraints(MediaTrackConstraints),
142}
143
144/// Desired media stream track settings for [`MediaTrackConstraints`]
145#[derive(Default, Debug, Clone)]
146#[non_exhaustive]
147pub struct MediaTrackConstraints {
148    // ConstrainULong width;
149    // ConstrainULong height;
150    // ConstrainDouble aspectRatio;
151    // ConstrainDouble frameRate;
152    // ConstrainDOMString facingMode;
153    // ConstrainDOMString resizeMode;
154    pub sample_rate: Option<f32>,
155    // ConstrainULong sampleSize;
156    // ConstrainBoolean echoCancellation;
157    // ConstrainBoolean autoGainControl;
158    // ConstrainBoolean noiseSuppression;
159    pub latency: Option<f64>,
160    pub channel_count: Option<u32>, // TODO model as ConstrainULong;
161    pub device_id: Option<String>,
162    // ConstrainDOMString groupId;
163}
164
165impl From<MediaTrackConstraints> for AudioContextOptions {
166    fn from(value: MediaTrackConstraints) -> Self {
167        let latency_hint = match value.latency {
168            Some(v) => AudioContextLatencyCategory::Custom(v),
169            None => AudioContextLatencyCategory::Interactive,
170        };
171        let sink_id = value.device_id.unwrap_or(String::from(""));
172
173        AudioContextOptions {
174            latency_hint,
175            sample_rate: value.sample_rate,
176            sink_id,
177            render_size_hint: Default::default(),
178        }
179    }
180}
181
182/// Check if the provided device_id is available for playback
183///
184/// It should be "" or a valid input `deviceId` returned from [`enumerate_devices_sync`]
185fn is_valid_device_id(device_id: &str) -> bool {
186    if device_id.is_empty() {
187        true
188    } else {
189        enumerate_devices_sync()
190            .into_iter()
191            .filter(|d| d.kind == MediaDeviceInfoKind::AudioInput)
192            .any(|d| d.device_id() == device_id)
193    }
194}
195
196/// Prompt for permission to use a media input (audio only)
197///
198/// This produces a [`MediaStream`] with tracks containing the requested types of media, which can
199/// be used inside a [`MediaStreamAudioSourceNode`](crate::node::MediaStreamAudioSourceNode).
200///
201/// It is okay for the `MediaStream` struct to go out of scope, any corresponding stream will still be
202/// kept alive and emit audio buffers. Call the `close()` method if you want to stop the media
203/// input and release all system resources.
204///
205/// This function operates synchronously, which may be undesirable on the control thread. An async
206/// version is currently not implemented.
207///
208/// # Panics
209///
210/// This function will panic when the selected audio backend cannot create or start the input
211/// stream. A public `try_get_user_media_sync` could be added in the future to handle these errors
212/// without panicking, similar to [`AudioContext::try_new`](crate::context::AudioContext::new).
213///
214/// # Example
215///
216/// ```no_run
217/// use web_audio_api::context::{BaseAudioContext, AudioContext};
218/// use web_audio_api::context::{AudioContextLatencyCategory, AudioContextOptions};
219/// use web_audio_api::media_devices;
220/// use web_audio_api::media_devices::MediaStreamConstraints;
221/// use web_audio_api::node::AudioNode;
222///
223/// let context = AudioContext::default();
224/// let mic = media_devices::get_user_media_sync(MediaStreamConstraints::Audio);
225///
226/// // register as media element in the audio context
227/// let background = context.create_media_stream_source(&mic);
228///
229/// // connect the node directly to the destination node (speakers)
230/// background.connect(&context.destination());
231///
232/// // enjoy listening
233/// std::thread::sleep(std::time::Duration::from_secs(4));
234/// ```
235pub fn get_user_media_sync(constraints: MediaStreamConstraints) -> MediaStream {
236    try_get_user_media_sync(constraints).unwrap_or_else(|e| panic!("InvalidStateError - {e}"))
237}
238
239pub(crate) fn try_get_user_media_sync(
240    constraints: MediaStreamConstraints,
241) -> BackendResult<MediaStream> {
242    let (channel_count, mut options) = match constraints {
243        MediaStreamConstraints::Audio => (None, AudioContextOptions::default()),
244        MediaStreamConstraints::AudioWithConstraints(cs) => (cs.channel_count, cs.into()),
245    };
246
247    if !is_valid_device_id(&options.sink_id) {
248        log::error!("NotFoundError: invalid deviceId {:?}", options.sink_id);
249        options.sink_id = String::from("");
250    }
251
252    crate::io::build_input(options, channel_count)
253}