@@ -179,8 +179,8 @@ pub enum InputSnapshot {
179179}
180180
181181impl 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
281395fn 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