Skip to content

Commit e370c25

Browse files
committed
fix: stop repeated Windows camera resource allocation
1 parent 39752db commit e370c25

5 files changed

Lines changed: 381 additions & 56 deletions

File tree

apps/desktop-gpui/src/devices.rs

Lines changed: 207 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -179,8 +179,8 @@ pub enum InputSnapshot {
179179
}
180180

181181
impl InputSnapshot {
182-
pub fn cameras() -> Self {
183-
Self::Cameras(list_cameras())
182+
pub fn cameras(previous: &[CameraOption]) -> Self {
183+
Self::Cameras(list_cameras_with_previous(previous))
184184
}
185185

186186
pub fn microphones() -> Self {
@@ -189,7 +189,20 @@ impl InputSnapshot {
189189

190190
pub fn install(self, snapshot: &mut DeviceSnapshot) -> bool {
191191
match self {
192-
Self::Cameras(cameras) if snapshot.cameras != cameras => {
192+
Self::Cameras(mut cameras) => {
193+
for camera in &mut cameras {
194+
if camera.formats.is_empty()
195+
&& let Some(current) = snapshot.cameras.iter().find(|current| {
196+
current.same_device(camera) && !current.formats.is_empty()
197+
})
198+
{
199+
camera.best_format = current.best_format;
200+
camera.formats = current.formats.clone();
201+
}
202+
}
203+
if snapshot.cameras == cameras {
204+
return false;
205+
}
193206
snapshot.cameras = cameras;
194207
}
195208
Self::Microphones(microphones) if snapshot.microphones != microphones => {
@@ -251,6 +264,107 @@ mod input_enumeration_tests {
251264
assert!(InputSnapshot::Microphones(Vec::new()).install(&mut snapshot));
252265
assert_eq!(snapshot.cameras, vec![camera]);
253266
}
267+
fn camera_fixture(id: &str) -> CameraOption {
268+
CameraOption {
269+
device_id: id.into(),
270+
model_id: None,
271+
label: "Camera".into(),
272+
best_format: None,
273+
formats: Vec::new(),
274+
}
275+
}
276+
277+
fn formats_fixture() -> Vec<CameraFormat> {
278+
vec![CameraFormat {
279+
width: 1920,
280+
height: 1080,
281+
frame_rate: 30.0,
282+
}]
283+
}
284+
285+
#[test]
286+
fn repeated_camera_refresh_does_not_reopen_unchanged_devices() {
287+
let mut previous = vec![camera_fixture("camera-a").with_formats(&[], formats_fixture)];
288+
for _ in 0..1000 {
289+
let refreshed = camera_fixture("camera-a").with_formats(&previous, || {
290+
panic!("unchanged camera must not reopen its driver")
291+
});
292+
assert_eq!(refreshed, previous[0]);
293+
previous = vec![refreshed];
294+
}
295+
}
296+
297+
#[test]
298+
fn new_camera_does_not_inherit_another_devices_formats() {
299+
let previous = vec![camera_fixture("camera-a").with_formats(&[], formats_fixture)];
300+
let mut probes = 0;
301+
let refreshed = camera_fixture("camera-b").with_formats(&previous, || {
302+
probes += 1;
303+
Vec::new()
304+
});
305+
assert_eq!(probes, 1);
306+
assert!(refreshed.formats.is_empty());
307+
assert!(refreshed.best_format.is_none());
308+
}
309+
310+
#[test]
311+
fn reconnect_refreshes_formats_after_camera_disappears() {
312+
let connected = camera_fixture("camera-a").with_formats(&[], formats_fixture);
313+
let after_disconnect = Vec::new();
314+
let changed_format = CameraFormat {
315+
width: 1280,
316+
height: 720,
317+
frame_rate: 60.0,
318+
};
319+
let reconnected =
320+
camera_fixture("camera-a").with_formats(&after_disconnect, || vec![changed_format]);
321+
assert_ne!(reconnected.formats, connected.formats);
322+
assert_eq!(reconnected.best_format, Some(changed_format));
323+
}
324+
325+
#[test]
326+
fn renamed_camera_refreshes_capabilities() {
327+
let previous = vec![camera_fixture("camera-a").with_formats(&[], formats_fixture)];
328+
let mut camera = camera_fixture("camera-a");
329+
camera.label = "Reconfigured virtual camera".into();
330+
let mut probes = 0;
331+
camera.with_formats(&previous, || {
332+
probes += 1;
333+
Vec::new()
334+
});
335+
assert_eq!(probes, 1);
336+
}
337+
#[test]
338+
fn failed_probe_is_not_retried_by_background_refresh() {
339+
let previous = vec![camera_fixture("camera-a").with_formats(&[], Vec::new)];
340+
let refreshed = camera_fixture("camera-a").with_formats(&previous, || {
341+
panic!("background refresh must not retry a native probe")
342+
});
343+
assert!(refreshed.formats.is_empty());
344+
}
345+
346+
#[test]
347+
fn late_refresh_does_not_erase_successful_explicit_retry() {
348+
let refreshed = vec![camera_fixture("camera-a")];
349+
let mut snapshot = DeviceSnapshot {
350+
cameras: vec![camera_fixture("camera-a").with_formats(&[], formats_fixture)],
351+
..Default::default()
352+
};
353+
assert!(!InputSnapshot::Cameras(refreshed).install(&mut snapshot));
354+
assert_eq!(snapshot.cameras[0].formats, formats_fixture());
355+
}
356+
357+
#[test]
358+
fn changed_device_does_not_inherit_previous_capabilities() {
359+
let mut changed = camera_fixture("camera-a");
360+
changed.label = "Changed virtual camera".into();
361+
let mut snapshot = DeviceSnapshot {
362+
cameras: vec![camera_fixture("camera-a").with_formats(&[], formats_fixture)],
363+
..Default::default()
364+
};
365+
assert!(InputSnapshot::Cameras(vec![changed]).install(&mut snapshot));
366+
assert!(snapshot.cameras[0].formats.is_empty());
367+
}
254368
}
255369

256370
/// Just the capture targets.
@@ -279,44 +393,107 @@ impl TargetSnapshot {
279393
}
280394

281395
fn list_cameras() -> Vec<CameraOption> {
396+
list_cameras_with_previous(&[])
397+
}
398+
399+
fn list_cameras_with_previous(previous: &[CameraOption]) -> Vec<CameraOption> {
282400
cap_camera::list_cameras()
283401
.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-
309402
CameraOption {
310403
device_id: camera.device_id().to_string(),
311404
model_id: camera.model_id().cloned(),
312405
label: camera.display_name().to_string(),
313-
best_format,
314-
formats,
406+
best_format: None,
407+
formats: Vec::new(),
315408
}
409+
.with_formats(previous, || {
410+
camera
411+
.formats()
412+
.unwrap_or_default()
413+
.into_iter()
414+
.map(|format| CameraFormat {
415+
width: format.width(),
416+
height: format.height(),
417+
frame_rate: format.frame_rate(),
418+
})
419+
.collect()
420+
})
316421
})
317422
.collect()
318423
}
319424

425+
impl CameraOption {
426+
fn same_device(&self, other: &Self) -> bool {
427+
self.device_id == other.device_id
428+
&& self.model_id == other.model_id
429+
&& self.label == other.label
430+
}
431+
432+
fn with_formats(
433+
mut self,
434+
previous: &[Self],
435+
load_formats: impl FnOnce() -> Vec<CameraFormat>,
436+
) -> Self {
437+
// Windows format probing opens native camera sources; polling an unchanged
438+
// device must reuse metadata rather than repeatedly activating its driver.
439+
if let Some(previous) = previous.iter().find(|previous| previous.same_device(&self)) {
440+
self.best_format = previous.best_format;
441+
self.formats = previous.formats.clone();
442+
return self;
443+
}
444+
445+
let mut formats = load_formats();
446+
let mut seen = std::collections::HashSet::new();
447+
formats.retain(|format| {
448+
seen.insert((
449+
format.width,
450+
format.height,
451+
format.frame_rate.round() as u32,
452+
))
453+
});
454+
formats.sort_by(|a, b| {
455+
(u64::from(b.width) * u64::from(b.height))
456+
.cmp(&(u64::from(a.width) * u64::from(a.height)))
457+
.then(b.frame_rate.total_cmp(&a.frame_rate))
458+
});
459+
self.best_format = formats.first().copied();
460+
self.formats = formats;
461+
self
462+
}
463+
}
464+
465+
pub fn camera_formats(device_id: &str) -> Result<Vec<CameraFormat>, String> {
466+
let camera = cap_camera::list_cameras()
467+
.find(|camera| camera.device_id() == device_id)
468+
.ok_or_else(|| "Camera is no longer available. Reconnect it and try again.".to_string())?;
469+
let formats = camera.formats().ok_or_else(|| {
470+
"Could not read camera formats. Check camera access and try again.".to_string()
471+
})?;
472+
if formats.is_empty() {
473+
return Err(
474+
"No camera formats are available. Check camera access and try again.".to_string(),
475+
);
476+
}
477+
Ok(CameraOption {
478+
device_id: camera.device_id().to_string(),
479+
model_id: camera.model_id().cloned(),
480+
label: camera.display_name().to_string(),
481+
best_format: None,
482+
formats: Vec::new(),
483+
}
484+
.with_formats(&[], || {
485+
formats
486+
.into_iter()
487+
.map(|format| CameraFormat {
488+
width: format.width(),
489+
height: format.height(),
490+
frame_rate: format.frame_rate(),
491+
})
492+
.collect()
493+
})
494+
.formats)
495+
}
496+
320497
/// Mirrors `MicrophoneFeed::list_with_settings`: the default input device is
321498
/// inserted first so it heads the list, then every other input device is
322499
/// appended, deduped by name.

0 commit comments

Comments
 (0)