1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
//! Primitives of the MediaDevices API
//!
//! The MediaDevices interface provides access to connected media input devices like microphones.
//!
//! <https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices>

use rustc_hash::FxHasher;
use std::hash::{Hash, Hasher};

use crate::context::{AudioContextLatencyCategory, AudioContextOptions};
use crate::media_streams::MediaStream;

/// List the available media output devices, such as speakers, headsets, loopbacks, etc
///
/// The media device_id can be used to specify the [`sink_id` of the `AudioContext`](crate::context::AudioContextOptions::sink_id)
///
/// ```no_run
/// use web_audio_api::media_devices::{enumerate_devices_sync, MediaDeviceInfoKind};
///
/// let devices = enumerate_devices_sync();
/// assert_eq!(devices[0].device_id(), "1");
/// assert_eq!(devices[0].group_id(), None);
/// assert_eq!(devices[0].kind(), MediaDeviceInfoKind::AudioOutput);
/// assert_eq!(devices[0].label(), "Macbook Pro Builtin Speakers");
/// ```
pub fn enumerate_devices_sync() -> Vec<MediaDeviceInfo> {
    crate::io::enumerate_devices_sync()
}

// Internal struct to derive a stable id for a given input / output device
// cf. https://github.com/orottier/web-audio-api-rs/issues/356
#[derive(Hash)]
pub(crate) struct DeviceId {
    kind: MediaDeviceInfoKind,
    host: String,
    device_name: String,
    num_channels: u16,
    index: u8,
}

impl DeviceId {
    pub(crate) fn as_string(
        kind: MediaDeviceInfoKind,
        host: String,
        device_name: String,
        num_channels: u16,
        index: u8,
    ) -> String {
        let device_info = Self {
            kind,
            host,
            device_name,
            num_channels,
            index,
        };

        let mut hasher = FxHasher::default();
        device_info.hash(&mut hasher);
        format!("{}", hasher.finish())
    }
}

/// Describes input/output type of a media device
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub enum MediaDeviceInfoKind {
    VideoInput,
    AudioInput,
    AudioOutput,
}

/// Describes a single media input or output device
///
/// Call [`enumerate_devices_sync`] to obtain a list of devices for your hardware.
#[derive(Debug)]
pub struct MediaDeviceInfo {
    device_id: String,
    group_id: Option<String>,
    kind: MediaDeviceInfoKind,
    label: String,
    device: Box<dyn std::any::Any>,
}

impl MediaDeviceInfo {
    pub(crate) fn new(
        device_id: String,
        group_id: Option<String>,
        kind: MediaDeviceInfoKind,
        label: String,
        device: Box<dyn std::any::Any>,
    ) -> Self {
        Self {
            device_id,
            group_id,
            kind,
            label,
            device,
        }
    }

    /// Identifier for the represented device
    ///
    /// The current implementation is not stable across sessions so you should not persist this
    /// value
    pub fn device_id(&self) -> &str {
        &self.device_id
    }

    /// Two devices have the same group identifier if they belong to the same physical device
    pub fn group_id(&self) -> Option<&str> {
        self.group_id.as_deref()
    }

    /// Enumerated value that is either "videoinput", "audioinput" or "audiooutput".
    pub fn kind(&self) -> MediaDeviceInfoKind {
        self.kind
    }

    /// Friendly label describing this device
    pub fn label(&self) -> &str {
        &self.label
    }

    pub(crate) fn device(self) -> Box<dyn std::any::Any> {
        self.device
    }
}

/// Dictionary used to instruct what sort of tracks to include in the [`MediaStream`] returned by
/// [`get_user_media_sync`]
pub enum MediaStreamConstraints {
    Audio,
    AudioWithConstraints(MediaTrackConstraints),
}

/// Desired media stream track settings for [`MediaTrackConstraints`]
#[derive(Default)]
#[non_exhaustive]
pub struct MediaTrackConstraints {
    // ConstrainULong width;
    // ConstrainULong height;
    // ConstrainDouble aspectRatio;
    // ConstrainDouble frameRate;
    // ConstrainDOMString facingMode;
    // ConstrainDOMString resizeMode;
    pub sample_rate: Option<f32>,
    // ConstrainULong sampleSize;
    // ConstrainBoolean echoCancellation;
    // ConstrainBoolean autoGainControl;
    // ConstrainBoolean noiseSuppression;
    pub latency: Option<f64>,
    //ConstrainULong channelCount;
    pub device_id: Option<String>,
    // ConstrainDOMString groupId;
}

impl From<MediaTrackConstraints> for AudioContextOptions {
    fn from(value: MediaTrackConstraints) -> Self {
        let latency_hint = match value.latency {
            Some(v) => AudioContextLatencyCategory::Custom(v),
            None => AudioContextLatencyCategory::Interactive,
        };
        let sink_id = value.device_id.unwrap_or(String::from(""));

        AudioContextOptions {
            latency_hint,
            sample_rate: value.sample_rate,
            sink_id,
            render_size_hint: Default::default(),
        }
    }
}

/// Check if the provided device_id is available for playback
///
/// It should be "" or a valid input `deviceId` returned from [`enumerate_devices_sync`]
fn is_valid_device_id(device_id: &str) -> bool {
    if device_id.is_empty() {
        true
    } else {
        enumerate_devices_sync()
            .into_iter()
            .filter(|d| d.kind == MediaDeviceInfoKind::AudioInput)
            .any(|d| d.device_id() == device_id)
    }
}

/// Prompt for permission to use a media input (audio only)
///
/// This produces a [`MediaStream`] with tracks containing the requested types of media, which can
/// be used inside a [`MediaStreamAudioSourceNode`](crate::node::MediaStreamAudioSourceNode).
///
/// It is okay for the `MediaStream` struct to go out of scope, any corresponding stream will still be
/// kept alive and emit audio buffers. Call the `close()` method if you want to stop the media
/// input and release all system resources.
///
/// This function operates synchronously, which may be undesirable on the control thread. An async
/// version is currently not implemented.
///
/// # Example
///
/// ```no_run
/// use web_audio_api::context::{BaseAudioContext, AudioContext};
/// use web_audio_api::context::{AudioContextLatencyCategory, AudioContextOptions};
/// use web_audio_api::media_devices;
/// use web_audio_api::media_devices::MediaStreamConstraints;
/// use web_audio_api::node::AudioNode;
///
/// let context = AudioContext::default();
/// let mic = media_devices::get_user_media_sync(MediaStreamConstraints::Audio);
///
/// // register as media element in the audio context
/// let background = context.create_media_stream_source(&mic);
///
/// // connect the node directly to the destination node (speakers)
/// background.connect(&context.destination());
///
/// // enjoy listening
/// std::thread::sleep(std::time::Duration::from_secs(4));
/// ```
pub fn get_user_media_sync(constraints: MediaStreamConstraints) -> MediaStream {
    let mut options = match constraints {
        MediaStreamConstraints::Audio => AudioContextOptions::default(),
        MediaStreamConstraints::AudioWithConstraints(cs) => cs.into(),
    };

    if !is_valid_device_id(&options.sink_id) {
        log::error!("NotFoundError: invalid deviceId {:?}", options.sink_id);
        options.sink_id = String::from("");
    }

    crate::io::build_input(options)
}