1use dioxus::prelude::*;
4
5use crate::AudioError;
6#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
7use crate::AudioErrorKind;
8use crate::{AudioInputDevice, AudioInputId};
9
10pub fn reconcile_selected_device(
13 selected: Option<AudioInputId>,
14 devices: &[AudioInputDevice],
15) -> Option<AudioInputId> {
16 selected.filter(|selected_id| devices.iter().any(|device| device.id == *selected_id))
17}
18
19#[derive(Clone, Copy, Debug, PartialEq, Eq)]
20#[non_exhaustive]
21pub enum MicrophonePermission {
22 Unsupported,
23 Unknown,
24 Prompt,
25 Granted,
26 Denied,
27}
28
29#[derive(Clone, Debug, PartialEq, Eq)]
30#[non_exhaustive]
31pub enum DeviceListStatus {
32 Unsupported,
33 Loading,
34 Ready,
35 Failed(AudioError),
36}
37
38#[derive(Clone, Copy, PartialEq)]
39pub struct AudioInputDevices {
40 devices: ReadSignal<Vec<AudioInputDevice>>,
41 selected: Signal<Option<AudioInputId>>,
42 status: ReadSignal<DeviceListStatus>,
43 permission: ReadSignal<MicrophonePermission>,
44 refresh: Callback,
45 request_permission: Callback,
46}
47
48impl AudioInputDevices {
49 pub fn devices(self) -> ReadSignal<Vec<AudioInputDevice>> {
50 self.devices
51 }
52
53 pub fn selected(self) -> Signal<Option<AudioInputId>> {
54 self.selected
55 }
56
57 pub fn status(self) -> ReadSignal<DeviceListStatus> {
58 self.status
59 }
60
61 pub fn permission(self) -> ReadSignal<MicrophonePermission> {
62 self.permission
63 }
64
65 pub fn select(mut self, id: Option<AudioInputId>) {
66 self.selected.set(id);
67 }
68
69 pub fn refresh(self) {
70 self.refresh.call(());
71 }
72
73 pub fn request_permission(self) {
74 self.request_permission.call(());
75 }
76}
77
78pub fn use_audio_input_devices() -> AudioInputDevices {
79 let devices = use_signal(Vec::<AudioInputDevice>::new);
80 let selected = use_signal(|| None::<AudioInputId>);
81
82 #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
83 let mut status = use_signal(|| DeviceListStatus::Loading);
84 #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
85 let mut status = use_signal(|| DeviceListStatus::Loading);
86
87 #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
88 let mut permission = use_signal(|| MicrophonePermission::Unknown);
89 #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
90 let mut permission = use_signal(|| MicrophonePermission::Unknown);
91
92 #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
93 use_effect(move || {
94 status.set(DeviceListStatus::Unsupported);
95 permission.set(MicrophonePermission::Unsupported);
96 });
97
98 #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
99 let refresh_generation = use_hook(|| std::rc::Rc::new(std::cell::Cell::new(0_u64)));
100 #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
101 let permission_runtime = use_hook(|| std::rc::Rc::new(PermissionRuntime::default()));
102 #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
103 {
104 let permission_runtime = permission_runtime.clone();
105 use_hook(|| std::rc::Rc::new(PermissionUnmountGuard(permission_runtime)));
106 }
107
108 #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
109 let refresh = use_callback(move |()| {
110 let request = refresh_generation.get().wrapping_add(1);
111 refresh_generation.set(request);
112 status.set(DeviceListStatus::Loading);
113 let refresh_generation = refresh_generation.clone();
114 spawn(async move {
115 let result = web::enumerate_inputs().await;
116 if refresh_generation.get() != request {
117 return;
118 }
119 match result {
120 Ok(latest) => {
121 if latest.iter().any(|device| !device.label.is_empty()) {
122 permission.set(MicrophonePermission::Granted);
123 }
124 let reconciled = reconcile_selected_device(selected(), &latest);
125 if reconciled != selected() {
126 let mut selected = selected;
127 selected.set(reconciled);
128 }
129 let mut devices = devices;
130 devices.set(latest);
131 status.set(DeviceListStatus::Ready);
132 }
133 Err(error) => status.set(DeviceListStatus::Failed(error)),
134 }
135 });
136 });
137
138 #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
139 let refresh = use_callback(|()| {});
140
141 #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
142 let request_permission = use_callback(move |()| {
143 let request = permission_runtime.generation.get().wrapping_add(1);
144 permission_runtime.generation.set(request);
145 permission.set(MicrophonePermission::Prompt);
146 let permission_runtime = permission_runtime.clone();
147 let dioxus_runtime = dioxus::core::Runtime::current();
148 let dioxus_scope = dioxus_runtime.current_scope_id();
149 wasm_bindgen_futures::spawn_local(async move {
150 let result = web::request_microphone_permission().await;
151 if !permission_runtime.mounted.get() || permission_runtime.generation.get() != request {
152 return;
153 }
154 dioxus_runtime.in_scope(dioxus_scope, || match result {
155 Ok(()) => {
156 permission.set(MicrophonePermission::Granted);
157 refresh.call(());
158 }
159 Err(error) => {
160 permission.set(if error.kind() == AudioErrorKind::PermissionDenied {
161 MicrophonePermission::Denied
162 } else {
163 MicrophonePermission::Unknown
164 });
165 status.set(DeviceListStatus::Failed(error));
166 }
167 });
168 });
169 });
170
171 #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
172 let request_permission = use_callback(|()| {});
173
174 #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
175 {
176 use_hook(move || {
177 refresh.call(());
178 std::rc::Rc::new(web::DeviceChangeListener::new(refresh))
179 });
180 }
181
182 AudioInputDevices {
183 devices: devices.into(),
184 selected,
185 status: status.into(),
186 permission: permission.into(),
187 refresh,
188 request_permission,
189 }
190}
191
192#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
193struct PermissionRuntime {
194 generation: std::cell::Cell<u64>,
195 mounted: std::cell::Cell<bool>,
196}
197
198#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
199impl Default for PermissionRuntime {
200 fn default() -> Self {
201 Self {
202 generation: std::cell::Cell::new(0),
203 mounted: std::cell::Cell::new(true),
204 }
205 }
206}
207
208#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
209struct PermissionUnmountGuard(std::rc::Rc<PermissionRuntime>);
210
211#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
212impl Drop for PermissionUnmountGuard {
213 fn drop(&mut self) {
214 self.0.mounted.set(false);
215 self.0
216 .generation
217 .set(self.0.generation.get().wrapping_add(1));
218 }
219}
220
221#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
222pub(crate) mod web {
223 use js_sys::Array;
224 use wasm_bindgen::JsCast;
225 use wasm_bindgen::prelude::*;
226 use web_sys::{MediaDeviceInfo, MediaDeviceKind, MediaDevices, MediaStream};
227
228 use super::*;
229
230 pub async fn enumerate_inputs() -> Result<Vec<AudioInputDevice>, AudioError> {
231 let media_devices = media_devices()?;
232 let value = wasm_bindgen_futures::JsFuture::from(
233 media_devices
234 .enumerate_devices()
235 .map_err(audio_error_from_js)?,
236 )
237 .await
238 .map_err(audio_error_from_js)?;
239 let entries = Array::from(&value);
240
241 Ok(entries
242 .iter()
243 .filter_map(|entry| entry.dyn_into::<MediaDeviceInfo>().ok())
244 .filter(|device| device.kind() == MediaDeviceKind::Audioinput)
245 .map(|device| {
246 let id = device.device_id();
247 AudioInputDevice::new(AudioInputId::new(&id), device.label(), id == "default")
248 })
249 .collect())
250 }
251
252 pub async fn request_microphone_permission() -> Result<(), AudioError> {
253 let constraints = web_sys::MediaStreamConstraints::new();
254 constraints.set_audio(&JsValue::TRUE);
255 let value = wasm_bindgen_futures::JsFuture::from(
256 media_devices()?
257 .get_user_media_with_constraints(&constraints)
258 .map_err(audio_error_from_js)?,
259 )
260 .await
261 .map_err(audio_error_from_js)?;
262 let stream = value
263 .dyn_into::<MediaStream>()
264 .map_err(audio_error_from_js)?;
265 stop_stream(&stream);
266 Ok(())
267 }
268
269 pub struct DeviceChangeListener {
270 media_devices: Option<MediaDevices>,
271 callback: Option<Closure<dyn FnMut()>>,
272 }
273
274 impl DeviceChangeListener {
275 pub fn new(refresh: Callback) -> Self {
276 let Ok(media_devices) = media_devices() else {
277 return Self {
278 media_devices: None,
279 callback: None,
280 };
281 };
282 let runtime = dioxus::core::Runtime::current();
283 let scope = runtime.current_scope_id();
284 let callback =
285 Closure::wrap(
286 Box::new(move || runtime.in_scope(scope, || refresh.call(())))
287 as Box<dyn FnMut()>,
288 );
289 let _ = media_devices.add_event_listener_with_callback(
290 "devicechange",
291 callback.as_ref().unchecked_ref(),
292 );
293 Self {
294 media_devices: Some(media_devices),
295 callback: Some(callback),
296 }
297 }
298 }
299
300 impl Drop for DeviceChangeListener {
301 fn drop(&mut self) {
302 if let (Some(media_devices), Some(callback)) = (&self.media_devices, &self.callback) {
303 let _ = media_devices.remove_event_listener_with_callback(
304 "devicechange",
305 callback.as_ref().unchecked_ref(),
306 );
307 }
308 }
309 }
310
311 pub(crate) fn media_devices() -> Result<MediaDevices, AudioError> {
312 web_sys::window()
313 .ok_or_else(AudioError::unsupported)?
314 .navigator()
315 .media_devices()
316 .map_err(audio_error_from_js)
317 }
318
319 pub(crate) fn stop_stream(stream: &MediaStream) {
320 for track in stream.get_tracks().iter() {
321 if let Ok(track) = track.dyn_into::<web_sys::MediaStreamTrack>() {
322 track.stop();
323 }
324 }
325 }
326
327 pub(crate) fn audio_error_from_js(value: JsValue) -> AudioError {
328 let name = value
329 .dyn_ref::<web_sys::DomException>()
330 .map(web_sys::DomException::name)
331 .unwrap_or_default();
332 let kind = match name.as_str() {
333 "NotAllowedError" | "SecurityError" => AudioErrorKind::PermissionDenied,
334 "NotFoundError" | "OverconstrainedError" => AudioErrorKind::DeviceNotFound,
335 "NotReadableError" | "AbortError" => AudioErrorKind::DeviceUnavailable,
336 _ => AudioErrorKind::Backend,
337 };
338 let message = value
339 .dyn_ref::<web_sys::DomException>()
340 .map(web_sys::DomException::message)
341 .or_else(|| value.as_string())
342 .filter(|message| !message.is_empty())
343 .unwrap_or_else(|| "browser audio operation failed".to_string());
344 AudioError::new(kind, message)
345 }
346}