#![expect(
unsafe_code,
reason = "DirectShow COM (device enumeration + IAMVideoProcAmp / IAMCameraControl)"
)]
use windows::Win32::Media::DirectShow::{IAMCameraControl, IAMVideoProcAmp, IBaseFilter};
use windows::Win32::System::Com::StructuredStorage::IPropertyBag;
use windows::Win32::System::Com::{
CLSCTX_INPROC_SERVER, COINIT_MULTITHREADED, CoCreateInstance, CoInitializeEx, IEnumMoniker,
IMoniker,
};
use windows::Win32::System::Variant::{VARIANT, VT_BSTR};
use windows::core::{GUID, Interface, w};
use crate::Camera;
use crate::controls::{
AutoState, AutoToggle, CameraControl, CameraState, ControlError, ControlRange,
};
const CLSID_SYSTEM_DEVICE_ENUM: GUID = GUID::from_u128(0x62be5d10_60eb_11d0_bd3b_00a0c911ce86);
const CLSID_VIDEO_INPUT_DEVICE_CATEGORY: GUID =
GUID::from_u128(0x860bb310_5d01_11d0_bd3b_00a0c911ce86);
const VPA_BRIGHTNESS: i32 = 0;
const VPA_CONTRAST: i32 = 1;
const VPA_HUE: i32 = 2;
const VPA_SATURATION: i32 = 3;
const VPA_SHARPNESS: i32 = 4;
const VPA_WHITE_BALANCE: i32 = 6;
const CC_ZOOM: i32 = 3;
const CC_EXPOSURE: i32 = 4;
const CC_FOCUS: i32 = 6;
const FLAG_AUTO: i32 = 0x1;
const FLAG_MANUAL: i32 = 0x2;
#[derive(Clone, Copy)]
enum Prop {
VideoProcAmp(i32),
CameraControl(i32),
}
impl CameraControl {
fn prop(self) -> Prop {
match self {
Self::Zoom => Prop::CameraControl(CC_ZOOM),
Self::Focus => Prop::CameraControl(CC_FOCUS),
Self::Exposure => Prop::CameraControl(CC_EXPOSURE),
Self::Brightness => Prop::VideoProcAmp(VPA_BRIGHTNESS),
Self::Contrast => Prop::VideoProcAmp(VPA_CONTRAST),
Self::Saturation => Prop::VideoProcAmp(VPA_SATURATION),
Self::Sharpness => Prop::VideoProcAmp(VPA_SHARPNESS),
Self::WhiteBalance => Prop::VideoProcAmp(VPA_WHITE_BALANCE),
Self::Tint => Prop::VideoProcAmp(VPA_HUE),
}
}
}
impl AutoToggle {
fn prop(self) -> Prop {
match self {
Self::Focus => Prop::CameraControl(CC_FOCUS),
Self::Exposure => Prop::CameraControl(CC_EXPOSURE),
Self::WhiteBalance => Prop::VideoProcAmp(VPA_WHITE_BALANCE),
}
}
}
pub fn enumerate() -> Vec<Camera> {
monikers()
.map(|monikers| {
monikers
.into_iter()
.filter_map(|m| camera_from_moniker(&m))
.collect()
})
.unwrap_or_default()
}
pub fn control_range(
unique_id: &str,
control: CameraControl,
) -> Result<ControlRange, ControlError> {
let dev = Device::open(unique_id)?;
dev.range(control.prop()).map(|(range, _)| range)
}
pub fn control_ranges(unique_id: &str) -> Result<Vec<(CameraControl, ControlRange)>, ControlError> {
Ok(read_camera_state(unique_id)?.controls)
}
pub fn read_camera_state(unique_id: &str) -> Result<CameraState, ControlError> {
let dev = Device::open(unique_id)?;
let mut state = CameraState::default();
for control in CameraControl::ALL {
if let Ok((range, _)) = dev.range(control.prop()) {
state.controls.push((control, range));
}
}
for toggle in AutoToggle::ALL {
if let Ok((_, caps)) = dev.range(toggle.prop())
&& caps & FLAG_AUTO != 0
&& let Ok(current) = dev.auto_engaged(toggle.prop())
{
state.autos.push((
toggle,
AutoState {
current,
default: true,
},
));
}
}
Ok(state)
}
pub fn set_control(
unique_id: &str,
control: CameraControl,
value: i32,
) -> Result<(), ControlError> {
let dev = Device::open(unique_id)?;
dev.set(control.prop(), value, FLAG_MANUAL)
}
pub fn set_auto(unique_id: &str, toggle: AutoToggle, on: bool) -> Result<(), ControlError> {
let dev = Device::open(unique_id)?;
dev.set_auto(toggle.prop(), on)
}
pub fn apply_settings(
unique_id: &str,
autos: &[(AutoToggle, bool)],
values: &[(CameraControl, i32)],
) -> Result<(), ControlError> {
let dev = Device::open(unique_id)?;
let mut first_err = None;
for (toggle, on) in autos {
if let Err(e) = dev.set_auto(toggle.prop(), *on) {
first_err.get_or_insert(e);
}
}
for (control, value) in values {
if let Err(e) = dev.set(control.prop(), *value, FLAG_MANUAL) {
first_err.get_or_insert(e);
}
}
first_err.map_or(Ok(()), Err)
}
struct Device {
proc_amp: Option<IAMVideoProcAmp>,
camera_control: Option<IAMCameraControl>,
}
impl Device {
fn open(unique_id: &str) -> Result<Self, ControlError> {
let monikers = monikers().map_err(|e| ControlError::Io(e.to_string()))?;
for moniker in monikers {
if read_property(&moniker, w!("DevicePath")).as_deref() != Some(unique_id) {
continue;
}
let filter: IBaseFilter = unsafe { moniker.BindToObject(None, None) }
.map_err(|e| ControlError::Io(e.to_string()))?;
return Ok(Self {
proc_amp: filter.cast().ok(),
camera_control: filter.cast().ok(),
});
}
Err(ControlError::NotFound)
}
fn range(&self, prop: Prop) -> Result<(ControlRange, i32), ControlError> {
let (mut min, mut max, mut step, mut default, mut caps) = (0, 0, 0, 0, 0);
unsafe {
match prop {
Prop::VideoProcAmp(id) => self
.proc_amp
.as_ref()
.ok_or(ControlError::Unsupported)?
.GetRange(
id,
&raw mut min,
&raw mut max,
&raw mut step,
&raw mut default,
&raw mut caps,
),
Prop::CameraControl(id) => self
.camera_control
.as_ref()
.ok_or(ControlError::Unsupported)?
.GetRange(
id,
&raw mut min,
&raw mut max,
&raw mut step,
&raw mut default,
&raw mut caps,
),
}
}
.map_err(|_| ControlError::Unsupported)?;
let current = self.get(prop).map_or(default, |(value, _)| value);
Ok((
ControlRange {
min,
max,
default,
current,
},
caps,
))
}
fn get(&self, prop: Prop) -> Result<(i32, i32), ControlError> {
let (mut value, mut flags) = (0, 0);
unsafe {
match prop {
Prop::VideoProcAmp(id) => self
.proc_amp
.as_ref()
.ok_or(ControlError::Unsupported)?
.Get(id, &raw mut value, &raw mut flags),
Prop::CameraControl(id) => self
.camera_control
.as_ref()
.ok_or(ControlError::Unsupported)?
.Get(id, &raw mut value, &raw mut flags),
}
}
.map_err(|_| ControlError::Unsupported)?;
Ok((value, flags))
}
fn auto_engaged(&self, prop: Prop) -> Result<bool, ControlError> {
Ok(self.get(prop)?.1 & FLAG_AUTO != 0)
}
fn set(&self, prop: Prop, value: i32, flags: i32) -> Result<(), ControlError> {
unsafe {
match prop {
Prop::VideoProcAmp(id) => self
.proc_amp
.as_ref()
.ok_or(ControlError::Unsupported)?
.Set(id, value, flags),
Prop::CameraControl(id) => self
.camera_control
.as_ref()
.ok_or(ControlError::Unsupported)?
.Set(id, value, flags),
}
}
.map_err(|_| ControlError::Unsupported)
}
fn set_auto(&self, prop: Prop, on: bool) -> Result<(), ControlError> {
let value = self.get(prop).map_or(0, |(v, _)| v);
self.set(prop, value, if on { FLAG_AUTO } else { FLAG_MANUAL })
}
}
fn monikers() -> windows::core::Result<Vec<IMoniker>> {
unsafe {
let _ = CoInitializeEx(None, COINIT_MULTITHREADED);
let dev_enum: windows::Win32::Media::DirectShow::ICreateDevEnum =
CoCreateInstance(&CLSID_SYSTEM_DEVICE_ENUM, None, CLSCTX_INPROC_SERVER)?;
let mut enum_moniker: Option<IEnumMoniker> = None;
dev_enum.CreateClassEnumerator(
&CLSID_VIDEO_INPUT_DEVICE_CATEGORY,
&raw mut enum_moniker,
0,
)?;
let Some(enum_moniker) = enum_moniker else {
return Ok(Vec::new());
};
let mut all = Vec::new();
loop {
let mut chunk = [const { None }; 8];
let mut fetched = 0;
let hr = enum_moniker.Next(&mut chunk, Some(&raw mut fetched));
all.extend(chunk.into_iter().take(fetched as usize).flatten());
if hr.is_err() || fetched == 0 {
break;
}
}
Ok(all)
}
}
fn camera_from_moniker(moniker: &IMoniker) -> Option<Camera> {
let unique_id = read_property(moniker, w!("DevicePath"))?;
let (vendor_id, product_id) = parse_device_path_ids(&unique_id)?;
let name = read_property(moniker, w!("FriendlyName")).unwrap_or_else(|| "Camera".into());
let serial_number = usb_serial_from_device_path(&unique_id);
Some(Camera {
name,
unique_id,
serial_number,
vendor_id,
product_id,
max_resolution: None,
max_fps: None,
})
}
pub(crate) fn usb_serial_from_device_path(path: &str) -> Option<String> {
let instance = path.split('#').nth(2)?.split('\\').next()?.trim();
if instance.is_empty() || instance.contains('&') {
return None;
}
Some(instance.to_string())
}
fn read_property(moniker: &IMoniker, name: windows::core::PCWSTR) -> Option<String> {
unsafe {
let bag: IPropertyBag = moniker.BindToStorage(None, None).ok()?;
let mut value = VARIANT::default();
bag.Read(name, &raw mut value, None).ok()?;
if value.Anonymous.Anonymous.vt != VT_BSTR {
return None;
}
Some(value.Anonymous.Anonymous.Anonymous.bstrVal.to_string())
}
}
fn parse_device_path_ids(path: &str) -> Option<(u16, u16)> {
let lower = path.to_ascii_lowercase();
let hex_after = |marker: &str| -> Option<u16> {
let rest = lower.split(marker).nth(1)?;
u16::from_str_radix(rest.get(..4)?, 16).ok()
};
Some((hex_after("vid_")?, hex_after("pid_")?))
}
#[cfg(test)]
mod tests {
use super::usb_serial_from_device_path;
#[test]
fn real_usb_serial_is_extracted() {
let path = r"\\?\usb#vid_046d&pid_0893&mi_00#6B123456#{65e8773d-8f56-11d0-a3b9-00a0c9223196}\global";
assert_eq!(
usb_serial_from_device_path(path).as_deref(),
Some("6B123456")
);
}
#[test]
fn parent_generated_instance_is_rejected() {
let path = r"\\?\usb#vid_046d&pid_0893&mi_00#9&56d9c30&0&0000#{65e8773d-8f56-11d0-a3b9-00a0c9223196}\global";
assert_eq!(usb_serial_from_device_path(path), None);
}
}