Skip to content

Commit 5cccfae

Browse files
Merge pull request #2248 from CapSoftware/fix/windows-camera-idle-resources
fix: stop repeated Windows camera resource allocation
2 parents 39752db + 56316d3 commit 5cccfae

5 files changed

Lines changed: 439 additions & 56 deletions

File tree

apps/desktop-gpui/src/devices.rs

Lines changed: 266 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,23 @@ pub struct DeviceSnapshot {
135135
}
136136

137137
impl DeviceSnapshot {
138+
pub fn update_camera_formats(
139+
&mut self,
140+
queried: &CameraOption,
141+
formats: Vec<CameraFormat>,
142+
) -> bool {
143+
let Some(camera) = self
144+
.cameras
145+
.iter_mut()
146+
.find(|camera| camera.same_device(queried))
147+
else {
148+
return false;
149+
};
150+
camera.best_format = formats.first().copied();
151+
camera.formats = formats;
152+
true
153+
}
154+
138155
/// Enumerate everything. This blocks — AVFoundation camera discovery and the
139156
/// window-server queries are both slow enough to drop frames — so callers
140157
/// should run it on the background executor, never inside `render`.
@@ -179,8 +196,8 @@ pub enum InputSnapshot {
179196
}
180197

181198
impl InputSnapshot {
182-
pub fn cameras() -> Self {
183-
Self::Cameras(list_cameras())
199+
pub fn cameras(previous: &[CameraOption]) -> Self {
200+
Self::Cameras(list_cameras_with_previous(previous))
184201
}
185202

186203
pub fn microphones() -> Self {
@@ -189,7 +206,20 @@ impl InputSnapshot {
189206

190207
pub fn install(self, snapshot: &mut DeviceSnapshot) -> bool {
191208
match self {
192-
Self::Cameras(cameras) if snapshot.cameras != cameras => {
209+
Self::Cameras(mut cameras) => {
210+
for camera in &mut cameras {
211+
if camera.formats.is_empty()
212+
&& let Some(current) = snapshot.cameras.iter().find(|current| {
213+
current.same_device(camera) && !current.formats.is_empty()
214+
})
215+
{
216+
camera.best_format = current.best_format;
217+
camera.formats = current.formats.clone();
218+
}
219+
}
220+
if snapshot.cameras == cameras {
221+
return false;
222+
}
193223
snapshot.cameras = cameras;
194224
}
195225
Self::Microphones(microphones) if snapshot.microphones != microphones => {
@@ -251,6 +281,149 @@ mod input_enumeration_tests {
251281
assert!(InputSnapshot::Microphones(Vec::new()).install(&mut snapshot));
252282
assert_eq!(snapshot.cameras, vec![camera]);
253283
}
284+
fn camera_fixture(id: &str) -> CameraOption {
285+
CameraOption {
286+
device_id: id.into(),
287+
model_id: None,
288+
label: "Camera".into(),
289+
best_format: None,
290+
formats: Vec::new(),
291+
}
292+
}
293+
294+
fn formats_fixture() -> Vec<CameraFormat> {
295+
vec![CameraFormat {
296+
width: 1920,
297+
height: 1080,
298+
frame_rate: 30.0,
299+
}]
300+
}
301+
302+
#[test]
303+
fn completed_query_updates_matching_camera_capabilities() {
304+
let queried = camera_fixture("camera-a");
305+
let mut snapshot = DeviceSnapshot {
306+
cameras: vec![queried.clone()],
307+
..Default::default()
308+
};
309+
assert!(snapshot.update_camera_formats(&queried, formats_fixture()));
310+
assert_eq!(snapshot.cameras[0].formats, formats_fixture());
311+
assert_eq!(
312+
snapshot.cameras[0].best_format,
313+
formats_fixture().first().copied()
314+
);
315+
}
316+
317+
#[test]
318+
fn completed_query_cannot_overwrite_replacement_camera_capabilities() {
319+
let queried = camera_fixture("camera-a");
320+
let mut changed_id = queried.clone();
321+
changed_id.device_id = "camera-b".into();
322+
let mut changed_model = queried.clone();
323+
changed_model.model_id =
324+
Some(cap_camera::ModelID::try_from("046d:08e5".to_string()).unwrap());
325+
let mut changed_label = queried.clone();
326+
changed_label.label = "Replacement camera".into();
327+
for replacement in [changed_id, changed_model, changed_label] {
328+
let mut snapshot = DeviceSnapshot {
329+
cameras: vec![replacement.clone()],
330+
..Default::default()
331+
};
332+
assert!(!snapshot.update_camera_formats(&queried, formats_fixture()));
333+
assert_eq!(snapshot.cameras, vec![replacement]);
334+
}
335+
}
336+
337+
#[test]
338+
fn completed_query_cannot_restore_disconnected_camera() {
339+
let mut snapshot = DeviceSnapshot::default();
340+
assert!(!snapshot.update_camera_formats(&camera_fixture("camera-a"), formats_fixture()));
341+
assert!(snapshot.cameras.is_empty());
342+
}
343+
344+
#[test]
345+
fn repeated_camera_refresh_does_not_reopen_unchanged_devices() {
346+
let mut previous = vec![camera_fixture("camera-a").with_formats(&[], formats_fixture)];
347+
for _ in 0..1000 {
348+
let refreshed = camera_fixture("camera-a").with_formats(&previous, || {
349+
panic!("unchanged camera must not reopen its driver")
350+
});
351+
assert_eq!(refreshed, previous[0]);
352+
previous = vec![refreshed];
353+
}
354+
}
355+
356+
#[test]
357+
fn new_camera_does_not_inherit_another_devices_formats() {
358+
let previous = vec![camera_fixture("camera-a").with_formats(&[], formats_fixture)];
359+
let mut probes = 0;
360+
let refreshed = camera_fixture("camera-b").with_formats(&previous, || {
361+
probes += 1;
362+
Vec::new()
363+
});
364+
assert_eq!(probes, 1);
365+
assert!(refreshed.formats.is_empty());
366+
assert!(refreshed.best_format.is_none());
367+
}
368+
369+
#[test]
370+
fn reconnect_refreshes_formats_after_camera_disappears() {
371+
let connected = camera_fixture("camera-a").with_formats(&[], formats_fixture);
372+
let after_disconnect = Vec::new();
373+
let changed_format = CameraFormat {
374+
width: 1280,
375+
height: 720,
376+
frame_rate: 60.0,
377+
};
378+
let reconnected =
379+
camera_fixture("camera-a").with_formats(&after_disconnect, || vec![changed_format]);
380+
assert_ne!(reconnected.formats, connected.formats);
381+
assert_eq!(reconnected.best_format, Some(changed_format));
382+
}
383+
384+
#[test]
385+
fn renamed_camera_refreshes_capabilities() {
386+
let previous = vec![camera_fixture("camera-a").with_formats(&[], formats_fixture)];
387+
let mut camera = camera_fixture("camera-a");
388+
camera.label = "Reconfigured virtual camera".into();
389+
let mut probes = 0;
390+
camera.with_formats(&previous, || {
391+
probes += 1;
392+
Vec::new()
393+
});
394+
assert_eq!(probes, 1);
395+
}
396+
#[test]
397+
fn failed_probe_is_not_retried_by_background_refresh() {
398+
let previous = vec![camera_fixture("camera-a").with_formats(&[], Vec::new)];
399+
let refreshed = camera_fixture("camera-a").with_formats(&previous, || {
400+
panic!("background refresh must not retry a native probe")
401+
});
402+
assert!(refreshed.formats.is_empty());
403+
}
404+
405+
#[test]
406+
fn late_refresh_does_not_erase_successful_explicit_retry() {
407+
let refreshed = vec![camera_fixture("camera-a")];
408+
let mut snapshot = DeviceSnapshot {
409+
cameras: vec![camera_fixture("camera-a").with_formats(&[], formats_fixture)],
410+
..Default::default()
411+
};
412+
assert!(!InputSnapshot::Cameras(refreshed).install(&mut snapshot));
413+
assert_eq!(snapshot.cameras[0].formats, formats_fixture());
414+
}
415+
416+
#[test]
417+
fn changed_device_does_not_inherit_previous_capabilities() {
418+
let mut changed = camera_fixture("camera-a");
419+
changed.label = "Changed virtual camera".into();
420+
let mut snapshot = DeviceSnapshot {
421+
cameras: vec![camera_fixture("camera-a").with_formats(&[], formats_fixture)],
422+
..Default::default()
423+
};
424+
assert!(InputSnapshot::Cameras(vec![changed]).install(&mut snapshot));
425+
assert!(snapshot.cameras[0].formats.is_empty());
426+
}
254427
}
255428

256429
/// Just the capture targets.
@@ -279,44 +452,107 @@ impl TargetSnapshot {
279452
}
280453

281454
fn list_cameras() -> Vec<CameraOption> {
455+
list_cameras_with_previous(&[])
456+
}
457+
458+
fn list_cameras_with_previous(previous: &[CameraOption]) -> Vec<CameraOption> {
282459
cap_camera::list_cameras()
283460
.map(|camera| {
284-
let mut formats = camera
285-
.formats()
286-
.unwrap_or_default()
287-
.into_iter()
288-
.map(|format| CameraFormat {
289-
width: format.width(),
290-
height: format.height(),
291-
frame_rate: format.frame_rate(),
292-
})
293-
.collect::<Vec<_>>();
294-
let mut seen = std::collections::HashSet::new();
295-
formats.retain(|format| {
296-
seen.insert((
297-
format.width,
298-
format.height,
299-
format.frame_rate.round() as u32,
300-
))
301-
});
302-
formats.sort_by(|a, b| {
303-
(b.width * b.height)
304-
.cmp(&(a.width * a.height))
305-
.then(b.frame_rate.total_cmp(&a.frame_rate))
306-
});
307-
let best_format = formats.first().copied();
308-
309461
CameraOption {
310462
device_id: camera.device_id().to_string(),
311463
model_id: camera.model_id().cloned(),
312464
label: camera.display_name().to_string(),
313-
best_format,
314-
formats,
465+
best_format: None,
466+
formats: Vec::new(),
315467
}
468+
.with_formats(previous, || {
469+
camera
470+
.formats()
471+
.unwrap_or_default()
472+
.into_iter()
473+
.map(|format| CameraFormat {
474+
width: format.width(),
475+
height: format.height(),
476+
frame_rate: format.frame_rate(),
477+
})
478+
.collect()
479+
})
316480
})
317481
.collect()
318482
}
319483

484+
impl CameraOption {
485+
fn same_device(&self, other: &Self) -> bool {
486+
self.device_id == other.device_id
487+
&& self.model_id == other.model_id
488+
&& self.label == other.label
489+
}
490+
491+
fn with_formats(
492+
mut self,
493+
previous: &[Self],
494+
load_formats: impl FnOnce() -> Vec<CameraFormat>,
495+
) -> Self {
496+
// Windows format probing opens native camera sources; polling an unchanged
497+
// device must reuse metadata rather than repeatedly activating its driver.
498+
if let Some(previous) = previous.iter().find(|previous| previous.same_device(&self)) {
499+
self.best_format = previous.best_format;
500+
self.formats = previous.formats.clone();
501+
return self;
502+
}
503+
504+
let mut formats = load_formats();
505+
let mut seen = std::collections::HashSet::new();
506+
formats.retain(|format| {
507+
seen.insert((
508+
format.width,
509+
format.height,
510+
format.frame_rate.round() as u32,
511+
))
512+
});
513+
formats.sort_by(|a, b| {
514+
(u64::from(b.width) * u64::from(b.height))
515+
.cmp(&(u64::from(a.width) * u64::from(a.height)))
516+
.then(b.frame_rate.total_cmp(&a.frame_rate))
517+
});
518+
self.best_format = formats.first().copied();
519+
self.formats = formats;
520+
self
521+
}
522+
}
523+
524+
pub fn camera_formats(device_id: &str) -> Result<Vec<CameraFormat>, String> {
525+
let camera = cap_camera::list_cameras()
526+
.find(|camera| camera.device_id() == device_id)
527+
.ok_or_else(|| "Camera is no longer available. Reconnect it and try again.".to_string())?;
528+
let formats = camera.formats().ok_or_else(|| {
529+
"Could not read camera formats. Check camera access and try again.".to_string()
530+
})?;
531+
if formats.is_empty() {
532+
return Err(
533+
"No camera formats are available. Check camera access and try again.".to_string(),
534+
);
535+
}
536+
Ok(CameraOption {
537+
device_id: camera.device_id().to_string(),
538+
model_id: camera.model_id().cloned(),
539+
label: camera.display_name().to_string(),
540+
best_format: None,
541+
formats: Vec::new(),
542+
}
543+
.with_formats(&[], || {
544+
formats
545+
.into_iter()
546+
.map(|format| CameraFormat {
547+
width: format.width(),
548+
height: format.height(),
549+
frame_rate: format.frame_rate(),
550+
})
551+
.collect()
552+
})
553+
.formats)
554+
}
555+
320556
/// Mirrors `MicrophoneFeed::list_with_settings`: the default input device is
321557
/// inserted first so it heads the list, then every other input device is
322558
/// appended, deduped by name.

0 commit comments

Comments
 (0)