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