1use serde::Serialize;
16
17mod controls;
18pub use controls::{AutoState, AutoToggle, CameraControl, CameraState, ControlError, ControlRange};
19
20mod capture_types;
21pub use capture_types::{CaptureError, Frame};
22
23#[cfg(target_os = "macos")]
24mod macos;
25
26#[cfg(target_os = "macos")]
27mod capture;
28#[cfg(target_os = "macos")]
29pub use capture::{
30 CameraStream, camera_access_granted, camera_authorization, capture_frame,
31 request_camera_access, start_stream,
32};
33
34#[cfg(target_os = "windows")]
35mod com_windows;
36
37#[cfg(target_os = "windows")]
38mod capture_windows;
39#[cfg(target_os = "windows")]
40pub use capture_windows::{
41 CameraStream, camera_access_granted, camera_authorization, capture_frame,
42 request_camera_access, start_stream,
43};
44
45#[cfg(target_os = "macos")]
46mod uvc;
47#[cfg(target_os = "macos")]
48pub use uvc::{
49 apply_settings, control_range, control_ranges, read_camera_state, set_auto, set_control,
50};
51
52#[cfg(target_os = "windows")]
53mod uvc_windows;
54#[cfg(target_os = "windows")]
55pub use uvc_windows::{
56 apply_settings, control_range, control_ranges, read_camera_state, set_auto, set_control,
57};
58
59#[cfg(target_os = "linux")]
60mod linux;
61
62#[cfg(target_os = "linux")]
63mod capture_linux;
64#[cfg(target_os = "linux")]
65pub use capture_linux::{
66 CameraStream, camera_access_granted, camera_authorization, capture_frame,
67 request_camera_access, start_stream,
68};
69
70#[cfg(target_os = "linux")]
71mod uvc_linux;
72#[cfg(target_os = "linux")]
73pub use uvc_linux::{
74 apply_settings, control_range, control_ranges, read_camera_state, set_auto, set_control,
75};
76
77#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
78mod capture {
79 use std::sync::Arc;
81 use std::time::Duration;
82
83 use crate::capture_types::{CaptureError, Frame};
84
85 pub fn capture_frame(_unique_id: &str, _timeout: Duration) -> Result<Frame, CaptureError> {
87 Err(CaptureError::Unsupported)
88 }
89
90 pub struct CameraStream;
92
93 impl CameraStream {
94 #[must_use]
95 pub fn latest_frame(&self) -> Option<Arc<Frame>> {
96 None
97 }
98
99 #[must_use]
100 pub fn take_frame(&self) -> Option<Arc<Frame>> {
101 None
102 }
103
104 #[must_use]
105 pub fn frame_generation(&self) -> u64 {
106 0
107 }
108 }
109
110 pub fn start_stream(_unique_id: &str) -> Result<CameraStream, CaptureError> {
112 Err(CaptureError::Unsupported)
113 }
114
115 #[must_use]
117 pub fn camera_access_granted() -> bool {
118 false
119 }
120
121 #[must_use]
123 pub fn camera_authorization() -> crate::CameraAuthorization {
124 crate::CameraAuthorization::Undetermined
125 }
126
127 pub fn request_camera_access() {}
129}
130#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
131pub use capture::{
132 CameraStream, camera_access_granted, camera_authorization, capture_frame,
133 request_camera_access, start_stream,
134};
135
136#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
137mod uvc {
138 use crate::controls::{AutoToggle, CameraControl, CameraState, ControlError, ControlRange};
140
141 pub fn control_range(_id: &str, _c: CameraControl) -> Result<ControlRange, ControlError> {
143 Err(ControlError::Unsupported)
144 }
145
146 pub fn control_ranges(_id: &str) -> Result<Vec<(CameraControl, ControlRange)>, ControlError> {
148 Ok(Vec::new())
149 }
150
151 pub fn read_camera_state(_id: &str) -> Result<CameraState, ControlError> {
153 Ok(CameraState::default())
154 }
155
156 pub fn set_control(_id: &str, _c: CameraControl, _value: i32) -> Result<(), ControlError> {
158 Err(ControlError::Unsupported)
159 }
160
161 pub fn set_auto(_id: &str, _t: AutoToggle, _on: bool) -> Result<(), ControlError> {
163 Err(ControlError::Unsupported)
164 }
165
166 pub fn apply_settings(
168 _id: &str,
169 _autos: &[(AutoToggle, bool)],
170 _values: &[(CameraControl, i32)],
171 ) -> Result<(), ControlError> {
172 Err(ControlError::Unsupported)
173 }
174}
175#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
176pub use uvc::{
177 apply_settings, control_range, control_ranges, read_camera_state, set_auto, set_control,
178};
179
180pub const LOGITECH_VID: u16 = 0x046d;
183
184#[derive(Debug, Clone, Copy, PartialEq, Eq)]
191pub enum CameraAuthorization {
192 Granted,
194 Denied,
196 Undetermined,
198}
199
200#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
202pub struct Camera {
203 pub name: String,
205 pub unique_id: String,
209 pub serial_number: Option<String>,
212 pub vendor_id: u16,
214 pub product_id: u16,
216 pub max_resolution: Option<(u32, u32)>,
219 pub max_fps: Option<u32>,
221}
222
223impl Camera {
224 #[must_use]
232 pub fn config_key(&self) -> String {
233 if let Some(serial) = self
234 .serial_number
235 .as_deref()
236 .map(str::trim)
237 .filter(|s| !s.is_empty())
238 {
239 format!(
240 "camera:{:04x}:{:04x}:serial:{}",
241 self.vendor_id,
242 self.product_id,
243 serial.to_ascii_lowercase()
244 )
245 } else {
246 format!("camera:{:04x}:{:04x}", self.vendor_id, self.product_id)
247 }
248 }
249}
250
251#[must_use]
254pub const fn capture_supported() -> bool {
255 cfg!(any(
256 target_os = "macos",
257 target_os = "windows",
258 target_os = "linux"
259 ))
260}
261
262#[cfg(target_os = "macos")]
269pub(crate) static USB_QUIESCE: std::sync::Mutex<()> = std::sync::Mutex::new(());
270
271#[must_use]
277pub fn enumerate_cameras() -> Vec<Camera> {
278 enumerate_all()
279 .into_iter()
280 .filter(|camera| camera.vendor_id == LOGITECH_VID)
281 .collect()
282}
283
284#[cfg(target_os = "macos")]
285fn enumerate_all() -> Vec<Camera> {
286 let _quiesce = USB_QUIESCE
290 .lock()
291 .unwrap_or_else(std::sync::PoisonError::into_inner);
292 let serials = uvc::usb_serials_by_location();
293 macos::enumerate()
294 .iter()
295 .filter_map(|raw| {
296 let mut camera = Camera::from_raw(&raw.name, &raw.unique_id, &raw.model_id)?;
297 if raw.max_width > 0 && raw.max_height > 0 {
298 camera.max_resolution = Some((raw.max_width, raw.max_height));
299 }
300 if raw.max_fps > 0 {
301 camera.max_fps = Some(raw.max_fps);
302 }
303 if let Some(location) = uvc::location_hint(&raw.unique_id) {
304 camera.serial_number = serials.get(&location).cloned();
305 }
306 Some(camera)
307 })
308 .collect()
309}
310
311#[cfg(target_os = "windows")]
312fn enumerate_all() -> Vec<Camera> {
313 uvc_windows::enumerate()
314}
315
316#[cfg(target_os = "linux")]
317fn enumerate_all() -> Vec<Camera> {
318 linux::nodes().iter().map(linux::describe).collect()
319}
320
321#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
322fn enumerate_all() -> Vec<Camera> {
323 Vec::new()
324}
325
326#[cfg(any(test, target_os = "macos"))]
327impl Camera {
328 fn from_raw(name: &str, unique_id: &str, model_id: &str) -> Option<Self> {
336 let (vendor_id, product_id) = parse_vid_pid(model_id)?;
337 Some(Self {
338 name: name.to_string(),
339 unique_id: unique_id.to_string(),
340 serial_number: None,
341 vendor_id,
342 product_id,
343 max_resolution: None,
344 max_fps: None,
345 })
346 }
347}
348
349#[cfg(any(test, target_os = "macos"))]
354fn parse_vid_pid(model_id: &str) -> Option<(u16, u16)> {
355 let vendor_id = parse_marker(model_id, "VendorID_")?;
356 let product_id = parse_marker(model_id, "ProductID_")?;
357 Some((vendor_id, product_id))
358}
359
360#[cfg(any(test, target_os = "macos"))]
362fn parse_marker(haystack: &str, marker: &str) -> Option<u16> {
363 let rest = haystack.split(marker).nth(1)?;
364 let digits: String = rest.chars().take_while(char::is_ascii_digit).collect();
365 digits.parse().ok()
366}
367
368#[cfg(test)]
369mod tests {
370 use super::*;
371
372 #[test]
373 fn parses_logitech_streamcam_model_id() {
374 assert_eq!(
375 parse_vid_pid("UVC Camera VendorID_1133 ProductID_2195"),
376 Some((0x046d, 0x0893))
377 );
378 }
379
380 #[test]
381 fn rejects_model_id_without_usb_ids() {
382 assert_eq!(parse_vid_pid("FaceTime HD Camera"), None);
383 assert_eq!(parse_vid_pid("VendorID_1133 only"), None);
384 }
385
386 #[test]
387 fn from_raw_keeps_usb_cameras_and_drops_the_rest() {
388 assert_eq!(
389 Camera::from_raw(
390 "Logitech StreamCam",
391 "0x1123000046d0893",
392 "UVC Camera VendorID_1133 ProductID_2195",
393 ),
394 Some(Camera {
395 name: "Logitech StreamCam".to_string(),
396 unique_id: "0x1123000046d0893".to_string(),
397 serial_number: None,
398 vendor_id: LOGITECH_VID,
399 product_id: 0x0893,
400 max_resolution: None,
401 max_fps: None,
402 })
403 );
404 assert_eq!(
405 Camera::from_raw("FaceTime HD Camera", "uuid", "FaceTime HD Camera"),
406 None
407 );
408 }
409
410 #[test]
411 fn config_key_prefers_usb_serial_over_capture_id() {
412 let with_serial = Camera {
413 name: "Logitech StreamCam".into(),
414 unique_id: "0x1123000046d0893".into(),
415 serial_number: Some("ABC123".into()),
416 vendor_id: LOGITECH_VID,
417 product_id: 0x0893,
418 max_resolution: None,
419 max_fps: None,
420 };
421 assert_eq!(with_serial.config_key(), "camera:046d:0893:serial:abc123");
422 let moved = Camera {
424 unique_id: "0x14110000046d0893".into(),
425 ..with_serial.clone()
426 };
427 assert_eq!(moved.config_key(), with_serial.config_key());
428
429 let no_serial = Camera {
430 serial_number: None,
431 unique_id: "0x1123000046d0893".into(),
432 ..with_serial.clone()
433 };
434 assert_eq!(no_serial.config_key(), "camera:046d:0893");
436 let moved_no_serial = Camera {
437 unique_id: "0x14110000046d0893".into(),
438 ..no_serial
439 };
440 assert_eq!(moved_no_serial.config_key(), "camera:046d:0893");
441 }
442}