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}