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}