Files
Behavision/web/src/views/CameraLive.jsx
Suriyakumarvijayanayagam ffae7e45d5 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
2026-09-04 16:59:27 +05:30

86 lines
3.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useEffect, useRef, useState } from 'react'
import { streamCameraLive } from '../api.js'
// The live view of one camera.
//
// It is a few frames a second, not video, and the label says so rather than
// letting somebody conclude the camera is stuttering. That is the honest limit
// of relaying through the agent's outbound connection; true 25 fps would need
// WebRTC and a TURN server, which is a different piece of infrastructure.
//
// **Nothing is uploaded from the shop until this component is mounted**, and it
// stops within seconds of it going away. That is the whole reason a live view
// is affordable at all, and why this is a deliberate action rather than
// something every camera card does on its own.
export default function CameraLive({ camera, onClose }) {
const img = useRef(null)
const [state, setState] = useState('waiting')
// Nothing has arrived after a sensible wait. Its own state, because "the
// shop PC has not answered" is a different thing to tell somebody than
// "connecting", and leaving a spinner up forever tells them nothing at all.
const [stalled, setStalled] = useState(false)
useEffect(() => {
const ctrl = new AbortController()
const stop = streamCameraLive({
cameraId: camera.id, img,
onState: s => { setState(s); if (s === 'live') setStalled(false) },
signal: ctrl.signal,
})
// Generous: the server holds the agent's poll, the agent then has to reach
// the camera, and a cold start is a couple of seconds even when everything
// works.
const t = setTimeout(() => setStalled(true), 12000)
return () => { stop(); ctrl.abort(); clearTimeout(t) }
}, [camera.id])
// What to say when nothing arrives. The two causes need different actions -
// one is the camera, the other is the PC - so they must not share a message.
const stalledNote = camera.connected === false
? 'This camera was not connecting when the shop PC last reported. Check it is powered on and reachable on the shop’s network.'
: camera.connected == null
? 'The shop PC has not reported on this camera yet. It may still be starting up.'
: 'The shop PC is not sending frames. It may be offline, or its Behavision app may not be running.'
const note = stalled && state !== 'live'
? stalledNote
: {
waiting: 'Asking the shop PC…',
live: 'Live',
reconnecting: 'Reconnecting…',
}[state]
return (
<div className="overlay" onClick={onClose}>
<aside className="drawer live" onClick={e => e.stopPropagation()}>
<header className="drawer-head">
<div>
<h2>{camera.label}</h2>
<p className="sub">{camera.site}</p>
</div>
<button className="ghost" onClick={onClose}>Close</button>
</header>
<div className="drawer-body">
<div className="liveshot">
{/* Seeded with the stored snapshot so the first second shows the
camera rather than a black rectangle. It is the same view, a
minute old, which is a far better place to start from than
nothing. */}
<img ref={img} alt={`Live view from ${camera.label}`} />
{state !== 'live' && (
<div className="livewait">
<span>{stalled ? 'No picture yet' : note}</span>
</div>
)}
</div>
<p className="hint" style={{ marginTop: 10 }}>
{state === 'live'
? 'Live. The shop only uploads while this view is open.'
: note}
</p>
</div>
</aside>
</div>
)
}