#![expect(
unsafe_code,
reason = "Media Foundation COM (device activation + IMFSourceReader sample loop)"
)]
#![allow(
clippy::cast_possible_truncation,
clippy::cast_possible_wrap,
clippy::cast_sign_loss,
reason = "pixel dimensions and strides are bounded and copied verbatim"
)]
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::{Arc, Mutex, mpsc};
use std::time::{Duration, Instant};
use windows::Win32::Media::MediaFoundation::{
IMFActivate, IMFMediaSource, IMFMediaType, IMFSourceReader, MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE,
MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE_VIDCAP_GUID,
MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE_VIDCAP_SYMBOLIC_LINK, MF_MT_DEFAULT_STRIDE,
MF_MT_FRAME_SIZE, MF_MT_MAJOR_TYPE, MF_MT_SUBTYPE, MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING,
MF_SOURCE_READER_FIRST_VIDEO_STREAM, MF_VERSION, MFCreateAttributes, MFCreateMediaType,
MFCreateSourceReaderFromMediaSource, MFEnumDeviceSources, MFMediaType_Video, MFSTARTUP_LITE,
MFStartup, MFVideoFormat_NV12, MFVideoFormat_RGB24, MFVideoFormat_RGB32, MFVideoFormat_YUY2,
};
use windows::Win32::System::Com::{COINIT_MULTITHREADED, CoInitializeEx, CoTaskMemFree};
pub use crate::capture_types::{CaptureError, Frame};
const TARGET_WIDTH: u32 = 1280;
const SETUP_TIMEOUT: Duration = Duration::from_secs(5);
struct Shared {
latest: Mutex<Option<Arc<Frame>>>,
generation: AtomicU64,
stop: AtomicBool,
}
pub struct CameraStream {
shared: Arc<Shared>,
reader: Option<std::thread::JoinHandle<()>>,
}
impl CameraStream {
#[must_use]
pub fn latest_frame(&self) -> Option<Arc<Frame>> {
self.shared.latest.lock().ok().and_then(|slot| slot.clone())
}
#[must_use]
pub fn take_frame(&self) -> Option<Arc<Frame>> {
self.shared
.latest
.lock()
.ok()
.and_then(|mut slot| slot.take())
}
#[must_use]
pub fn frame_generation(&self) -> u64 {
self.shared.generation.load(Ordering::Relaxed)
}
}
impl Drop for CameraStream {
fn drop(&mut self) {
self.shared.stop.store(true, Ordering::Relaxed);
if let Some(reader) = self.reader.take() {
let _ = reader.join();
}
}
}
pub fn start_stream(unique_id: &str) -> Result<CameraStream, CaptureError> {
let shared = Arc::new(Shared {
latest: Mutex::new(None),
generation: AtomicU64::new(0),
stop: AtomicBool::new(false),
});
let (setup_tx, setup_rx) = mpsc::channel();
let thread_shared = Arc::clone(&shared);
let id = unique_id.to_string();
let reader = std::thread::Builder::new()
.name("openlogi-camera-reader".into())
.spawn(move || reader_thread(&id, &thread_shared, &setup_tx))
.map_err(|e| CaptureError::Setup(e.to_string()))?;
match setup_rx.recv_timeout(SETUP_TIMEOUT) {
Ok(Ok(())) => Ok(CameraStream {
shared,
reader: Some(reader),
}),
Ok(Err(e)) => {
let _ = reader.join();
Err(e)
}
Err(_) => {
shared.stop.store(true, Ordering::Relaxed);
Err(CaptureError::Timeout)
}
}
}
pub fn capture_frame(unique_id: &str, timeout: Duration) -> Result<Frame, CaptureError> {
let stream = start_stream(unique_id)?;
let deadline = Instant::now() + timeout;
loop {
if let Some(frame) = stream.take_frame() {
return Ok(Arc::unwrap_or_clone(frame));
}
if Instant::now() >= deadline {
return Err(CaptureError::Timeout);
}
std::thread::sleep(Duration::from_millis(30));
}
}
#[must_use]
pub fn camera_access_granted() -> bool {
true
}
#[must_use]
pub fn camera_authorization() -> crate::CameraAuthorization {
crate::CameraAuthorization::Granted
}
pub fn request_camera_access() {}
fn reader_thread(unique_id: &str, shared: &Shared, setup: &mpsc::Sender<Result<(), CaptureError>>) {
let reader = unsafe {
let _ = CoInitializeEx(None, COINIT_MULTITHREADED);
if let Err(e) = MFStartup(MF_VERSION, MFSTARTUP_LITE) {
let _ = setup.send(Err(CaptureError::Setup(e.to_string())));
return;
}
match open_reader(unique_id) {
Ok(opened) => opened,
Err(e) => {
let _ = setup.send(Err(e));
return;
}
}
};
let (reader, stride_hint) = reader;
let _ = setup.send(Ok(()));
while !shared.stop.load(Ordering::Relaxed) {
unsafe {
let (mut flags, mut sample) = (0u32, None);
if reader
.ReadSample(
MF_SOURCE_READER_FIRST_VIDEO_STREAM.0 as u32,
0,
None,
Some(&raw mut flags),
None,
Some(&raw mut sample),
)
.is_err()
{
break;
}
let Some(sample) = sample else { continue };
let Ok(buffer) = sample.ConvertToContiguousBuffer() else {
continue;
};
let (mut data, mut len) = (std::ptr::null_mut(), 0u32);
if buffer
.Lock(&raw mut data, None, Some(&raw mut len))
.is_err()
{
continue;
}
store_frame(shared, data, len as usize, stride_hint);
let _ = buffer.Unlock();
}
}
}
#[derive(Clone, Copy)]
struct StrideHint {
width: u32,
height: u32,
stride: i32,
}
unsafe fn open_reader(unique_id: &str) -> Result<(IMFSourceReader, StrideHint), CaptureError> {
unsafe {
let source = activate_source(unique_id)?;
let mut reader_attrs = None;
MFCreateAttributes(&raw mut reader_attrs, 1).map_err(setup_err)?;
let reader_attrs = reader_attrs.ok_or_else(|| setup_err("MFCreateAttributes"))?;
reader_attrs
.SetUINT32(&MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING, 1)
.map_err(setup_err)?;
let reader = MFCreateSourceReaderFromMediaSource(&source, &reader_attrs)
.map_err(|e| access_or_setup(&e))?;
let stream = MF_SOURCE_READER_FIRST_VIDEO_STREAM.0 as u32;
let mut best: Option<(u32, IMFMediaType)> = None;
let mut index = 0u32;
while let Ok(native) = reader.GetNativeMediaType(stream, index) {
index += 1;
let convertible = native.GetGUID(&MF_MT_SUBTYPE).is_ok_and(|subtype| {
[
MFVideoFormat_NV12,
MFVideoFormat_YUY2,
MFVideoFormat_RGB24,
MFVideoFormat_RGB32,
]
.contains(&subtype)
});
if !convertible {
continue;
}
if let Ok(size) = native.GetUINT64(&MF_MT_FRAME_SIZE) {
let width = (size >> 32) as u32;
let score = width.abs_diff(TARGET_WIDTH);
if best.as_ref().is_none_or(|(s, _)| score < *s) {
best = Some((score, native));
}
}
}
if let Some((_, native)) = &best {
reader
.SetCurrentMediaType(stream, None, native)
.map_err(setup_err)?;
}
let output = MFCreateMediaType().map_err(setup_err)?;
output
.SetGUID(&MF_MT_MAJOR_TYPE, &MFMediaType_Video)
.map_err(setup_err)?;
output
.SetGUID(&MF_MT_SUBTYPE, &MFVideoFormat_RGB32)
.map_err(setup_err)?;
reader
.SetCurrentMediaType(stream, None, &output)
.map_err(setup_err)?;
let current = reader.GetCurrentMediaType(stream).map_err(setup_err)?;
let size = current.GetUINT64(&MF_MT_FRAME_SIZE).map_err(setup_err)?;
let width = (size >> 32) as u32;
let height = (size & 0xFFFF_FFFF) as u32;
let stride = current
.GetUINT32(&MF_MT_DEFAULT_STRIDE)
.map_or(width as i32 * 4, |s| s as i32);
Ok((
reader,
StrideHint {
width,
height,
stride,
},
))
}
}
unsafe fn activate_source(unique_id: &str) -> Result<IMFMediaSource, CaptureError> {
unsafe {
let mut enum_attrs = None;
MFCreateAttributes(&raw mut enum_attrs, 1).map_err(setup_err)?;
let enum_attrs = enum_attrs.ok_or_else(|| setup_err("MFCreateAttributes"))?;
enum_attrs
.SetGUID(
&MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE,
&MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE_VIDCAP_GUID,
)
.map_err(setup_err)?;
let (mut devices, mut count) = (std::ptr::null_mut::<Option<IMFActivate>>(), 0u32);
MFEnumDeviceSources(&enum_attrs, &raw mut devices, &raw mut count).map_err(setup_err)?;
let list = std::slice::from_raw_parts(devices, count as usize);
let mut chosen = None;
for activate in list.iter().flatten() {
let (mut link, mut len) = (windows::core::PWSTR::null(), 0u32);
if activate
.GetAllocatedString(
&MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE_VIDCAP_SYMBOLIC_LINK,
&raw mut link,
&raw mut len,
)
.is_err()
{
continue;
}
let link_str = link.to_string().unwrap_or_default();
CoTaskMemFree(Some(link.as_ptr().cast()));
if device_instance(&link_str).eq_ignore_ascii_case(device_instance(unique_id)) {
chosen = Some(activate.clone());
break;
}
}
let result = match chosen {
Some(activate) => activate
.ActivateObject::<IMFMediaSource>()
.map_err(|e| access_or_setup(&e)),
None => Err(CaptureError::NotFound),
};
CoTaskMemFree(Some(devices.cast()));
result
}
}
fn device_instance(interface_path: &str) -> &str {
interface_path.split("#{").next().unwrap_or(interface_path)
}
fn store_frame(shared: &Shared, data: *mut u8, len: usize, hint: StrideHint) {
let (width, height) = (hint.width as usize, hint.height as usize);
let row_bytes = width * 4;
let stride = hint.stride.unsigned_abs() as usize;
if width == 0 || height == 0 || data.is_null() || stride * (height - 1) + row_bytes > len {
return;
}
let mut bgra = vec![0u8; row_bytes * height];
for y in 0..height {
let src_row = if hint.stride < 0 { height - 1 - y } else { y };
unsafe {
std::ptr::copy_nonoverlapping(
data.add(src_row * stride),
bgra.as_mut_ptr().add(y * row_bytes),
row_bytes,
);
}
}
for px in bgra.chunks_exact_mut(4) {
px[3] = 0xFF;
}
if let Ok(mut slot) = shared.latest.lock() {
*slot = Some(Arc::new(Frame {
width: hint.width,
height: hint.height,
bgra,
}));
shared.generation.fetch_add(1, Ordering::Relaxed);
}
}
fn setup_err(e: impl std::fmt::Display) -> CaptureError {
CaptureError::Setup(e.to_string())
}
fn access_or_setup(e: &windows::core::Error) -> CaptureError {
const E_ACCESSDENIED: windows::core::HRESULT = windows::core::HRESULT(0x8007_0005_u32 as i32);
if e.code() == E_ACCESSDENIED {
CaptureError::AccessDenied
} else {
CaptureError::Setup(e.to_string())
}
}
#[cfg(test)]
mod tests {
use super::device_instance;
const DIRECTSHOW: &str = r"\\?\usb#vid_046d&pid_0893&mi_00#9&56d9c30&0&0000#{65e8773d-8f56-11d0-a3b9-00a0c9223196}\global";
const MEDIA_FOUNDATION: &str = r"\\?\usb#vid_046d&pid_0893&mi_00#9&56d9c30&0&0000#{e5323777-f976-4f5b-9b55-b94699c46e44}\global";
#[test]
fn instance_matches_across_interface_class_guids() {
assert_eq!(
device_instance(DIRECTSHOW),
device_instance(MEDIA_FOUNDATION),
"the stored DirectShow id must match MF's symbolic link"
);
assert_eq!(
device_instance(DIRECTSHOW),
r"\\?\usb#vid_046d&pid_0893&mi_00#9&56d9c30&0&0000"
);
}
#[test]
fn distinct_devices_stay_distinct() {
let other = r"\\?\usb#vid_046d&pid_0825&mi_00#7&1a2b3c&0&0000#{e5323777-f976-4f5b-9b55-b94699c46e44}\global";
assert_ne!(device_instance(DIRECTSHOW), device_instance(other));
}
#[test]
fn path_without_interface_guid_is_returned_whole() {
assert_eq!(device_instance("not-a-device-path"), "not-a-device-path");
}
}