use serde::Serialize;
mod controls;
pub use controls::{AutoState, AutoToggle, CameraControl, CameraState, ControlError, ControlRange};
mod capture_types;
pub use capture_types::{CaptureError, Frame};
#[cfg(target_os = "macos")]
mod macos;
#[cfg(target_os = "macos")]
mod capture;
#[cfg(target_os = "macos")]
pub use capture::{
CameraStream, camera_access_granted, camera_authorization, capture_frame,
request_camera_access, start_stream,
};
#[cfg(target_os = "windows")]
mod capture_windows;
#[cfg(target_os = "windows")]
pub use capture_windows::{
CameraStream, camera_access_granted, camera_authorization, capture_frame,
request_camera_access, start_stream,
};
#[cfg(target_os = "macos")]
mod uvc;
#[cfg(target_os = "macos")]
pub use uvc::{
apply_settings, control_range, control_ranges, read_camera_state, set_auto, set_control,
};
#[cfg(target_os = "windows")]
mod uvc_windows;
#[cfg(target_os = "windows")]
pub use uvc_windows::{
apply_settings, control_range, control_ranges, read_camera_state, set_auto, set_control,
};
#[cfg(target_os = "linux")]
mod linux;
#[cfg(target_os = "linux")]
mod capture_linux;
#[cfg(target_os = "linux")]
pub use capture_linux::{
CameraStream, camera_access_granted, camera_authorization, capture_frame,
request_camera_access, start_stream,
};
#[cfg(target_os = "linux")]
mod uvc_linux;
#[cfg(target_os = "linux")]
pub use uvc_linux::{
apply_settings, control_range, control_ranges, read_camera_state, set_auto, set_control,
};
#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
mod capture {
use std::sync::Arc;
use std::time::Duration;
use crate::capture_types::{CaptureError, Frame};
pub fn capture_frame(_unique_id: &str, _timeout: Duration) -> Result<Frame, CaptureError> {
Err(CaptureError::Unsupported)
}
pub struct CameraStream;
impl CameraStream {
#[must_use]
pub fn latest_frame(&self) -> Option<Arc<Frame>> {
None
}
#[must_use]
pub fn take_frame(&self) -> Option<Arc<Frame>> {
None
}
#[must_use]
pub fn frame_generation(&self) -> u64 {
0
}
}
pub fn start_stream(_unique_id: &str) -> Result<CameraStream, CaptureError> {
Err(CaptureError::Unsupported)
}
#[must_use]
pub fn camera_access_granted() -> bool {
false
}
#[must_use]
pub fn camera_authorization() -> crate::CameraAuthorization {
crate::CameraAuthorization::Undetermined
}
pub fn request_camera_access() {}
}
#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
pub use capture::{
CameraStream, camera_access_granted, camera_authorization, capture_frame,
request_camera_access, start_stream,
};
#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
mod uvc {
use crate::controls::{AutoToggle, CameraControl, CameraState, ControlError, ControlRange};
pub fn control_range(_id: &str, _c: CameraControl) -> Result<ControlRange, ControlError> {
Err(ControlError::Unsupported)
}
pub fn control_ranges(_id: &str) -> Result<Vec<(CameraControl, ControlRange)>, ControlError> {
Ok(Vec::new())
}
pub fn read_camera_state(_id: &str) -> Result<CameraState, ControlError> {
Ok(CameraState::default())
}
pub fn set_control(_id: &str, _c: CameraControl, _value: i32) -> Result<(), ControlError> {
Err(ControlError::Unsupported)
}
pub fn set_auto(_id: &str, _t: AutoToggle, _on: bool) -> Result<(), ControlError> {
Err(ControlError::Unsupported)
}
pub fn apply_settings(
_id: &str,
_autos: &[(AutoToggle, bool)],
_values: &[(CameraControl, i32)],
) -> Result<(), ControlError> {
Err(ControlError::Unsupported)
}
}
#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
pub use uvc::{
apply_settings, control_range, control_ranges, read_camera_state, set_auto, set_control,
};
pub const LOGITECH_VID: u16 = 0x046d;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CameraAuthorization {
Granted,
Denied,
Undetermined,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct Camera {
pub name: String,
pub unique_id: String,
pub serial_number: Option<String>,
pub vendor_id: u16,
pub product_id: u16,
pub max_resolution: Option<(u32, u32)>,
pub max_fps: Option<u32>,
}
impl Camera {
#[must_use]
pub fn config_key(&self) -> String {
if let Some(serial) = self
.serial_number
.as_deref()
.map(str::trim)
.filter(|s| !s.is_empty())
{
format!(
"camera:{:04x}:{:04x}:serial:{}",
self.vendor_id,
self.product_id,
serial.to_ascii_lowercase()
)
} else {
format!("camera:{:04x}:{:04x}", self.vendor_id, self.product_id)
}
}
}
#[must_use]
pub const fn capture_supported() -> bool {
cfg!(any(
target_os = "macos",
target_os = "windows",
target_os = "linux"
))
}
#[cfg(target_os = "macos")]
pub(crate) static USB_QUIESCE: std::sync::Mutex<()> = std::sync::Mutex::new(());
#[must_use]
pub fn enumerate_cameras() -> Vec<Camera> {
enumerate_all()
.into_iter()
.filter(|camera| camera.vendor_id == LOGITECH_VID)
.collect()
}
#[cfg(target_os = "macos")]
fn enumerate_all() -> Vec<Camera> {
let _quiesce = USB_QUIESCE
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let serials = uvc::usb_serials_by_location();
macos::enumerate()
.iter()
.filter_map(|raw| {
let mut camera = Camera::from_raw(&raw.name, &raw.unique_id, &raw.model_id)?;
if raw.max_width > 0 && raw.max_height > 0 {
camera.max_resolution = Some((raw.max_width, raw.max_height));
}
if raw.max_fps > 0 {
camera.max_fps = Some(raw.max_fps);
}
if let Some(location) = uvc::location_hint(&raw.unique_id) {
camera.serial_number = serials.get(&location).cloned();
}
Some(camera)
})
.collect()
}
#[cfg(target_os = "windows")]
fn enumerate_all() -> Vec<Camera> {
uvc_windows::enumerate()
}
#[cfg(target_os = "linux")]
fn enumerate_all() -> Vec<Camera> {
linux::nodes().iter().map(linux::describe).collect()
}
#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
fn enumerate_all() -> Vec<Camera> {
Vec::new()
}
#[cfg(any(test, target_os = "macos"))]
impl Camera {
fn from_raw(name: &str, unique_id: &str, model_id: &str) -> Option<Self> {
let (vendor_id, product_id) = parse_vid_pid(model_id)?;
Some(Self {
name: name.to_string(),
unique_id: unique_id.to_string(),
serial_number: None,
vendor_id,
product_id,
max_resolution: None,
max_fps: None,
})
}
}
#[cfg(any(test, target_os = "macos"))]
fn parse_vid_pid(model_id: &str) -> Option<(u16, u16)> {
let vendor_id = parse_marker(model_id, "VendorID_")?;
let product_id = parse_marker(model_id, "ProductID_")?;
Some((vendor_id, product_id))
}
#[cfg(any(test, target_os = "macos"))]
fn parse_marker(haystack: &str, marker: &str) -> Option<u16> {
let rest = haystack.split(marker).nth(1)?;
let digits: String = rest.chars().take_while(char::is_ascii_digit).collect();
digits.parse().ok()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_logitech_streamcam_model_id() {
assert_eq!(
parse_vid_pid("UVC Camera VendorID_1133 ProductID_2195"),
Some((0x046d, 0x0893))
);
}
#[test]
fn rejects_model_id_without_usb_ids() {
assert_eq!(parse_vid_pid("FaceTime HD Camera"), None);
assert_eq!(parse_vid_pid("VendorID_1133 only"), None);
}
#[test]
fn from_raw_keeps_usb_cameras_and_drops_the_rest() {
assert_eq!(
Camera::from_raw(
"Logitech StreamCam",
"0x1123000046d0893",
"UVC Camera VendorID_1133 ProductID_2195",
),
Some(Camera {
name: "Logitech StreamCam".to_string(),
unique_id: "0x1123000046d0893".to_string(),
serial_number: None,
vendor_id: LOGITECH_VID,
product_id: 0x0893,
max_resolution: None,
max_fps: None,
})
);
assert_eq!(
Camera::from_raw("FaceTime HD Camera", "uuid", "FaceTime HD Camera"),
None
);
}
#[test]
fn config_key_prefers_usb_serial_over_capture_id() {
let with_serial = Camera {
name: "Logitech StreamCam".into(),
unique_id: "0x1123000046d0893".into(),
serial_number: Some("ABC123".into()),
vendor_id: LOGITECH_VID,
product_id: 0x0893,
max_resolution: None,
max_fps: None,
};
assert_eq!(with_serial.config_key(), "camera:046d:0893:serial:abc123");
let moved = Camera {
unique_id: "0x14110000046d0893".into(),
..with_serial.clone()
};
assert_eq!(moved.config_key(), with_serial.config_key());
let no_serial = Camera {
serial_number: None,
unique_id: "0x1123000046d0893".into(),
..with_serial.clone()
};
assert_eq!(no_serial.config_key(), "camera:046d:0893");
let moved_no_serial = Camera {
unique_id: "0x14110000046d0893".into(),
..no_serial
};
assert_eq!(moved_no_serial.config_key(), "camera:046d:0893");
}
}