Skip to content

Commit f74bca6

Browse files
authored
Merge pull request #5 from ruscher/main
Fix Finishing. Good job!
2 parents de7678a + 3990ee1 commit f74bca6

3 files changed

Lines changed: 71 additions & 21 deletions

File tree

usr/share/biglinux/bigcam/core/backends/gphoto2_backend.py

Lines changed: 51 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -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

usr/share/biglinux/bigcam/script/run_webcam_gphoto2.sh

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -79,18 +79,12 @@ if ! timeout 10 gphoto2 --auto-detect 2>&1 | grep -q "$USB_PORT"; then
7979
# Try to find camera by name at a different port
8080
NEW_PORT=$(timeout 10 gphoto2 --auto-detect 2>/dev/null | grep -i "$CAM_NAME" | grep -oP 'usb:\S+' | head -1)
8181
if [ -n "$NEW_PORT" ]; then
82-
echo "INFO: Camera found at new port: $NEW_PORT"
82+
echo "INFO: Camera '$CAM_NAME' found at new port: $NEW_PORT"
8383
USB_PORT="$NEW_PORT"
8484
else
85-
# Last resort: just pick any Canon/DSLR camera
86-
NEW_PORT=$(timeout 10 gphoto2 --auto-detect 2>/dev/null | grep -v '^Model\|^---' | grep 'usb:' | grep -oP 'usb:\S+' | head -1)
87-
if [ -n "$NEW_PORT" ]; then
88-
echo "INFO: Using first available camera at port: $NEW_PORT"
89-
USB_PORT="$NEW_PORT"
90-
else
91-
echo "ERROR: No camera detected at any port."
92-
exit 1
93-
fi
85+
# Do NOT pick a random camera — that would stream the wrong one
86+
echo "ERROR: Camera '$CAM_NAME' not detected at any port."
87+
exit 1
9488
fi
9589
fi
9690

usr/share/biglinux/bigcam/ui/window.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,10 @@ def _on_camera_selected(self, _selector: CameraSelector, camera: CameraInfo) ->
278278
and backend.is_camera_streaming(camera))
279279
cached_controls = self._controls_cache.get(camera.id)
280280

281+
print(f"[DEBUG] already_streaming={already_streaming}, cached_controls={cached_controls is not None}, camera.id={camera.id}")
282+
if hasattr(backend, "_active_streams"):
283+
print(f"[DEBUG] _active_streams={dict(backend._active_streams)}")
284+
281285
if already_streaming and cached_controls is not None:
282286
# Hot-swap: camera already streaming, just switch the GStreamer pipeline
283287
print(f"[DEBUG] Hot-swap to {camera.name} (already streaming)")
@@ -340,9 +344,21 @@ def on_done(result: tuple[bool, list]) -> None:
340344
run_async(do_controls_then_stream, on_success=on_done)
341345
else:
342346
# V4L2, libcamera, PipeWire: load controls async + start stream
347+
# Stop only GStreamer pipeline on UI thread (instant)
348+
old_camera = self._stream_engine._current_camera
349+
old_backend_obj = None
350+
if old_camera and old_camera.backend != camera.backend:
351+
old_backend_obj = self._camera_manager.get_backend(old_camera.backend)
352+
self._stream_engine.stop(stop_backend=False)
353+
354+
# Start the V4L2 camera immediately
343355
self._controls_page.set_camera(camera)
344356
self._stream_engine.play(camera)
345357

358+
# Stop old backend (gphoto2) in background — has time.sleep() calls
359+
if old_backend_obj and hasattr(old_backend_obj, "stop_streaming"):
360+
run_async(lambda: old_backend_obj.stop_streaming(old_camera))
361+
346362
def _on_retry(self, _preview: PreviewArea) -> None:
347363
"""Re-attempt camera connection when user clicks Try Again."""
348364
if self._active_camera:

0 commit comments

Comments
 (0)