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:
@@ -3,6 +3,7 @@ package cameras
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
@@ -38,11 +39,15 @@ type Live struct {
|
||||
pollDelay time.Duration
|
||||
}
|
||||
|
||||
// Defaults chosen to be honest about a shop's uplink rather than impressive:
|
||||
// 640 px at quality 60 is ~25-35 KB, so 4 fps is ~120 KB/s per watcher, and a
|
||||
// camera nobody is watching costs nothing at all.
|
||||
// Defaults, measured against the office camera rather than guessed.
|
||||
//
|
||||
// The engine produces ~12 distinct frames a second, so asking for more than
|
||||
// that only re-sends pictures the viewer already has - which is why the poll
|
||||
// runs slightly ahead of it and identical frames are dropped rather than sent.
|
||||
// 640 px at quality 60 is ~20 KB, so a watcher costs ~200 KB/s at the full
|
||||
// rate, and a camera nobody is watching costs nothing at all.
|
||||
const (
|
||||
DefaultLiveFPS = 4.0
|
||||
DefaultLiveFPS = 15.0
|
||||
DefaultLiveWidth = 640
|
||||
DefaultLiveQuality = 60
|
||||
)
|
||||
@@ -105,6 +110,13 @@ func (l *Live) serve(ctx context.Context, cameraID string) {
|
||||
|
||||
tick := time.NewTicker(interval)
|
||||
defer tick.Stop()
|
||||
// The engine re-serves its latest frame until the pipeline produces a new
|
||||
// one, so polling faster than it encodes returns the SAME picture again.
|
||||
// Measured: 93 polls in 6 s yielded 72 distinct frames. Sending the
|
||||
// duplicates would cost a fifth of the bandwidth for nothing, so the poll
|
||||
// runs a little ahead of the engine and the repeats are dropped - which is
|
||||
// what lets the rate follow the camera instead of a guess.
|
||||
var lastSum [32]byte
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
@@ -128,6 +140,11 @@ func (l *Live) serve(ctx context.Context, cameraID string) {
|
||||
// stream, and the next tick may well have one.
|
||||
continue
|
||||
}
|
||||
if sum := sha256.Sum256(jpeg); sum == lastSum {
|
||||
continue
|
||||
} else {
|
||||
lastSum = sum
|
||||
}
|
||||
var hdr [4]byte
|
||||
binary.BigEndian.PutUint32(hdr[:], uint32(len(jpeg)))
|
||||
if _, err := pw.Write(hdr[:]); err != nil {
|
||||
@@ -142,9 +159,10 @@ func (l *Live) serve(ctx context.Context, cameraID string) {
|
||||
}
|
||||
|
||||
func (l *Live) fps() float64 {
|
||||
if l.FPS <= 0 || l.FPS > 15 {
|
||||
// Above this the relay stops being cheap and stops being honest about
|
||||
// what an outbound HTTP push can carry.
|
||||
if l.FPS <= 0 || l.FPS > 25 {
|
||||
// A ceiling rather than a target: duplicate frames are dropped, so
|
||||
// polling above what the engine encodes costs requests and no
|
||||
// bandwidth - but it is still work, on the PC doing the recognition.
|
||||
return DefaultLiveFPS
|
||||
}
|
||||
return l.FPS
|
||||
|
||||
Reference in New Issue
Block a user