feat(v1): latest-frame-only reader thread for low-latency RTSP

Adds _FrameGrabber which decodes in a background thread and keeps only the
newest frame; VideoSource drop_stale (STREAM only) returns the latest frame
so a slow consumer never builds a backlog. app enables it for streams.
REPLAY is unchanged. 82 tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
ila
2026-07-22 00:04:00 +08:00
co-authored by Claude Opus 4.8
parent b32c36fc44
commit 6e69518742
6 changed files with 164 additions and 5 deletions
+69 -1
View File
@@ -1,8 +1,10 @@
import time
import cv2
import numpy as np
import pytest
from v1.video_source import SourceMode, SourceStatus, VideoSource
from v1.video_source import SourceMode, SourceStatus, VideoSource, _FrameGrabber
def _write_sample_video(path):
@@ -135,6 +137,72 @@ def test_stream_recovers_after_mid_stream_drop():
assert recovered.image is not None
class _ScriptedCapture:
def __init__(self, frames, fail_after=False):
self._frames = list(frames)
self._fail_after = fail_after
def isOpened(self):
return True
def read(self):
if self._frames:
return True, self._frames.pop(0)
return False, None
def get(self, _property_id):
return 0.0
def release(self):
pass
def test_frame_grabber_keeps_only_the_latest_frame():
frame_a = np.full((4, 4, 3), 1, dtype=np.uint8)
frame_b = np.full((4, 4, 3), 2, dtype=np.uint8)
frame_c = np.full((4, 4, 3), 3, dtype=np.uint8)
grabber = _FrameGrabber(_ScriptedCapture([frame_a, frame_b, frame_c]))
assert grabber._pump() is True
assert grabber._pump() is True
assert grabber._pump() is True
image, failed = grabber.take_latest()
assert failed is False
assert image is frame_c # older frames dropped
def test_frame_grabber_marks_failed_on_read_failure():
grabber = _FrameGrabber(_ScriptedCapture([np.zeros((4, 4, 3), dtype=np.uint8)]))
assert grabber._pump() is True
assert grabber._pump() is False
_, failed = grabber.take_latest()
assert failed is True
def test_stream_with_drop_stale_returns_a_connected_frame():
source = VideoSource(
"rtsp://demo",
mode=SourceMode.STREAM,
drop_stale=True,
capture_factory=lambda _s: _OpenCapture(),
)
try:
packet = None
for _ in range(40):
packet = source.read()
if packet.status is SourceStatus.CONNECTED and packet.image is not None:
break
time.sleep(0.02)
assert packet is not None
assert packet.status is SourceStatus.CONNECTED
assert packet.image is not None
finally:
source.close()
class _NegativeFirstTimestampCapture:
def __init__(self):
self._read_count = 0