diff --git a/crates/camera-directshow/examples/cli.rs b/crates/camera-directshow/examples/cli.rs index 00161139dd8..0cc75850c30 100644 --- a/crates/camera-directshow/examples/cli.rs +++ b/crates/camera-directshow/examples/cli.rs @@ -42,7 +42,12 @@ mod windows { let device = selected.0; - let video_control = device.output_pin().cast::().ok(); + let output_pin = device + .output_pin() + .expect("failed to bind capture filter for selected device") + .clone(); + + let video_control = output_pin.cast::().ok(); let formats = device .media_types() @@ -65,7 +70,7 @@ mod windows { if let Some(video_control) = &video_control { let time_per_frame_list = video_control.time_per_frame_list( - device.output_pin(), + &output_pin, i as i32, SIZE { cx: width, diff --git a/crates/camera-directshow/src/lib.rs b/crates/camera-directshow/src/lib.rs index 9563e20b58b..e7ddbf6ff40 100644 --- a/crates/camera-directshow/src/lib.rs +++ b/crates/camera-directshow/src/lib.rs @@ -2,7 +2,7 @@ #![allow(non_snake_case)] use std::{ - cell::RefCell, + cell::{OnceCell, RefCell}, ffi::{OsString, c_void}, mem::ManuallyDrop, ops::Deref, @@ -359,32 +359,54 @@ impl Iterator for VideoInputDeviceIterator { } } +/// The parts of a device that require actually instantiating the capture +/// filter. Binding these opens the camera through its KS driver, which costs a +/// thread and dozens of kernel handles that are not reclaimed on release - so +/// it is deferred until something genuinely needs a filter, pin or format. #[derive(Clone)] -pub struct VideoInputDevice { - moniker: IMoniker, - prop_bag: IPropertyBag, +struct BoundFilter { filter: IBaseFilter, output_pin: IPin, stream_config: IAMStreamConfig, } +#[derive(Clone)] +pub struct VideoInputDevice { + moniker: IMoniker, + prop_bag: IPropertyBag, + bound: OnceCell, +} + impl VideoInputDevice { fn new(moniker: IMoniker) -> windows_core::Result { + // BindToObject is deliberately not called here; see `BoundFilter`. let prop_bag: IPropertyBag = unsafe { moniker.BindToStorage(None, None) }?; - let filter: IBaseFilter = unsafe { moniker.BindToObject(None, None) }?; + Ok(Self { + moniker, + prop_bag, + bound: OnceCell::new(), + }) + } + + fn bound(&self) -> windows_core::Result<&BoundFilter> { + if let Some(bound) = self.bound.get() { + return Ok(bound); + } + + let filter: IBaseFilter = unsafe { self.moniker.BindToObject(None, None) }?; let output_pin = filter .get_pin(PINDIR_OUTPUT, PIN_CATEGORY_CAPTURE, GUID::zeroed()) .ok_or(E_FAIL)?; let stream_config = output_pin.cast::().ok().ok_or(E_FAIL)?; - Ok(Self { - moniker, - prop_bag, + let _ = self.bound.set(BoundFilter { filter, output_pin, stream_config, - }) + }); + + self.bound.get().ok_or_else(|| E_FAIL.into()) } pub fn name(&self) -> Option { @@ -410,22 +432,24 @@ impl VideoInputDevice { } pub fn media_types(&self) -> Option> { - self.stream_config + self.bound() + .ok()? + .stream_config .media_types() .map(|inner| VideoMediaTypesIterator { inner }) .ok() } - pub fn filter(&self) -> &IBaseFilter { - &self.filter + pub fn filter(&self) -> Option<&IBaseFilter> { + Some(&self.bound().ok()?.filter) } - pub fn stream_config(&self) -> &IAMStreamConfig { - &self.stream_config + pub fn stream_config(&self) -> Option<&IAMStreamConfig> { + Some(&self.bound().ok()?.stream_config) } - pub fn output_pin(&self) -> &IPin { - &self.output_pin + pub fn output_pin(&self) -> Option<&IPin> { + Some(&self.bound().ok()?.output_pin) } pub fn start_capturing( @@ -433,8 +457,11 @@ impl VideoInputDevice { format: &AMMediaType, callback: SinkCallback, ) -> Result { + let bound = self.bound().map_err(StartCapturingError::Other)?.clone(); + unsafe { - self.stream_config + bound + .stream_config .SetFormat(&**format) .map_err(StartCapturingError::Other)?; @@ -460,7 +487,7 @@ impl VideoInputDevice { .SetFiltergraph(&graph_builder) .map_err(StartCapturingError::ConfigureGraph)?; graph_builder - .AddFilter(&self.filter, None) + .AddFilter(&bound.filter, None) .map_err(StartCapturingError::ConfigureGraph)?; let sink_filter: IBaseFilter = sink_filter @@ -476,14 +503,14 @@ impl VideoInputDevice { .FindInterface( Some(&PIN_CATEGORY_CAPTURE), Some(&MEDIATYPE_Video), - &self.filter, + &bound.filter, &IAMStreamConfig::IID, &mut stream_config, ) .map_err(StartCapturingError::ConfigureGraph)?; graph_builder - .Connect(&self.output_pin, &input_sink_pin) + .Connect(&bound.output_pin, &input_sink_pin) .map_err(StartCapturingError::ConfigureGraph)?; media_control.Run().map_err(StartCapturingError::Run)?; @@ -491,7 +518,7 @@ impl VideoInputDevice { Ok(CaptureHandle { media_control, graph_builder, - output_capture_pin: self.output_pin, + output_capture_pin: bound.output_pin.clone(), input_sink_pin, }) } diff --git a/crates/camera-windows/examples/enumeration_leak.rs b/crates/camera-windows/examples/enumeration_leak.rs new file mode 100644 index 00000000000..5051b0c8e15 --- /dev/null +++ b/crates/camera-windows/examples/enumeration_leak.rs @@ -0,0 +1,39 @@ +//! Splits the camera-enumeration leak between the Media Foundation and +//! DirectShow halves of `get_devices()`. +//! +//! Usage: enumeration_leak.exe [mf|ds|both] [iterations] +//! +//! Sample handles/threads from outside while it runs; whichever mode grows is +//! the leaking half. + +fn main() { + let mode = std::env::args().nth(1).unwrap_or_else(|| "both".into()); + let iterations: usize = std::env::args() + .nth(2) + .and_then(|v| v.parse().ok()) + .unwrap_or(30); + + println!("pid {} mode={mode} iterations={iterations}", std::process::id()); + + let _ = cap_camera_directshow::initialize_directshow(); + let _ = cap_camera_mediafoundation::initialize_mediafoundation(); + + for i in 1..=iterations { + let n = match mode.as_str() { + "mf" => cap_camera_mediafoundation::DeviceSourcesIterator::new() + .map(|it| it.count()) + .unwrap_or(0), + + "ds" => cap_camera_directshow::VideoInputDeviceIterator::new() + .map(|it| it.count()) + .unwrap_or(0), + + _ => cap_camera_windows::get_devices().map(|d| d.len()).unwrap_or(0), + }; + + println!("{i}: {n} device(s)"); + std::thread::sleep(std::time::Duration::from_millis(1000)); + } + + println!("done"); +}