Stream¶
One thread per camera. The frame never stops flowing.
Global Configuration¶
Set before any VideoCapture is created — these are process-wide FFmpeg options:
os.environ["OPENCV_FFMPEG_CAPTURE_OPTIONS"] = (
"rtsp_transport;tcp"
"|fflags;nobuffer"
"|flags;low_delay"
"|analyzeduration;1000000"
"|probesize;1000000"
"|stimeout;5000000"
)
| Option | Purpose |
|---|---|
rtsp_transport;tcp |
Force TCP for RTSP. UDP drops packets on congested networks. TCP is reliable. |
fflags;nobuffer |
Disable FFmpeg's internal buffering. Frames arrive as fast as possible. |
flags;low_delay |
Minimize decode latency. Trades quality for speed — appropriate for surveillance. |
analyzeduration;1000000 |
1 second to analyze the stream format. Prevents hanging on slow handshakes. |
probesize;1000000 |
1 MB probe size. Enough to detect stream metadata without reading the whole file. |
stimeout;5000000 |
5-second socket timeout. If no data arrives in 5s, the connection is dead. |
Thread Stack Size¶
Default Python thread stack is 8 MB. At 20 cameras, that's 160 MB of stack alone. Reducing to 512 KB saves ~150 MB with zero impact — stream reader threads use trivial stack depth. Must be called before any threads are spawned.
RTSPStream¶
class RTSPStream:
"""Reads a single RTSP stream in a dedicated thread with automatic reconnection."""
_MAX_CONSECUTIVE_FAILURES = 30
_BASE_RECONNECT_DELAY = 1.0
_MAX_RECONNECT_DELAY = 60.0
Constructor¶
Starts the daemon thread immediately. No separate start() call. By the time the constructor returns, frames may already be flowing.
| Attribute | Type | Purpose |
|---|---|---|
camera |
CameraConfig |
Camera configuration reference |
_frame |
np.ndarray \| None |
Latest decoded frame |
_has_frame |
bool |
Whether any frame has been received |
_lock |
threading.Lock |
Protects _frame and _has_frame |
_stopped |
threading.Event |
Signal to shut down the reader |
_consecutive_failures |
int |
Reconnection attempt counter (drives backoff) |
_connected |
bool |
Whether the stream is currently live |
_thread |
threading.Thread |
Daemon thread, named f"stream-{camera.id}" |
API¶
is_connected → bool¶
Property. Whether the stream is currently connected and producing frames. Read-only.
latest_frame() → tuple[bool, np.ndarray | None]¶
Returns (True, frame_copy) or (False, None).
Critical detail: returns frame.copy(). Without the copy, the caller would hold a reference to the same numpy array the reader thread is actively overwriting. Race condition. The copy is cheap relative to the cost of face detection.
stop()¶
Sets _stopped event. The reader thread checks this on every iteration and exits cleanly. No forced termination, no cleanup headaches.
Internal Methods¶
_run()¶
Main loop of the reader thread:
_connect() → cv2.VideoCapture | None¶
Opens the RTSP stream with cv2.VideoCapture(url, cv2.CAP_FFMPEG).
- Success: sets
_connected = True, resets_consecutive_failuresto 0, returns theVideoCapture. - Failure: releases the capture, sets
_connected = False, returnsNone.
_read_loop(cap) → None¶
Continuous cap.read() loop. Each successful read stores the frame under _lock.
Failure counting: consecutive failures increment a counter. At 30 consecutive failures, the loop exits, triggering reconnection. The _connected flag goes False.
Why 30? A single dropped frame is normal. Ten in a row means the stream is degraded. Thirty means the connection is dead but hasn't timed out yet.
_reconnect_wait() → None¶
Exponential backoff formula:
| Attempt | Delay |
|---|---|
| 1 | 2.0s |
| 2 | 4.0s |
| 3 | 8.0s |
| 4 | 16.0s |
| 5 | 32.0s |
| 6+ | 60.0s (capped) |
Uses self._stopped.wait(timeout=delay) instead of time.sleep(). This means stop() immediately interrupts the wait — no waiting for the full backoff to expire during shutdown.
Thread Safety Model¶
Reader Thread Main Thread
───────────── ───────────
cap.read()
↓
_lock.acquire()
_frame = frame
_has_frame = True
_lock.release()
stream.latest_frame()
_lock.acquire()
return _frame.copy()
_lock.release()
↓
detector.detect(frame_copy)
The lock window is minimal — just a reference assignment and a bool flip. Contention is negligible.