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