1use openlogi_device_registry::camera::find_unnamed_camera;
21use serde::Serialize;
22
23mod controls;
24pub use controls::{AutoState, AutoToggle, CameraControl, CameraState, ControlError, ControlRange};
25
26mod capture_types;
27pub use capture_types::{CaptureError, Frame};
28
29#[cfg(target_os = "macos")]
30mod macos;
31
32#[cfg(target_os = "macos")]
33mod capture_macos;
34#[cfg(target_os = "macos")]
35pub use capture_macos::{
36 CameraStream, camera_access_granted, camera_authorization, capture_frame,
37 request_camera_access, start_stream,
38};
39
40#[cfg(target_os = "windows")]
41mod com_windows;
42
43#[cfg(target_os = "windows")]
44mod capture_windows;
45#[cfg(target_os = "windows")]
46pub use capture_windows::{
47 CameraStream, camera_access_granted, camera_authorization, capture_frame,
48 request_camera_access, start_stream,
49};
50
51#[cfg(target_os = "macos")]
52mod uvc_macos;
53#[cfg(target_os = "macos")]
54pub use uvc_macos::{
55 apply_settings, control_range, control_ranges, read_camera_state, set_auto, set_control,
56};
57
58#[cfg(target_os = "windows")]
59mod uvc_windows;
60#[cfg(target_os = "windows")]
61pub use uvc_windows::{
62 apply_settings, control_range, control_ranges, read_camera_state, set_auto, set_control,
63};
64
65#[cfg(target_os = "linux")]
66mod linux;
67
68#[cfg(target_os = "linux")]
69mod capture_linux;
70#[cfg(target_os = "linux")]
71pub use capture_linux::{
72 CameraStream, camera_access_granted, camera_authorization, capture_frame,
73 request_camera_access, start_stream,
74};
75
76#[cfg(target_os = "linux")]
77mod uvc_linux;
78#[cfg(target_os = "linux")]
79pub use uvc_linux::{
80 apply_settings, control_range, control_ranges, read_camera_state, set_auto, set_control,
81};
82
83#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
84mod capture {
85 use std::sync::Arc;
87 use std::time::Duration;
88
89 use crate::capture_types::{CaptureError, Frame};
90
91 pub fn capture_frame(_unique_id: &str, _timeout: Duration) -> Result<Frame, CaptureError> {
93 Err(CaptureError::Unsupported)
94 }
95
96 pub struct CameraStream;
98
99 impl CameraStream {
100 #[must_use]
101 pub fn latest_frame(&self) -> Option<Arc<Frame>> {
102 None
103 }
104
105 #[must_use]
106 pub fn take_frame(&self) -> Option<Arc<Frame>> {
107 None
108 }
109
110 #[must_use]
111 pub fn frame_generation(&self) -> u64 {
112 0
113 }
114 }
115
116 pub fn start_stream(_unique_id: &str) -> Result<CameraStream, CaptureError> {
118 Err(CaptureError::Unsupported)
119 }
120
121 #[must_use]
123 pub fn camera_access_granted() -> bool {
124 false
125 }
126
127 #[must_use]
129 pub fn camera_authorization() -> crate::CameraAuthorization {
130 crate::CameraAuthorization::Undetermined
131 }
132
133 pub fn request_camera_access() {}
135}
136#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
137pub use capture::{
138 CameraStream, camera_access_granted, camera_authorization, capture_frame,
139 request_camera_access, start_stream,
140};
141
142#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
143mod uvc {
144 use crate::controls::{AutoToggle, CameraControl, CameraState, ControlError, ControlRange};
146
147 pub fn control_range(_id: &str, _c: CameraControl) -> Result<ControlRange, ControlError> {
149 Err(ControlError::Unsupported)
150 }
151
152 pub fn control_ranges(_id: &str) -> Result<Vec<(CameraControl, ControlRange)>, ControlError> {
154 Ok(Vec::new())
155 }
156
157 pub fn read_camera_state(_id: &str) -> Result<CameraState, ControlError> {
159 Ok(CameraState::default())
160 }
161
162 pub fn set_control(_id: &str, _c: CameraControl, _value: i32) -> Result<(), ControlError> {
164 Err(ControlError::Unsupported)
165 }
166
167 pub fn set_auto(_id: &str, _t: AutoToggle, _on: bool) -> Result<(), ControlError> {
169 Err(ControlError::Unsupported)
170 }
171
172 pub fn apply_settings(
174 _id: &str,
175 _autos: &[(AutoToggle, bool)],
176 _values: &[(CameraControl, i32)],
177 ) -> Result<(), ControlError> {
178 Err(ControlError::Unsupported)
179 }
180}
181#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
182pub use uvc::{
183 apply_settings, control_range, control_ranges, read_camera_state, set_auto, set_control,
184};
185
186pub use openlogi_device_registry::LOGITECH_VENDOR_ID as LOGITECH_VID;
189
190#[derive(Debug, Clone, Copy, PartialEq, Eq)]
197pub enum CameraAuthorization {
198 Granted,
200 Denied,
202 Undetermined,
204}
205
206#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
208pub struct Camera {
209 pub name: String,
211 pub unique_id: String,
215 pub serial_number: Option<String>,
218 pub vendor_id: u16,
220 pub product_id: u16,
222 pub max_resolution: Option<(u32, u32)>,
225 pub max_fps: Option<u32>,
227}
228
229impl Camera {
230 #[must_use]
238 pub fn config_key(&self) -> String {
239 if let Some(serial) = self
240 .serial_number
241 .as_deref()
242 .map(str::trim)
243 .filter(|s| !s.is_empty())
244 {
245 format!(
246 "camera:{:04x}:{:04x}:serial:{}",
247 self.vendor_id,
248 self.product_id,
249 serial.to_ascii_lowercase()
250 )
251 } else {
252 format!("camera:{:04x}:{:04x}", self.vendor_id, self.product_id)
253 }
254 }
255
256 fn with_registry_name(mut self) -> Self {
260 if let Some(known) = find_unnamed_camera(self.vendor_id, self.product_id) {
261 known.name.clone_into(&mut self.name);
262 }
263 self
264 }
265}
266
267#[must_use]
270pub const fn capture_supported() -> bool {
271 cfg!(any(
272 target_os = "macos",
273 target_os = "windows",
274 target_os = "linux"
275 ))
276}
277
278#[cfg(target_os = "macos")]
285pub(crate) static USB_QUIESCE: std::sync::Mutex<()> = std::sync::Mutex::new(());
286
287#[must_use]
293pub fn enumerate_cameras() -> Vec<Camera> {
294 enumerate_all()
295 .into_iter()
296 .filter(|camera| camera.vendor_id == LOGITECH_VID)
297 .map(Camera::with_registry_name)
298 .collect()
299}
300
301#[cfg(target_os = "macos")]
302fn enumerate_all() -> Vec<Camera> {
303 let _quiesce = USB_QUIESCE
307 .lock()
308 .unwrap_or_else(std::sync::PoisonError::into_inner);
309 let serials = uvc_macos::usb_serials_by_location();
310 macos::enumerate()
311 .iter()
312 .filter_map(|raw| {
313 let mut camera = Camera::from_raw(&raw.name, &raw.unique_id, &raw.model_id)?;
314 if raw.max_width > 0 && raw.max_height > 0 {
315 camera.max_resolution = Some((raw.max_width, raw.max_height));
316 }
317 if raw.max_fps > 0 {
318 camera.max_fps = Some(raw.max_fps);
319 }
320 if let Some(location) = uvc_macos::location_hint(&raw.unique_id) {
321 camera.serial_number = serials.get(&location).cloned();
322 }
323 Some(camera)
324 })
325 .collect()
326}
327
328#[cfg(target_os = "windows")]
329fn enumerate_all() -> Vec<Camera> {
330 uvc_windows::enumerate()
331}
332
333#[cfg(target_os = "linux")]
334fn enumerate_all() -> Vec<Camera> {
335 linux::cameras()
336}
337
338#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
339fn enumerate_all() -> Vec<Camera> {
340 Vec::new()
341}
342
343#[cfg(any(test, target_os = "macos"))]
344impl Camera {
345 fn from_raw(name: &str, unique_id: &str, model_id: &str) -> Option<Self> {
353 let (vendor_id, product_id) = parse_vid_pid(model_id)?;
354 Some(Self {
355 name: name.to_string(),
356 unique_id: unique_id.to_string(),
357 serial_number: None,
358 vendor_id,
359 product_id,
360 max_resolution: None,
361 max_fps: None,
362 })
363 }
364}
365
366#[cfg(any(test, target_os = "macos"))]
371fn parse_vid_pid(model_id: &str) -> Option<(u16, u16)> {
372 let vendor_id = parse_marker(model_id, "VendorID_")?;
373 let product_id = parse_marker(model_id, "ProductID_")?;
374 Some((vendor_id, product_id))
375}
376
377#[cfg(any(test, target_os = "macos"))]
379fn parse_marker(haystack: &str, marker: &str) -> Option<u16> {
380 let rest = haystack.split(marker).nth(1)?;
381 let digits: String = rest.chars().take_while(char::is_ascii_digit).collect();
382 digits.parse().ok()
383}
384
385#[cfg(test)]
386mod tests {
387 use super::*;
388
389 #[test]
390 fn parses_logitech_streamcam_model_id() {
391 assert_eq!(
392 parse_vid_pid("UVC Camera VendorID_1133 ProductID_2195"),
393 Some((0x046d, 0x0893))
394 );
395 }
396
397 #[test]
398 fn rejects_model_id_without_usb_ids() {
399 assert_eq!(parse_vid_pid("FaceTime HD Camera"), None);
400 assert_eq!(parse_vid_pid("VendorID_1133 only"), None);
401 }
402
403 #[test]
404 fn from_raw_keeps_usb_cameras_and_drops_the_rest() {
405 assert_eq!(
406 Camera::from_raw(
407 "Logitech StreamCam",
408 "0x1123000046d0893",
409 "UVC Camera VendorID_1133 ProductID_2195",
410 ),
411 Some(Camera {
412 name: "Logitech StreamCam".to_string(),
413 unique_id: "0x1123000046d0893".to_string(),
414 serial_number: None,
415 vendor_id: LOGITECH_VID,
416 product_id: 0x0893,
417 max_resolution: None,
418 max_fps: None,
419 })
420 );
421 assert_eq!(
422 Camera::from_raw("FaceTime HD Camera", "uuid", "FaceTime HD Camera"),
423 None
424 );
425 }
426
427 #[test]
428 fn camera_without_a_product_string_takes_its_registry_name() {
429 let c510 = Camera::from_raw(
431 "USB Camera VID:1133 PID:2077",
432 "0x2141000046d081d",
433 "UVC Camera VendorID_1133 ProductID_2077",
434 )
435 .expect("USB camera")
436 .with_registry_name();
437 assert_eq!(c510.name, "Logitech HD Webcam C510");
438 assert_eq!(c510.product_id, 0x081d);
439 assert_eq!(c510.unique_id, "0x2141000046d081d");
441 assert_eq!(c510.config_key(), "camera:046d:081d");
442 }
443
444 #[test]
445 fn camera_that_names_itself_keeps_the_os_name() {
446 let streamcam = Camera::from_raw(
447 "Logitech StreamCam",
448 "0x1123000046d0893",
449 "UVC Camera VendorID_1133 ProductID_2195",
450 )
451 .expect("USB camera")
452 .with_registry_name();
453 assert_eq!(streamcam.name, "Logitech StreamCam");
454 }
455
456 #[test]
457 fn config_key_prefers_usb_serial_over_capture_id() {
458 let with_serial = Camera {
459 name: "Logitech StreamCam".into(),
460 unique_id: "0x1123000046d0893".into(),
461 serial_number: Some("ABC123".into()),
462 vendor_id: LOGITECH_VID,
463 product_id: 0x0893,
464 max_resolution: None,
465 max_fps: None,
466 };
467 assert_eq!(with_serial.config_key(), "camera:046d:0893:serial:abc123");
468 let moved = Camera {
470 unique_id: "0x14110000046d0893".into(),
471 ..with_serial.clone()
472 };
473 assert_eq!(moved.config_key(), with_serial.config_key());
474
475 let no_serial = Camera {
476 serial_number: None,
477 unique_id: "0x1123000046d0893".into(),
478 ..with_serial.clone()
479 };
480 assert_eq!(no_serial.config_key(), "camera:046d:0893");
482 let moved_no_serial = Camera {
483 unique_id: "0x14110000046d0893".into(),
484 ..no_serial
485 };
486 assert_eq!(moved_no_serial.config_key(), "camera:046d:0893");
487 }
488}