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