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}
89
90impl MediaDeviceInfo {
91    pub(crate) fn new(
92        device_id: String,
93        group_id: Option<String>,
94        kind: MediaDeviceInfoKind,
95        label: String,
96    ) -> Self {
97        Self {
98            device_id,
99            group_id,
100            kind,
101            label,
102        }
103    }
104
105    /// Identifier for the represented device
106    ///
107    /// The current implementation is not stable across sessions so you should not persist this
108    /// value
109    pub fn device_id(&self) -> &str {
110        &self.device_id
111    }
112
113    /// Two devices have the same group identifier if they belong to the same physical device
114    pub fn group_id(&self) -> Option<&str> {
115        self.group_id.as_deref()
116    }
117
118    /// Enumerated value that is either "videoinput", "audioinput" or "audiooutput".
119    pub fn kind(&self) -> MediaDeviceInfoKind {
120        self.kind
121    }
122
123    /// Friendly label describing this device
124    pub fn label(&self) -> &str {
125        &self.label
126    }
127}
128
129/// Dictionary used to instruct what sort of tracks to include in the [`MediaStream`] returned by
130/// [`get_user_media_sync`]
131#[derive(Clone, Debug)]
132pub enum MediaStreamConstraints {
133    Audio,
134    AudioWithConstraints(MediaTrackConstraints),
135}
136
137/// Desired media stream track settings for [`MediaTrackConstraints`]
138#[derive(Default, Debug, Clone)]
139#[non_exhaustive]
140pub struct MediaTrackConstraints {
141    // ConstrainULong width;
142    // ConstrainULong height;
143    // ConstrainDouble aspectRatio;
144    // ConstrainDouble frameRate;
145    // ConstrainDOMString facingMode;
146    // ConstrainDOMString resizeMode;
147    pub sample_rate: Option<f32>,
148    // ConstrainULong sampleSize;
149    // ConstrainBoolean echoCancellation;
150    // ConstrainBoolean autoGainControl;
151    // ConstrainBoolean noiseSuppression;
152    pub latency: Option<f64>,
153    pub channel_count: Option<u32>, // TODO model as ConstrainULong;
154    pub device_id: Option<String>,
155    // ConstrainDOMString groupId;
156}
157
158impl From<MediaTrackConstraints> for AudioContextOptions {
159    fn from(value: MediaTrackConstraints) -> Self {
160        let latency_hint = match value.latency {
161            Some(v) => AudioContextLatencyCategory::Custom(v),
162            None => AudioContextLatencyCategory::Interactive,
163        };
164        let sink_id = value.device_id.unwrap_or(String::from(""));
165
166        AudioContextOptions {
167            latency_hint,
168            sample_rate: value.sample_rate,
169            sink_id,
170            render_size_hint: Default::default(),
171        }
172    }
173}
174
175/// Check if the provided device_id is available for playback
176///
177/// It should be "" or a valid input `deviceId` returned from [`enumerate_devices_sync`]
178fn is_valid_device_id(device_id: &str) -> bool {
179    if device_id.is_empty() {
180        true
181    } else {
182        enumerate_devices_sync()
183            .into_iter()
184            .filter(|d| d.kind == MediaDeviceInfoKind::AudioInput)
185            .any(|d| d.device_id() == device_id)
186    }
187}
188
189/// Prompt for permission to use a media input (audio only)
190///
191/// This produces a [`MediaStream`] with tracks containing the requested types of media, which can
192/// be used inside a [`MediaStreamAudioSourceNode`](crate::node::MediaStreamAudioSourceNode).
193///
194/// It is okay for the `MediaStream` struct to go out of scope, any corresponding stream will still be
195/// kept alive and emit audio buffers. Call the `close()` method if you want to stop the media
196/// input and release all system resources.
197///
198/// This function operates synchronously, which may be undesirable on the control thread. An async
199/// version is currently not implemented.
200///
201/// # Panics
202///
203/// This function will panic when the selected audio backend cannot create or start the input
204/// stream. A public `try_get_user_media_sync` could be added in the future to handle these errors
205/// without panicking, similar to [`AudioContext::try_new`](crate::context::AudioContext::new).
206///
207/// # Example
208///
209/// ```no_run
210/// use web_audio_api::context::{BaseAudioContext, AudioContext};
211/// use web_audio_api::context::{AudioContextLatencyCategory, AudioContextOptions};
212/// use web_audio_api::media_devices;
213/// use web_audio_api::media_devices::MediaStreamConstraints;
214/// use web_audio_api::node::AudioNode;
215///
216/// let context = AudioContext::default();
217/// let mic = media_devices::get_user_media_sync(MediaStreamConstraints::Audio);
218///
219/// // register as media element in the audio context
220/// let background = context.create_media_stream_source(&mic);
221///
222/// // connect the node directly to the destination node (speakers)
223/// background.connect(&context.destination());
224///
225/// // enjoy listening
226/// std::thread::sleep(std::time::Duration::from_secs(4));
227/// ```
228pub fn get_user_media_sync(constraints: MediaStreamConstraints) -> MediaStream {
229    try_get_user_media_sync(constraints).unwrap_or_else(|e| panic!("InvalidStateError - {e}"))
230}
231
232pub(crate) fn try_get_user_media_sync(
233    constraints: MediaStreamConstraints,
234) -> BackendResult<MediaStream> {
235    let (channel_count, mut options) = match constraints {
236        MediaStreamConstraints::Audio => (None, AudioContextOptions::default()),
237        MediaStreamConstraints::AudioWithConstraints(cs) => (cs.channel_count, cs.into()),
238    };
239
240    if !is_valid_device_id(&options.sink_id) {
241        log::error!("NotFoundError: invalid deviceId {:?}", options.sink_id);
242        options.sink_id = String::from("");
243    }
244
245    crate::io::build_input(options, channel_count)
246}