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}