fix(windows): don't instantiate capture filters while enumerating cameras (leaks ~43 handles + 1 thread per poll) - #2129
Conversation
…eras
Listing DirectShow devices called IMoniker::BindToObject on every device,
which instantiates that camera's capture filter and opens the device through
its KS driver. Those resources are not reclaimed when the filter is released,
so every enumeration leaked handles and a thread.
The desktop app enumerates continuously - spawn_devices_snapshot_emitter
every 500ms-5s plus the frontend's 5s listVideoDevices poll - so the leak is
unbounded for the life of the process.
Measured on Windows 11 with 7 camera devices present (one physical, six
virtual: Quest Link x4, SpoutCam, OBS Virtual Camera), calling
cap_camera::list_cameras() once per second:
before 851 -> 2184 handles, 21 -> 52 threads in ~30s
after 356 -> 358 handles, 10 -> 10 threads, flat
That is roughly +43 handles and +1 thread per enumeration, scaling with
device count. Over an hour of normal app use it reaches thousands of threads
and tens of thousands of handles, which shows up as progressive slowdown and
then hard failures: STATUS_INVALID_HANDLE, access violations reading
devenum.dll, and thread stack exhaustion.
Fix: enumeration only needs the moniker's property bag (BindToStorage) for
name, id and model id. Defer BindToObject until something actually needs the
filter, pin or stream config - formats() or start_capturing(). The filter is
cached in a OnceCell so behaviour is unchanged for real users of it.
filter(), output_pin() and stream_config() now return Option because binding
can fail for a device that is unplugged or in use; media_types() already
returned Option and is unchanged for callers.
Adds crates/camera-windows/examples/enumeration_leak.rs to reproduce and
verify: run with 'mf', 'ds' or 'both' and watch handle/thread counts.
Before this change 'ds' climbs and 'mf' is flat; after, both are flat.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
|
||
| for i in 1..=iterations { |
There was a problem hiding this comment.
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!
Removes narration that restated the code it sat above (the example's match arms, and doc comments on bound()/filter()). Keeps only the BoundFilter note, which records the platform behaviour the fix exists for. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Valid — fixed in the latest push. Removed the narration: the example's match-arm comments (the module doc already says what the modes do) and the doc comments on Kept one comment, on |
Fixes the underlying cause of the Windows instability in #2115. Supersedes my earlier attempt in #2117 (see the correction there — that patch was real but treated a symptom on the Media Foundation side; this is the leak that was actually killing the process).
The bug
VideoInputDevice::new()calledIMoniker::BindToObjectfor every device during plain enumeration. That instantiates the camera's DirectShow capture filter and opens the device through its KS driver, and those resources are not reclaimed when the filter is released.Camera enumeration runs continuously —
spawn_devices_snapshot_emitterevery 500 ms→5 s, plus the frontend's 5 slistVideoDevicespoll — so the leak is unbounded for the life of the process.Measurement
Windows 11, 7 camera devices present (1 physical + 6 virtual: Quest Link ×4, SpoutCam, OBS Virtual Camera). Calling
cap_camera::list_cameras()once per second, nothing else running:≈ +43 handles and +1 thread per enumeration, scaling with device count.
Splitting the two halves of
get_devices()isolates it — Media Foundation is flat (317 handles, 9 threads, unchanged), DirectShow accounts for all of it.In the running desktop app the same growth was ~9 handles/second, monotonic. After the fix, handles and threads stay flat across a session.
Why this produces the crashes in #2115
Thousands of leaked threads (1 MB reserved stack each) and tens of thousands of handles per hour explain the whole symptom cluster, and why it looks random:
0xC000070A/STATUS_INVALID_HANDLEon a threadpool wait — handle exhaustion0xC0000005reading insidedevenum.dllthread 'tokio-runtime-worker' has overflowed its stackIt also explains the reporting bias: the leak is per device per poll, so a laptop with one webcam leaks ~6 handles per poll while a machine with Quest Link, OBS Virtual Camera and similar leaks 7× that. Multi-camera Windows setups get hit hard; a typical dev machine barely shows it.
The fix
Enumeration only needs the moniker's property bag (
BindToStorage) for name, id and model id.BindToObjectis deferred until something actually needs the filter, pin or stream config —formats()orstart_capturing()— and cached in aOnceCell, so behaviour is unchanged for real consumers.filter(),output_pin()andstream_config()now returnOptionbecause binding can fail for a device that is unplugged or in use.media_types()already returnedOption; callers are unchanged. Only the crate's own example needed updating.Reproducing it yourself
crates/camera-windows/examples/enumeration_leak.rsis included:Run with
mf,dsorbothand watch the process's handle/thread counts (Get-Process). Before this changedsclimbs steadily andmfis flat; after, both are flat. Happy to drop the example from the PR if you'd rather not carry it.Not addressed here
The polling itself is still aggressive — two independent loops re-enumerating every 2.5–5 s forever, each doing a full MF + DirectShow scan. With this fix that is no longer a leak, but caching results and refreshing on
WM_DEVICECHANGEinstead would remove a lot of steady-state work. Happy to do that separately if you want it.🤖 Generated with Claude Code
Greptile Summary
The PR defers DirectShow capture-filter instantiation until formats or capture are requested, avoiding resource growth during ordinary Windows camera enumeration.
Confidence Score: 4/5
The PR appears safe to merge, with only redundant comments in the new diagnostic example requiring non-blocking cleanup.
The lazy DirectShow binding path and current callers remain coherent, while the sole accepted concern is maintainability-only commentary in the diagnostic example.
Files Needing Attention: crates/camera-windows/examples/enumeration_leak.rs
Important Files Changed
Prompt To Fix All With AI
Reviews (1): Last reviewed commit: "fix(windows): don't instantiate capture ..." | Re-trigger Greptile
Context used: