4 fps was not "live", and it was a number I picked rather than measured. The engine actually produces ~12 distinct frames a second, so most of it was being left on the floor. Now: poll a little ahead of the engine and drop frames identical to the last one by hash. Measured end to end - 131 frames in 10 s, 13.1 fps, 20.3 KB each, 259 KB/s, zero duplicates. Every byte on the wire is a picture the viewer has not seen, and the rate follows the camera instead of a constant. Also records why this is MJPEG rather than passing the camera's own compressed video through, which would be smoother, cheaper and use no CPU. Probed the office camera: main 2304x1296@15, sub 800x448@15 - and BOTH are H.265, despite stream paths ending in ".264". Browsers play H.264 everywhere and H.265 only on some platforms, so passthrough cannot rely on it, and transcoding HEVC on the shop PC would put a video encoder on the machine already doing the recognition. So probe_source now reports `codec`. It decides what is possible, an installer can usually change it, and otherwise the only way to learn it is to read RTSP by hand - which is how this was found. The RTSP libraries used to establish that are NOT kept: they were only ever imported by a spike test, and two large dependencies in a shipped binary to answer a question OpenCV already knows is a bad trade. Their `go get` had also silently bumped the agent to go 1.25 and broken the desktop build, which is its own argument. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HViLj9gYNRtSr7YVZmW5sn
109 lines
3.3 KiB
Python
109 lines
3.3 KiB
Python
import numpy as np
|
|
|
|
from behavision.capture import VideoSource
|
|
|
|
|
|
def _source_with_frame(ts=100.0):
|
|
"""VideoSource opens nothing until run(), so the frame slot can be
|
|
populated directly — no camera needed."""
|
|
src = VideoSource("cam1", 0)
|
|
src._frame = np.zeros((4, 4, 3), dtype=np.uint8)
|
|
src._frame_ts = ts
|
|
return src
|
|
|
|
|
|
def test_latest_since_skips_an_already_seen_frame():
|
|
src = _source_with_frame(ts=100.0)
|
|
frame, ts = src.latest_since(100.0)
|
|
assert frame is None
|
|
assert ts == 100.0
|
|
|
|
|
|
def test_latest_since_returns_a_newer_frame():
|
|
src = _source_with_frame(ts=100.0)
|
|
frame, ts = src.latest_since(99.0)
|
|
assert frame is not None
|
|
assert ts == 100.0
|
|
|
|
|
|
def test_latest_since_returns_a_copy():
|
|
src = _source_with_frame()
|
|
frame, _ = src.latest_since(0.0)
|
|
frame[0, 0, 0] = 255
|
|
assert src._frame[0, 0, 0] == 0
|
|
|
|
|
|
def test_latest_since_with_no_frame_yet():
|
|
frame, ts = VideoSource("cam1", 0).latest_since(0.0)
|
|
assert frame is None
|
|
assert ts == 0.0
|
|
|
|
|
|
def test_first_connect_is_not_counted_as_a_reconnect():
|
|
assert VideoSource("cam1", 0).stats()["reconnects"] == 0
|
|
|
|
|
|
def _elapsed(fn):
|
|
import time
|
|
t = time.perf_counter()
|
|
result = fn()
|
|
return result, time.perf_counter() - t
|
|
|
|
|
|
def test_probe_fails_fast_on_an_unroutable_host():
|
|
"""The Test button must answer, not hang. cv2.VideoCapture blocks in its
|
|
constructor on TCP connect (~75s OS timeout), so a pre-flight check is the
|
|
only thing that bounds this."""
|
|
from behavision.capture import probe_source
|
|
|
|
result, seconds = _elapsed(lambda: probe_source(
|
|
"rtsp://10.255.255.1:554/x", connect_timeout=1.0))
|
|
assert result["ok"] is False
|
|
assert "10.255.255.1:554" in result["error"]
|
|
assert seconds < 5, f"took {seconds:.1f}s - the pre-flight is not working"
|
|
|
|
|
|
def test_probe_reports_connection_refused_immediately():
|
|
from behavision.capture import probe_source
|
|
|
|
result, seconds = _elapsed(lambda: probe_source("rtsp://127.0.0.1:1/x"))
|
|
assert result["ok"] is False
|
|
assert seconds < 5
|
|
|
|
|
|
def test_tcp_precheck_passes_through_non_url_sources():
|
|
"""A webcam index has no host to check; it must not be blocked."""
|
|
from behavision.capture import _tcp_reachable
|
|
|
|
assert _tcp_reachable(0, 1.0) == (True, "")
|
|
assert _tcp_reachable("not-a-url", 1.0)[0] is True
|
|
|
|
|
|
def test_the_probe_reports_the_stream_codec():
|
|
"""Which codec a camera emits decides whether head office can ever show
|
|
TRUE live video from it: a browser plays H.264 everywhere and H.265 only on
|
|
some platforms, so a passthrough relay cannot rely on H.265.
|
|
|
|
It is reported because otherwise the only way to learn it is to read RTSP by
|
|
hand — which is how it was found on the office camera, whose stream paths
|
|
end in ".264" while both channels emit H.265.
|
|
"""
|
|
from behavision.capture import _fourcc
|
|
|
|
class Cap:
|
|
def __init__(self, v):
|
|
self.v = v
|
|
|
|
def get(self, _):
|
|
return self.v
|
|
|
|
def code(text):
|
|
return sum(ord(c) << (8 * i) for i, c in enumerate(text))
|
|
|
|
assert _fourcc(Cap(code("hevc"))) == "hevc"
|
|
assert _fourcc(Cap(code("h264"))) == "h264"
|
|
# Nothing to report is "" rather than a misleading value: a camera that
|
|
# does not say must not read as one that said something.
|
|
assert _fourcc(Cap(0)) == ""
|
|
assert _fourcc(Cap(-1)) == ""
|