Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions crates/camera-directshow/examples/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,12 @@ mod windows {

let device = selected.0;

let video_control = device.output_pin().cast::<IAMVideoControl>().ok();
let output_pin = device
.output_pin()
.expect("failed to bind capture filter for selected device")
.clone();

let video_control = output_pin.cast::<IAMVideoControl>().ok();

let formats = device
.media_types()
Expand All @@ -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,
Expand Down
69 changes: 48 additions & 21 deletions crates/camera-directshow/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
#![allow(non_snake_case)]

use std::{
cell::RefCell,
cell::{OnceCell, RefCell},
ffi::{OsString, c_void},
mem::ManuallyDrop,
ops::Deref,
Expand Down Expand Up @@ -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<BoundFilter>,
}

impl VideoInputDevice {
fn new(moniker: IMoniker) -> windows_core::Result<Self> {
// 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::<IAMStreamConfig>().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<OsString> {
Expand All @@ -410,31 +432,36 @@ impl VideoInputDevice {
}

pub fn media_types(&self) -> Option<VideoMediaTypesIterator<'_>> {
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(
self,
format: &AMMediaType,
callback: SinkCallback,
) -> Result<CaptureHandle, StartCapturingError> {
let bound = self.bound().map_err(StartCapturingError::Other)?.clone();

unsafe {
self.stream_config
bound
.stream_config
.SetFormat(&**format)
.map_err(StartCapturingError::Other)?;

Expand All @@ -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
Expand All @@ -476,22 +503,22 @@ 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)?;

Ok(CaptureHandle {
media_control,
graph_builder,
output_capture_pin: self.output_pin,
output_capture_pin: bound.output_pin.clone(),
input_sink_pin,
})
}
Expand Down
39 changes: 39 additions & 0 deletions crates/camera-windows/examples/enumeration_leak.rs
Original file line number Diff line number Diff line change
@@ -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 {
Comment on lines +20 to +21

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Redundant branch narration

The comments above the mf, ds, and fallback branches only restate the immediately following calls, adding documentation that must be kept synchronized without providing non-obvious context.

Context Used: AGENTS.md (source)

Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/camera-windows/examples/enumeration_leak.rs
Line: 20-21

Comment:
**Redundant branch narration**

The comments above the `mf`, `ds`, and fallback branches only restate the immediately following calls, adding documentation that must be kept synchronized without providing non-obvious context.

**Context Used:** AGENTS.md ([source](https://github.com/capsoftware/cap/blob/main/AGENTS.md))

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

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");
}