Skip to content

Commit 5c06b5c

Browse files
committed
Fix locking, camera check, and writer finalization
Minimize time holding the DLCLiveProcessor lifecycle lock by moving expensive operations (frame.copy and timestamp handling) outside the lock and re-checking state before starting the worker to avoid races. Correct a duplicated condition in MultiCameraController so normal shutdown is detected using self._running. Move writer cleanup into a dedicated _finalize_writer(), add a warning when Queue.task_done() is called too many times, and centralize writer close/exception handling.
1 parent d0999a7 commit 5c06b5c

File tree

3 files changed

+23
-15
lines changed

3 files changed

+23
-15
lines changed

dlclivegui/services/dlc_processor.py

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,6 @@
1414
from typing import Any
1515

1616
import numpy as np
17-
18-
# from dlclive import DLCLive
1917
from PySide6.QtCore import QObject, Signal
2018

2119
from dlclivegui.config import DLCProcessorSettings, ModelType
@@ -216,16 +214,30 @@ def shutdown(self) -> None:
216214
self._initialized = False
217215

218216
def enqueue_frame(self, frame: np.ndarray, timestamp: float) -> None:
217+
# Keep lifecycle lock held only for quick state checks and snapshots.
219218
with self._lifecycle_lock:
220219
if self._state in (WorkerState.STOPPING, WorkerState.FAULTED) or self._stop_event.is_set():
221220
return
222-
frame_c = frame.copy()
223-
enq_time = time.perf_counter()
224221
t = self._worker_thread
225-
if t is None or not t.is_alive():
226-
self._start_worker_locked(frame_c, timestamp)
227-
return
228-
q = self._queue # snapshot under lock
222+
q = self._queue
223+
should_start = t is None or not t.is_alive()
224+
225+
frame_c = frame.copy()
226+
enq_time = time.perf_counter()
227+
228+
if should_start:
229+
# Re-acquire the lifecycle lock to safely (re)start the worker if needed.
230+
with self._lifecycle_lock:
231+
# Re-check state in case it changed while we were copying the frame.
232+
if self._state in (WorkerState.STOPPING, WorkerState.FAULTED) or self._stop_event.is_set():
233+
return
234+
t = self._worker_thread
235+
if t is None or not t.is_alive():
236+
# _start_worker_locked expects the lifecycle lock to be held.
237+
self._start_worker_locked(frame_c, timestamp)
238+
return
239+
# Worker is now running; refresh queue snapshot.
240+
q = self._queue
229241

230242
if q is None:
231243
return

dlclivegui/services/multi_camera_controller.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -514,7 +514,7 @@ def _on_camera_stopped(self, camera_id: str) -> None:
514514
# Check if all running cameras have stopped (normal shutdown)
515515
if (
516516
not self._started_cameras
517-
and not self._started_cameras
517+
and self._running
518518
and all(not t.isRunning() for t in self._threads.values() if t is not None)
519519
):
520520
self._running = False

dlclivegui/services/video_recorder.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -340,15 +340,11 @@ def _writer_loop(self) -> None:
340340
try:
341341
q.task_done()
342342
except ValueError:
343+
logger.warning("Queue task_done() called too many times in writer loop")
343344
pass
344345

345346
finally:
346-
writer = self._writer
347-
if writer is not None:
348-
try:
349-
writer.close()
350-
except Exception:
351-
logger.exception("Failed to close WriteGear during writer loop finalization")
347+
self._finalize_writer()
352348

353349
def _finalize_writer(self) -> None:
354350
writer = self._writer

0 commit comments

Comments
 (0)