screencapturekit/audio_devices.rs
1//! Audio input device enumeration using `AVFoundation`.
2//!
3//! This module provides access to available microphone devices on macOS.
4
5use std::ffi::c_void;
6
7/// Represents an audio input device (microphone).
8#[derive(Debug, Clone, PartialEq, Eq)]
9pub struct AudioInputDevice {
10 /// The unique device ID used with `SCStreamConfiguration::with_microphone_capture_device_id`
11 pub id: String,
12 /// Human-readable device name
13 pub name: String,
14 /// Whether this is the system default audio input device
15 pub is_default: bool,
16}
17
18impl AudioInputDevice {
19 /// List all available audio input devices.
20 ///
21 /// The whole list is read from a single device-discovery pass, so a
22 /// microphone plugged in or unplugged during the call cannot produce a
23 /// half-updated result (a stale count paired with fresh names, or an entry
24 /// whose `is_default` disagrees with [`default_device`](Self::default_device)).
25 ///
26 /// # Caching
27 ///
28 /// **Not cached.** Each call walks Apple's audio device list and copies the
29 /// per-device strings across the FFI boundary. The cost is small in
30 /// absolute terms (microseconds) but is **non-zero on every call**. Code
31 /// that repeatedly needs the device list (e.g. inside a UI render loop or
32 /// per-frame decision) should cache the result and re-list only when the
33 /// user signals a possible device change (e.g. on a settings-pane open or
34 /// an `AVAudioRouteChangeNotification`).
35 ///
36 /// # Example
37 ///
38 /// ```no_run
39 /// use screencapturekit::audio_devices::AudioInputDevice;
40 ///
41 /// let devices = AudioInputDevice::list();
42 /// for device in &devices {
43 /// println!("{}: {} {}", device.id, device.name,
44 /// if device.is_default { "(default)" } else { "" });
45 /// }
46 /// ```
47 #[must_use]
48 pub fn list() -> Vec<Self> {
49 Snapshot::take().devices()
50 }
51
52 /// Get the default audio input device, if any.
53 ///
54 /// Reads from the same single-pass snapshot as [`list`](Self::list), so the
55 /// returned device is guaranteed to be one of the listed devices rather
56 /// than an entry that appeared or vanished between two queries.
57 ///
58 /// # Example
59 ///
60 /// ```no_run
61 /// use screencapturekit::audio_devices::AudioInputDevice;
62 ///
63 /// if let Some(device) = AudioInputDevice::default_device() {
64 /// println!("Default microphone: {}", device.name);
65 /// }
66 /// ```
67 #[must_use]
68 pub fn default_device() -> Option<Self> {
69 let snapshot = Snapshot::take();
70 let index = isize::try_from(snapshot.default_index()?).ok()?;
71 snapshot.device_at(index)
72 }
73
74 /// List all devices and identify the default one in a single pass.
75 ///
76 /// Returns the device list plus the index of the default device within it.
77 /// Prefer this over calling [`list`](Self::list) and
78 /// [`default_device`](Self::default_device) separately: it halves the work
79 /// and rules out the two results disagreeing.
80 ///
81 /// # Example
82 ///
83 /// ```no_run
84 /// use screencapturekit::audio_devices::AudioInputDevice;
85 ///
86 /// let (devices, default_index) = AudioInputDevice::list_with_default();
87 /// if let Some(default) = default_index.and_then(|i| devices.get(i)) {
88 /// println!("Default: {}", default.name);
89 /// }
90 /// ```
91 #[must_use]
92 pub fn list_with_default() -> (Vec<Self>, Option<usize>) {
93 let snapshot = Snapshot::take();
94 let devices = snapshot.devices();
95 // Derive the index from the returned list rather than reusing the
96 // snapshot's index: `devices()` drops entries whose id or name is
97 // unreadable, so the snapshot index addresses the unfiltered array and
98 // would designate the wrong device, or fall outside the list entirely.
99 let default_index = devices.iter().position(|device| device.is_default);
100 (devices, default_index)
101 }
102}
103
104/// RAII wrapper around a Swift-side frozen device list.
105struct Snapshot {
106 ptr: *const c_void,
107}
108
109impl Snapshot {
110 fn take() -> Self {
111 Self {
112 ptr: unsafe { crate::ffi::sc_audio_input_devices_snapshot_create() },
113 }
114 }
115
116 fn count(&self) -> usize {
117 if self.ptr.is_null() {
118 return 0;
119 }
120 let count = unsafe { crate::ffi::sc_audio_input_devices_snapshot_count(self.ptr) };
121 usize::try_from(count).unwrap_or(0)
122 }
123
124 fn default_index(&self) -> Option<usize> {
125 if self.ptr.is_null() {
126 return None;
127 }
128 let index = unsafe { crate::ffi::sc_audio_input_devices_snapshot_default_index(self.ptr) };
129 usize::try_from(index).ok().filter(|i| *i < self.count())
130 }
131
132 fn device_at(&self, index: isize) -> Option<AudioInputDevice> {
133 let id = unsafe {
134 crate::utils::ffi_string::ffi_string_owned(|| {
135 crate::ffi::sc_audio_input_devices_snapshot_id_owned(self.ptr, index)
136 })
137 }?;
138 let name = unsafe {
139 crate::utils::ffi_string::ffi_string_owned(|| {
140 crate::ffi::sc_audio_input_devices_snapshot_name_owned(self.ptr, index)
141 })
142 }?;
143 let is_default =
144 unsafe { crate::ffi::sc_audio_input_devices_snapshot_is_default(self.ptr, index) };
145 Some(AudioInputDevice {
146 id,
147 name,
148 is_default,
149 })
150 }
151
152 fn devices(&self) -> Vec<AudioInputDevice> {
153 let count = self.count();
154 (0..count)
155 .filter_map(|i| isize::try_from(i).ok().and_then(|i| self.device_at(i)))
156 .collect()
157 }
158}
159
160impl Drop for Snapshot {
161 fn drop(&mut self) {
162 if !self.ptr.is_null() {
163 unsafe { crate::ffi::sc_audio_input_devices_snapshot_release(self.ptr) };
164 }
165 }
166}