Files
Behavision/server/internal/store/api_snapshots.go
Suriyakumarvijayanayagam 18686cbceb 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
2026-09-04 16:46:23 +05:30

133 lines
4.5 KiB
Go

package store
import (
"context"
"errors"
"fmt"
"time"
"github.com/jackc/pgx/v5"
)
// ErrNoSnapshot means this camera has no stored picture. It is an ordinary
// state - a camera added a minute ago has none - so callers report it as
// absence rather than as a failure.
var ErrNoSnapshot = errors.New("no snapshot for this camera")
// PutCameraSnapshot stores the latest frame from one of a site's cameras.
//
// The camera is resolved by (site_id, camera_id) IN THE INSERT, so an agent
// physically cannot store a picture against another site's camera even if it
// sends one - the same rule as every other agent-authenticated write here.
// `camera_id` is what the ENGINE knows the camera by, because that is the only
// name the shop PC has.
func (s *Store) PutCameraSnapshot(ctx context.Context,
clientID, siteID, cameraID string, jpeg []byte) error {
tx, err := s.pool.Begin(ctx)
if err != nil {
return err
}
defer func() { _ = tx.Rollback(context.WithoutCancel(ctx)) }()
var id string
err = tx.QueryRow(ctx, `
SELECT id::text FROM site_cameras
WHERE site_id = $1::uuid AND camera_id = $2 AND deleted_at IS NULL`,
siteID, cameraID).Scan(&id)
if errors.Is(err, pgx.ErrNoRows) {
// Head office has not been told about this camera yet, or it was
// removed. Neither is an error the agent can act on: the next sync
// adopts it and the snapshot after that lands.
return ErrNoSnapshot
}
if err != nil {
return err
}
if _, err := tx.Exec(ctx, `
INSERT INTO camera_snapshots (camera_id, client_id, site_id, image, bytes, captured_at)
VALUES ($1::uuid, $2::uuid, $3::uuid, $4, $5, now())
ON CONFLICT (camera_id) DO UPDATE
SET image = EXCLUDED.image, bytes = EXCLUDED.bytes,
captured_at = EXCLUDED.captured_at`,
id, clientID, siteID, jpeg, len(jpeg)); err != nil {
return fmt.Errorf("store snapshot: %w", err)
}
// snapshot_at is what tells the camera list a picture exists at all, and it
// is written in the SAME transaction as the bytes. Set apart, a camera
// could advertise a picture that is not there - which renders as a broken
// image on the one screen whose job is to show the camera.
if _, err := tx.Exec(ctx, `
UPDATE site_cameras SET snapshot_at = now() WHERE id = $1::uuid`,
id); err != nil {
return err
}
return tx.Commit(ctx)
}
// CameraSnapshot returns a camera's stored picture, scoped to the tenant.
func (s *Store) CameraSnapshot(ctx context.Context, clientID, cameraID string) (
[]byte, time.Time, error) {
var img []byte
var at time.Time
err := s.pool.QueryRow(ctx, `
SELECT image, captured_at FROM camera_snapshots
WHERE camera_id = $1::uuid AND client_id = $2::uuid`,
cameraID, clientID).Scan(&img, &at)
if errors.Is(err, pgx.ErrNoRows) {
return nil, time.Time{}, ErrNoSnapshot
}
return img, at, err
}
// CameraRef resolves one of a tenant's cameras to its site and the name the
// engine knows it by.
//
// Its job is to prove ownership before a live stream starts. Everything after
// that point is keyed on a camera id, and a hub relaying frames does not know
// whose camera it is holding — so this is the only place that can decide.
func (s *Store) CameraRef(ctx context.Context, clientID, cameraID string) (string, string, error) {
return s.cameraRef(ctx, `client_id = $2::uuid`, cameraID, clientID)
}
// CameraRefBySite is the same question asked by an agent, which is
// authenticated for a site rather than a tenant.
func (s *Store) CameraRefBySite(ctx context.Context, siteID, cameraID string) (string, string, error) {
return s.cameraRef(ctx, `site_id = $2::uuid`, cameraID, siteID)
}
func (s *Store) cameraRef(ctx context.Context, scope, cameraID, owner string) (string, string, error) {
var siteID, engineID string
err := s.pool.QueryRow(ctx, `
SELECT site_id::text, camera_id FROM site_cameras
WHERE id = $1::uuid AND `+scope+` AND deleted_at IS NULL`,
cameraID, owner).Scan(&siteID, &engineID)
if errors.Is(err, pgx.ErrNoRows) {
return "", "", ErrNoSnapshot
}
return siteID, engineID, err
}
// SiteCameraIDs lists a site's camera uuids, for the agent's live poll.
func (s *Store) SiteCameraIDs(ctx context.Context, siteID string) ([]string, error) {
rows, err := s.pool.Query(ctx, `
SELECT id::text FROM site_cameras
WHERE site_id = $1::uuid AND deleted_at IS NULL AND enabled`, siteID)
if err != nil {
return nil, err
}
defer rows.Close()
var out []string
for rows.Next() {
var id string
if err := rows.Scan(&id); err != nil {
return nil, err
}
out = append(out, id)
}
return out, rows.Err()
}