@@ -25,7 +25,8 @@ class GPhoto2Backend(CameraBackend):
2525
2626 _streaming_process : subprocess .Popen | None = None
2727 # Track active streaming sessions per camera port
28- _active_streams : dict [str , str ] = {} # port -> udp_port
28+ # port -> {"udp_port": str, "launch_port": str}
29+ _active_streams : dict [str , dict [str , str ]] = {}
2930
3031 def get_backend_type (self ) -> BackendType :
3132 return BackendType .GPHOTO2
@@ -85,7 +86,7 @@ def _release_usb_device(port: str) -> None:
8586 except (ProcessLookupError , FileNotFoundError , PermissionError ):
8687 pass
8788 if pids :
88- time .sleep (1 )
89+ time .sleep (3 )
8990 except Exception :
9091 pass
9192
@@ -241,6 +242,11 @@ def _refresh_port(cls, camera: CameraInfo) -> str:
241242 if name and name in camera .name :
242243 if port != old_port :
243244 print (f"[DEBUG] Port changed: { old_port } -> { port } " )
245+ # Update _active_streams key if camera was streaming
246+ if old_port in cls ._active_streams :
247+ stream_info = cls ._active_streams .pop (old_port )
248+ cls ._active_streams [port ] = stream_info
249+ print (f"[DEBUG] Updated _active_streams: { old_port } -> { port } " )
244250 camera .extra ["port" ] = port
245251 camera .device_path = port
246252 camera .id = f"gphoto2:{ port } "
@@ -353,9 +359,21 @@ def _refresh_port(cls, camera: CameraInfo) -> str:
353359
354360 def get_controls (self , camera : CameraInfo ) -> list [CameraControl ]:
355361 controls : list [CameraControl ] = []
356- port = camera .extra .get ("port" , camera .device_path )
362+
363+ # Refresh USB port first (device number changes after GVFS kill)
364+ port = self ._refresh_port (camera )
357365 print (f"[DEBUG] get_controls: port={ port } " )
358366
367+ # Check if the USB device actually exists
368+ try :
369+ bus , dev = port .replace ("usb:" , "" ).split ("," )
370+ usb_path = f"/dev/bus/usb/{ bus } /{ dev } "
371+ if not os .path .exists (usb_path ):
372+ print (f"[DEBUG] get_controls: { usb_path } does not exist, camera disconnected?" )
373+ return controls
374+ except (ValueError , OSError ):
375+ pass
376+
359377 # Ensure GVFS is dead and USB device is free
360378 self ._kill_gvfs ()
361379 self ._release_usb_device (port )
@@ -630,10 +648,10 @@ def start_streaming(self, camera: CameraInfo) -> bool:
630648 if line .startswith ("SUCCESS:" ):
631649 dev = line .split ("SUCCESS:" )[1 ].strip ()
632650 log .info ("GPhoto2 streaming started on %s" , dev )
633- self ._active_streams [port ] = udp_port
651+ self ._active_streams [port ] = { " udp_port" : udp_port , "launch_port" : port }
634652 return True
635653 log .info ("GPhoto2 script exited 0 (no explicit SUCCESS)" )
636- self ._active_streams [port ] = udp_port
654+ self ._active_streams [port ] = { " udp_port" : udp_port , "launch_port" : port }
637655 return True
638656
639657 log .error ("GPhoto2 script failed (code %d): %s" ,
@@ -652,23 +670,36 @@ def stop_streaming(self, camera: CameraInfo | None = None) -> None:
652670 if camera :
653671 port = camera .extra .get ("port" , camera .device_path )
654672 udp_port = str (camera .extra .get ("udp_port" , 5000 ))
655- self ._active_streams .pop (port , None )
673+ stream_info = self ._active_streams .pop (port , None )
674+ # Use the port the process was actually launched with
675+ launch_port = stream_info ["launch_port" ] if stream_info else port
656676
657677 # Graceful SIGTERM first — gives gphoto2 time to close PTP session
658678 subprocess .run (
659- ["pkill" , "-f" , f"gphoto2.*--port { port } " ],
679+ ["pkill" , "-f" , f"gphoto2.*--port { launch_port } " ],
660680 capture_output = True ,
661681 )
682+ # Also try current port if different
683+ if launch_port != port :
684+ subprocess .run (
685+ ["pkill" , "-f" , f"gphoto2.*--port { port } " ],
686+ capture_output = True ,
687+ )
662688 subprocess .run (
663689 ["pkill" , "-f" , f"ffmpeg.*udp://127.0.0.1:{ udp_port } " ],
664690 capture_output = True ,
665691 )
666692 time .sleep (2 )
667693 # Force-kill any survivors
668694 subprocess .run (
669- ["pkill" , "-9" , "-f" , f"gphoto2.*--port { port } " ],
695+ ["pkill" , "-9" , "-f" , f"gphoto2.*--port { launch_port } " ],
670696 capture_output = True ,
671697 )
698+ if launch_port != port :
699+ subprocess .run (
700+ ["pkill" , "-9" , "-f" , f"gphoto2.*--port { port } " ],
701+ capture_output = True ,
702+ )
672703 subprocess .run (
673704 ["pkill" , "-9" , "-f" , f"ffmpeg.*udp://127.0.0.1:{ udp_port } " ],
674705 capture_output = True ,
@@ -701,13 +732,22 @@ def is_camera_streaming(self, camera: CameraInfo) -> bool:
701732 port = camera .extra .get ("port" , camera .device_path )
702733 if port not in self ._active_streams :
703734 return False
704- # Verify the process is actually alive
705- udp_port = self ._active_streams [port ]
735+ # Verify the process is actually alive using the launch port
736+ stream_info = self ._active_streams [port ]
737+ launch_port = stream_info .get ("launch_port" , port )
706738 result = subprocess .run (
707- ["pgrep" , "-f" , f"gphoto2.*--port { port } " ],
739+ ["pgrep" , "-f" , f"gphoto2.*--port { launch_port } " ],
708740 capture_output = True ,
709741 )
710742 if result .returncode != 0 :
743+ # Also try current port (in case it matches)
744+ if launch_port != port :
745+ result = subprocess .run (
746+ ["pgrep" , "-f" , f"gphoto2.*--port { port } " ],
747+ capture_output = True ,
748+ )
749+ if result .returncode == 0 :
750+ return True
711751 # Process died — clean up
712752 self ._active_streams .pop (port , None )
713753 return False
0 commit comments