Live view at head office, relayed through the agent's outbound connection
I got this wrong first time. "Head office cannot show live video cheaply" conflated TRUE VIDEO with SEEING THE CAMERA NOW, and only the first needs WebRTC and a TURN server. The shop PC is behind a router with no inbound route, so head office cannot pull the engine's MJPEG. It can answer the agent's outbound requests, which is the shape of everything else here: the server holds a poll open, the agent asks "is anyone watching?", and pushes JPEGs up for exactly as long as somebody is. Measured on the office camera: 98 KB full frame, 20.8 KB re-encoded at 640/q60, so one watcher costs ~83 KB/s. 47 frames arrived in 12 seconds - 4 fps, as configured. The UI says "about 4 frames a second" rather than letting anyone conclude the camera stutters. Nothing is uploaded when nobody is looking, which is the whole cost argument: Publish returns false once the last viewer goes, interest lapses on a timer each viewer refreshes as it reads (so a closed tab stops the upload within seconds), one push is capped at five minutes, and the UI streams one camera at a time. LiveHub is deliberately the opposite of the arrivals Hub. There a doorbell pushes nothing because nothing may be lost; here a dropped frame is the correct outcome, so each viewer has a one-slot buffer that is overwritten - the only frame worth having is the newest, and a queue would show an ever-growing delay behind the shop instead of dropping back to live. Ownership is proved once, before anything streams: the relay is keyed on a camera id, a hub does not know whose camera it holds, and a camera id is not a secret. Verified: another tenant gets 404, no session gets 401, and an agent cannot push into another site's camera. Also fixes a bug I introduced with it - the Live button was gated on `connected`, which is head office's last report and up to two minutes stale, so it hid itself during every reconnect. "Is that camera really down?" is exactly when somebody wants to look, and a hidden control says "you cannot" where the honest answer is "here is why". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HViLj9gYNRtSr7YVZmW5sn
This commit is contained in:
89
CLAUDE.md
89
CLAUDE.md
@@ -2173,7 +2173,7 @@ Two bugs, both found by running it after a reboot rather than by reading it.
|
||||
stay up. A failure there means the server cannot authenticate to its own
|
||||
broker, which is exactly what this script exists to surface early.
|
||||
|
||||
## Where the live feed is, and why head office gets a picture instead
|
||||
## Live view at head office, and what it honestly is
|
||||
|
||||
Live video exists and always has — on the shop PC, where the camera is:
|
||||
|
||||
@@ -2181,20 +2181,81 @@ Live video exists and always has — on the shop PC, where the camera is:
|
||||
on the office camera: 1280×720, ~850 KB/s.
|
||||
- The desktop app's Live screen and the engine's own dashboard both render it.
|
||||
|
||||
**Head office does not, and cannot cheaply.** The engine serves that stream on
|
||||
`127.0.0.1` on a PC behind a shop's router with no inbound route. Putting live
|
||||
video on `platform.loyaly.ai` needs a relay — WebRTC with TURN, or the agent
|
||||
pushing a continuous stream up — which is infrastructure and bandwidth this does
|
||||
not have. Nor can the browser be pointed straight at the shop PC even on one
|
||||
LAN: the engine's API is Basic-authenticated with a credential it generates
|
||||
locally and never sends anywhere, and shipping that credential to the cloud so a
|
||||
web page could use it would put the key to the biometric API and the live face
|
||||
feed in the server's database. That is a far worse trade than not having live
|
||||
video at head office.
|
||||
Head office has it too, and this is the part that was got wrong first: the
|
||||
original answer here was "cannot cheaply", which conflated **true video** with
|
||||
**seeing the camera now**. Only the first needs infrastructure this does not
|
||||
have.
|
||||
|
||||
So head office shows the camera's **latest frame**, refreshed every 60 s. The
|
||||
agent already holds it in memory for its own stream, so it costs a memory copy
|
||||
rather than a camera round trip.
|
||||
The shop PC is behind a router with no inbound route, so head office cannot
|
||||
pull that stream. What it CAN do is answer the agent's outbound requests, which
|
||||
is the shape of everything else in this system — so `LiveHub` + `cameras.Live`
|
||||
relay frames the other way: head office holds a poll open, the agent asks "is
|
||||
anyone watching?", and pushes JPEGs up for exactly as long as somebody is.
|
||||
|
||||
**It is ~4 frames a second of 640 px JPEG, and the UI says so.** Measured on the
|
||||
office camera: the full frame is 98 KB, re-encoded at 640/q60 it is 20.8 KB, so
|
||||
one watcher costs ~83 KB/s. True 25 fps video would need WebRTC and a TURN
|
||||
server; for *"what does that camera see right now"* — which is the question
|
||||
somebody at head office is actually asking — a few frames a second is what the
|
||||
question requires, and a label saying "about 4 frames a second" is better than
|
||||
letting somebody conclude the camera stutters.
|
||||
|
||||
The browser cannot be pointed straight at the shop PC even on one LAN: the
|
||||
engine's API is Basic-authenticated with a credential it generates locally and
|
||||
never sends anywhere, and shipping that to the cloud so a web page could use it
|
||||
would put the key to the biometric API and the live face feed in the server's
|
||||
database.
|
||||
|
||||
**Nothing is uploaded when nobody is looking**, and that is the entire cost
|
||||
argument:
|
||||
|
||||
- `Publish` returns false once the last viewer has gone, which is what tells the
|
||||
agent to stop pushing. If it were ever optimistic every shop PC in an estate
|
||||
would upload continuously.
|
||||
- Interest lapses on a timer refreshed by each viewer as it reads, so a browser
|
||||
that vanishes without saying so — the normal way a tab closes — stops the
|
||||
upload within seconds.
|
||||
- One push is capped at five minutes. A tab left open for a week must not leave
|
||||
a shop uploading for a week; a viewer who is still there simply reconnects.
|
||||
- Only one camera streams at a time in the UI. A grid that went live all at once
|
||||
would put an estate's worth of cameras on the wire because somebody opened a
|
||||
page.
|
||||
|
||||
**`LiveHub` is the exact opposite of the arrivals `Hub`, deliberately.** There a
|
||||
doorbell pushes nothing because nothing may be lost. Here a dropped frame is the
|
||||
*correct* outcome: each viewer has a one-slot buffer and a full slot is
|
||||
overwritten, because the only frame worth having is the newest one and a queue
|
||||
would show an ever-growing delay behind the shop instead of dropping back to
|
||||
live.
|
||||
|
||||
Other decisions worth keeping:
|
||||
|
||||
- **Ownership is proved once, before anything streams.** Everything after that
|
||||
point is keyed on a camera id and a hub does not know whose camera it holds —
|
||||
and a camera id is not a secret. An agent pushing is checked against its own
|
||||
site for the same reason.
|
||||
- **The agent's poll is held open by the server** rather than answered at once.
|
||||
Polling every few seconds puts a floor under how quickly a view can start;
|
||||
polling slowly puts a ceiling on it. Holding it means pressing Live reaches
|
||||
the shop PC immediately and an idle site costs about two requests a minute.
|
||||
- **One request carries many frames**, each prefixed with its length. At a few
|
||||
frames a second, per-request overhead and TLS handshakes would cost more than
|
||||
the pictures.
|
||||
- **The engine does the re-encode** (`frame.jpg?width=&quality=`). It already
|
||||
has OpenCV open and the frame decoded; a scaler in the agent would be the same
|
||||
work twice. On demand only — a camera nobody watches must not pay for a second
|
||||
encode it will never use.
|
||||
- **The Live button is offered even when the card says the camera is down.**
|
||||
`connected` is head office's last report and can be two minutes stale, so
|
||||
gating on it hid the button during every reconnect — and "is that camera
|
||||
really down?" is exactly when somebody wants to look. A hidden control says
|
||||
*"you cannot"* where the honest answer is *"here is why"*, which the live view
|
||||
gives: it distinguishes a camera that is not connecting from a shop PC that is
|
||||
not answering.
|
||||
|
||||
Head office also still shows the camera's **latest frame** on the cards,
|
||||
refreshed every 60 s, which is what a page of cameras should cost when nobody
|
||||
has asked to watch one.
|
||||
|
||||
### The picture only worked if you had an S3 bucket
|
||||
|
||||
|
||||
Reference in New Issue
Block a user