Live view runs at the camera's real rate, and reports why it is MJPEG

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
This commit is contained in:
2026-09-04 16:59:27 +05:30
parent 18686cbceb
commit ffae7e45d5
7 changed files with 140 additions and 30 deletions

View File

@@ -47,6 +47,23 @@ def _tcp_reachable(source: "str | int", timeout: float
return False, f"cannot reach {parsed.hostname}:{port} - {exc.strerror or exc}"
def _fourcc(cap) -> str:
"""The stream's codec as a four-character code, or "" if unknown.
FFmpeg reports H.265 as "hevc" and H.264 as "h264"/"avc1" depending on the
container. Returned as-is rather than mapped to a friendly name: the raw
value is what somebody searching their camera's manual will match.
"""
try:
raw = int(cap.get(cv2.CAP_PROP_FOURCC))
except Exception:
return ""
if raw <= 0:
return ""
code = "".join(chr((raw >> (8 * i)) & 0xFF) for i in range(4))
return code.strip().strip("\x00")
def probe_source(source: "str | int", max_width: int = 1280,
timeout: float = 12.0, connect_timeout: float = 3.0) -> dict:
"""Open a candidate camera, grab one frame, and let go.
@@ -97,6 +114,17 @@ def probe_source(source: "str | int", max_width: int = 1280,
return {
"ok": True, "width": int(width), "height": int(height),
"downscaled_to": int(preview.shape[1]) if preview is not frame else None,
"fps": round(cap.get(cv2.CAP_PROP_FPS) or 0, 1),
# The codec decides whether head office can ever show TRUE live
# video from this camera. A browser plays H.264 everywhere; H.265
# only on some platforms, so a passthrough relay cannot rely on it
# and the picture has to be re-encoded frame by frame instead.
# Reported here because it is a property of the camera's settings
# that an installer can usually change, and because otherwise the
# only way to learn it is to read RTSP by hand — which is how this
# was found: a camera whose paths end in ".264" was emitting H.265
# on both streams.
"codec": _fourcc(cap),
"snapshot": (base64.b64encode(buf.tobytes()).decode("ascii")
if ok else None),
}