Restreaming
The video plane — Restreamer feeds, HoloVids, Holoprojector layouts/panels/groups, and Magic URLs.
The Restreaming plane is the most complex slice of the dashboard. It has three layers:
| Layer | Where it shows up | What it owns |
|---|---|---|
| Inputs / management | /restreaming/holovids | Camera feed sources, per-feed configuration, snapshots |
| Organization | /restreaming/layouts | Layouts — geometry only: cols × rows + tile rectangles, no feed bindings |
| Projection | /restreaming/holoprojector + /p/<token> | Views — a Layout (FK) plus per-slot camera_id bindings, optional public token |
The vocabulary is fixed and the slicing is load-bearing — read it twice. The same word ("panel", "group") shows up in both this UI and the upstream Restreamer; here it means our row in TrailBase, not theirs.
For field checks, stream probes, and network triage commands, use the Operator QA Runbook. That page is the operator-first path for FFmpeg, GStreamer, ping, nc, and nmap diagnostics.
The upstream
R2-D2 talks to a single datarhei/core container —
the "Restreamer" — at a fixed in-cluster URL (RESTREAMER_API_URL). Restreamer owns
only the live FFmpeg processes — RTSP/RTMP inputs, transcoding, HLS/RTMP outputs.
Everything else (layouts, panels, groups, magic URLs, feed metadata) is the dashboard's
responsibility and is persisted in TrailBase.
The dashboard's /api/restreaming/* routes are a typed wrapper over Restreamer's
upstream /api/v3/* surface.
Read-before-write is non-negotiable. Every mutation fetches the current process config, diffs in-memory, and PUTs the merged result. Without this, we'd corrupt processes mid-flight when two operators edit overlapping fields. The audit log is the second line of defence — see Known constraints at the end of this page.
Feeds — the inputs
A feed is one running FFmpeg process inside Restreamer. The dashboard's primary read
route is GET /api/restreaming/feeds, which returns enriched feeds — the upstream
process state joined with TrailBase metadata (alias, platform, tags, notes).
Feed lifecycle routes:
| Route | Purpose |
|---|---|
GET /api/restreaming/feeds | List enriched feeds |
POST /api/restreaming/feeds | Create a new feed (input + output config) |
GET /api/restreaming/feeds/{id} | Read one |
PUT /api/restreaming/feeds/{id} | Update (input + output) |
DELETE /api/restreaming/feeds/{id} | Remove |
POST /api/restreaming/feeds/{id}/command | start | stop | restart | reload |
POST /api/restreaming/feeds/bulk | Bulk create / delete / command, with optional dryRun |
POST /api/restreaming/feeds/import | Import a YAML stack, with dryRun + skipExisting |
The bulk and import routes are the workhorses for operators rolling out 20 cameras at
once. Every bulk call returns a per-item result ({ id, ok, error? }) — the toolbar
surfaces that as a status panel; it never claims success on partial failure.
Feed metadata
/api/restreaming/feed-meta stores the human layer on top of the raw upstream feed:
alias— what the operator calls this camera (vs. its IP-encoded ID)platform_id— FK into the platforms table (e.g. "M300 #14", "SeaFLIR-02")tags— string arraynotes— free-form
This data is persisted in TrailBase and joined on every list read. The upstream feed ID is the join key — if someone re-creates the feed with the same reference, the metadata sticks.
Tags and platforms
Two small auxiliary tables, both in TrailBase: /api/restreaming/tags and
/api/restreaming/platforms. CRUD is intentionally minimal — there is no UI for
deletion yet; tags are GC'd lazily.
HoloVids — the feed grid
The page at /restreaming/holovids is a grid of one tile per feed. The header tile
shows feed status; the body is an HLS player (via hls.js). Snapshots
(/api/restreaming/snapshots) populate the placeholder image when a feed is offline,
so the grid never goes to a black square.
Snapshots are ingested by a periodic job (currently triggered by the dashboard itself on
focus) that grabs a single JPEG per feed and POSTs to /api/restreaming/snapshots. The
read route returns a data URI per feed.
Layouts — geometry only
A layout is a cols × rows grid with a list of tile rectangles. No feed bindings.
That's by design: a layout is reusable — the same 2×2 left-rail, big-center, two-bottom
arrangement might be reused for ten different combinations of feeds.
Layouts live in TrailBase (restreamer_layouts). The Settings → Layout Management page
is the canonical CRUD surface, but the routes (/api/restreaming/panels etc.) accept
layout snapshots inline.
Panels — a layout + bindings + identity
A panel is a saved combination: layout (by reference) + per-slot camera bindings + aliases + a name. It's the first thing that can be projected on the Holoprojector page.
/api/restreaming/panels is full CRUD. /api/restreaming/panels/{id}/publish mints
(POST) or revokes (DELETE) a public token; while the token is alive,
/p/<token> renders the panel without any auth — the "Magic URL" for kiosk/external
display.
Groups — a curated set of panels
A group is an ordered list of panel IDs with a name. Groups have the same publish
mechanism — /api/restreaming/groups/{id}/publish mints a public token, and
/api/restreaming/groups/public/{token} returns the member panels for the public
viewer.
The public route is the only one in this plane that bypasses auth. It does not leak the
token in any response field (no token oracle), it returns minimum data (no upstream feed
URLs — only the layout + bindings needed to render), and it carries
X-Robots-Tag: noindex, nofollow.
Audit log
Every mutating call writes one line to data/restreamer-audit.jsonl:
{"ts":1716...,"user":"alice","action":"feed.update","feedId":"cam-14","before":{...},"after":{...},"ok":true}/api/restreaming/audit returns recent entries. The Settings → Restreaming page shows
the last 20 in a "Recent activity" panel and supports "undo last N" rollbacks (which
themselves go through the same routes, so they also audit).
Known constraints (do not break)
- Read-before-write on every mutation. Non-negotiable. Without it we'd corrupt processes mid-flight.
- No partial bulk silent failures. Bulk routes always return per-item results.
- Audit is mandatory. Every POST/PUT/DELETE writes an audit line. No exceptions.
- Live config is source of truth, not localStorage. localStorage is display-only.
Always reconcile against
GET /api/v3/processafter a mutation. - Validation in Next, not in the browser. Route validates ProcessConfig shape, address scheme, port range, reference uniqueness — so a malformed paste from the bulk wizard doesn't reach Restreamer.
- One bulk job at a time per browser tab. Client-side mutex; bulk-delete is disabled while bulk-restart is in flight.
- No
force-dynamicmagic on mutation routes. Plain JSON in, plain JSON out, no streaming, no SSE. - Auth-gated. When
AUTH_ENABLED=true, mutation routes require a TrailBase session cookie. Read routes stay public-within-cluster. Magic URLs bypass auth by design.
Built on
| Layer | Tech | Page |
|---|---|---|
| Default video engine | hls.js | hls.js |
| Opt-in low-latency engine | Custom-chrome wrapper + @ffmpeg/ffmpeg (in-flight) | Low-latency player |
| Layout editor grid | react-grid-layout | react-grid-layout |
| Feed picker / panels list | @tanstack/react-table via DataView | TanStack Table |
| Upstream live processes | Restreamer (datarhei/core) | Restreamer |
| Metadata / panels / groups | TrailBase tables | TrailBase |
| Snapshot history | ReductStore (dedicated cluster) | ReductStore |
| Auth gate | Keycloak session via jose-verified JWT + TrailBase cookie | Keycloak |
| HUD state slice | src/lib/playerHud.ts, src/lib/hudVariants.ts | Low-latency player |