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
132 lines
4.0 KiB
Go
132 lines
4.0 KiB
Go
package api
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/loyaly/behavision-server/internal/auth"
|
|
)
|
|
|
|
const liveCam = "22222222-3333-4444-5555-666666666666"
|
|
|
|
// The whole cost argument for this feature: a camera nobody is watching must
|
|
// cost a shop nothing at all. If Wanted were ever optimistic, every shop PC in
|
|
// an estate would upload continuously.
|
|
func TestNobodyWatchingMeansNothingIsWanted(t *testing.T) {
|
|
h := NewLiveHub()
|
|
if h.Wanted(liveCam) {
|
|
t.Fatal("a camera nobody has asked for was reported as wanted")
|
|
}
|
|
if got := h.WantedAmong([]string{liveCam, "other"}); len(got) != 0 {
|
|
t.Fatalf("wanted %v with no viewers", got)
|
|
}
|
|
}
|
|
|
|
func TestAViewerMakesACameraWantedAndReleasingStopsIt(t *testing.T) {
|
|
h := NewLiveHub()
|
|
_, release := h.Watch(liveCam)
|
|
if !h.Wanted(liveCam) {
|
|
t.Fatal("a watched camera was not wanted")
|
|
}
|
|
release()
|
|
if h.Wanted(liveCam) {
|
|
t.Fatal("the camera stayed wanted after the last viewer left")
|
|
}
|
|
}
|
|
|
|
// The opposite policy to the arrivals Hub, and deliberately so. There nothing
|
|
// may be lost; here the only frame worth having is the newest one, and a queue
|
|
// would show a viewer an ever-growing delay behind the shop instead of dropping
|
|
// back to live.
|
|
func TestASlowViewerGetsTheNewestFrameNotTheOldest(t *testing.T) {
|
|
h := NewLiveHub()
|
|
frames, release := h.Watch(liveCam)
|
|
defer release()
|
|
|
|
for _, f := range []string{"one", "two", "three"} {
|
|
h.Publish(liveCam, []byte(f))
|
|
}
|
|
select {
|
|
case got := <-frames:
|
|
if string(got) != "three" {
|
|
t.Fatalf("a slow viewer was served %q, want the newest frame", got)
|
|
}
|
|
default:
|
|
t.Fatal("nothing was delivered")
|
|
}
|
|
}
|
|
|
|
// Publish reporting false is what stops the shop PC uploading. If it kept
|
|
// saying true the agent would push into an empty room until the session cap.
|
|
func TestPublishReportsWhenTheLastViewerHasGone(t *testing.T) {
|
|
h := NewLiveHub()
|
|
_, release := h.Watch(liveCam)
|
|
if !h.Publish(liveCam, []byte("frame")) {
|
|
t.Fatal("publish said to stop while somebody was watching")
|
|
}
|
|
release()
|
|
if h.Publish(liveCam, []byte("frame")) {
|
|
t.Fatal("publish did not say to stop after the last viewer left")
|
|
}
|
|
}
|
|
|
|
// A browser that vanishes without saying so - the normal way a tab closes -
|
|
// must stop the upload on its own.
|
|
func TestInterestExpiresWithoutBeingRefreshed(t *testing.T) {
|
|
h := NewLiveHub()
|
|
frames, release := h.Watch(liveCam)
|
|
defer release()
|
|
_ = frames
|
|
|
|
h.mu.Lock()
|
|
h.cameras[liveCam].wanted = time.Now().Add(-time.Second)
|
|
h.mu.Unlock()
|
|
|
|
if h.Wanted(liveCam) {
|
|
t.Fatal("interest did not lapse")
|
|
}
|
|
if h.Publish(liveCam, []byte("frame")) {
|
|
t.Fatal("publish kept the shop uploading for a viewer that had gone")
|
|
}
|
|
}
|
|
|
|
// The relay is keyed on a camera id and a hub does not know whose camera it is
|
|
// holding, so ownership has to be proved before anything streams. Otherwise a
|
|
// camera id - which is not a secret - would be enough to watch another
|
|
// company's shop floor.
|
|
func TestAnotherTenantCannotWatchYourCamera(t *testing.T) {
|
|
srv, fs := newServer(t)
|
|
fs.addCameraRef(liveCam, "client-1", "site-1", "cam1")
|
|
|
|
rr := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodGet, "/api/cameras/"+liveCam+"/live", nil)
|
|
req = req.WithContext(context.WithValue(req.Context(), principalKey,
|
|
auth.Principal{ClientID: "someone-else", Role: "owner"}))
|
|
srv.handleWatchLive(rr, req)
|
|
|
|
if rr.Code != http.StatusNotFound {
|
|
t.Fatalf("status %d, want 404", rr.Code)
|
|
}
|
|
}
|
|
|
|
// An agent may only push into its OWN site's camera. The agent supplies this
|
|
// id, and a camera id is not a secret.
|
|
func TestAnAgentCannotPushIntoAnotherSitesCamera(t *testing.T) {
|
|
srv, fs := newServer(t)
|
|
fs.addCameraRef(liveCam, "client-1", "site-1", "cam1")
|
|
fs.addAgent("agent-token", AgentPrincipal{ClientID: "client-1", SiteID: "site-2"})
|
|
|
|
rr := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodPost,
|
|
"/api/agent/cameras/"+liveCam+"/live", nil)
|
|
req.Header.Set("Authorization", "Bearer agent-token")
|
|
srv.Routes().ServeHTTP(rr, req)
|
|
|
|
if rr.Code != http.StatusNotFound {
|
|
t.Fatalf("status %d, want 404", rr.Code)
|
|
}
|
|
}
|