R2-D2
Dashboard
Node Red
Restreaming
The Force
UpdatedNever
v1.0.0

Overview

Loading…
R2-D2
Dashboard
Node Red
Restreaming
The Force
UpdatedNever
v1.0.0
DDroidspeak / Docs
Operator handbook
Droidspeak
What is R2-D2?
Runtime architectureAuth — Keycloak migration plan
FleetRestreamingGalaxy MapThe RepublicTemple ArchivesSTANAG 4817The Force
Tech StackNext.js 15 + React 19Tailwind v4ZustandTanStack Table + DataViewhls.jsLow-latency playerreagraphoglreact-grid-layoutMonacoScalar API Referencefumadocs
Cluster InfraTrailBaseReductStoreRestreamer (datarhei/core)TBMQKeycloakLonghornkube-vipIngress (Caddy + nginx-ingress + Traefik)Netbird
Operator QA Runbook
Install — RKE2Install — Dokploy
Contributor guideRelease Notes
Tech Stack

hls.js

The current HLS playback engine — latency profile, error recovery, snapshot poster fallback.

hls.js (^1.6.16) is the default video playback engine in the dashboard. Every video tile — HoloVids grid, Holoprojector panel, public viewer at /p/<token> — uses hls.js unless the operator explicitly opts into the low-latency engine (which is still in-flight; see Low-latency player).

The upstream source is always the same shape: a .m3u8 URL served by the Restreamer (datarhei/core) at RESTREAMER_API_URL/<feed-id>/index.m3u8. The dashboard never generates manifests itself.

Why hls.js (and not native HLS)

Safari ships native HLS via <video src="...m3u8">. We don't rely on it because:

  • The HUD overlay needs the same code path on every browser, and Safari's native player won't surface granular buffer/error events to a React layer in a uniform way.
  • Snapshot-as-poster fallback requires manifest-load and fragment-load hooks; native HLS exposes a much sparser event set.
  • Telemetry — first-frame, rebuffer count, dropped frames — has to land in a cross-browser path so the planned 3-second first-frame watchdog (Phase 2 of the low-latency player) works the same everywhere.

The single exception: when Hls.isSupported() returns false (older iOS WebViews), we fall back to native HLS via <video src> with no HUD instrumentation.

Where it lives in src/

FileRole
src/components/restreaming/PanelViewer.tsxThe base player — initializes Hls, attaches to the <video>, owns the snapshot-poster fallback
src/components/restreaming/PanelViewerWithHistory.tsxSame player + the snapshot-history strip below the video
src/components/restreaming/SnapshotHistoryTab.tsxSide panel that browses ReductStore snapshots, jumps the player to the matching timestamp
src/components/restreaming/HoloVidsAssignmentEditor.tsxLayout-time picker — uses a smaller PanelViewer to preview each candidate feed
src/lib/restreamer.tsBuilds the .m3u8 URL for a given feed (centralized so RESTREAMER_API_URL is read once)

Player config

The hls.js instance is configured for low-latency-leaning behavior, accepting some fragility in exchange for lower glass-to-glass delay:

// effective config (paraphrased — see PanelViewer.tsx for the canonical form)
new Hls({
  lowLatencyMode: true,        // honor #EXT-X-PART tags if upstream emits them
  liveSyncDuration: 3,         // target 3s behind live edge
  liveMaxLatencyDuration: 8,   // bail to seek-to-live if we drift past 8s
  maxBufferLength: 6,          // keep 6s of forward buffer
  backBufferLength: 0,         // no back-buffer — we never seek backward on live
  enableWorker: true,          // demux/transmux in a Web Worker
  capLevelToPlayerSize: true,  // never pull a rendition larger than the tile
});

The Restreamer's HLS output is single-rendition for now (no ABR ladder), so capLevelToPlayerSize is a future-proofing flag — it does nothing until a multi-bitrate output is configured upstream.

Snapshot-poster fallback

The dashboard never shows a black video tile. The flow:

flowchart LR
  A[Mount tile] --> B[Render last snapshot as poster]
  B --> C[Hls.loadSource]
  C --> D{Manifest OK?}
  D -- yes --> E[First fragment plays, poster fades]
  D -- no, error --> F[Keep poster, schedule retry]
  F --> G{Retry budget left?}
  G -- yes --> C
  G -- no --> H[Mark tile degraded]

The poster image is the most recent snapshot for the feed, fetched from /api/restreaming/snapshots/latest?feedId=<id>. If no snapshot exists, the tile shows a static placeholder.

Error recovery

hls.js classifies errors as NETWORK_ERROR, MEDIA_ERROR, or OTHER_ERROR. The dashboard handles the first two and surfaces the third:

Error classHandler
NETWORK_ERROR (manifest 404, fragment 404, CORS)Backoff retry up to 5 attempts, then mark degraded
MEDIA_ERROR (decode failure, codec switch)Call hls.recoverMediaError() once; if it recurs, destroy + recreate
OTHER_ERRORLog, surface in the HUD's status pill, don't auto-retry

The retry budget resets when a fragment successfully plays. This means a feed that disconnects every 30 seconds (e.g. a flaky cellular backhaul) will keep retrying forever — by design; the operator sees a flapping "reconnecting" pill instead of a permanently-dead tile.

Visibility-gated tear-down

Tiles that are not visible (off-screen scroll, hidden behind a tab, page hidden via document.visibilityState) destroy their Hls instance. They re-create on re-visibility. This is enforced by an IntersectionObserver wrapping each tile.

Why: 16 simultaneous HLS workers on a 4×4 holoprojector grid pegged worker1's CPU. After visibility gating, only the visible tiles pay the cost.

The destroy/re-create cycle has a ~500ms cold-start cost. Operators who scroll rapidly through the HoloVids grid will see the tile "blink" as it tears down and rebuilds. This is acceptable — the alternative is a hot grid that melts the host.

What hls.js does not do

  • Not used for the snapshot history scrub. That's a sequence of static JPEGs pulled from ReductStore, not an HLS playlist.
  • Not used for the public group viewer's preview tiles. Those use the poster image only; the player only activates when a tile is expanded.
  • Not used for KLV telemetry. hls.js does not expose private MPEG-TS PIDs; KLV demux is the job of the low-latency player.

Extension points

  • Adding a new tile surface — import PanelViewer and pass it a feed ID; do not reach for <video> + new Hls() directly.
  • Changing latency profile — edit the config block in PanelViewer.tsx. Do not set per-tile overrides via props; latency tuning is global.
  • Adding a telemetry hook — subscribe to Hls.Events.FRAG_LOADED / Hls.Events.ERROR in PanelViewer.tsx and emit through the existing notify.ts channel.

See also

  • Low-latency player — opt-in sub-second engine for the public viewer
  • Restreaming — the plane that owns feeds and panels
  • QA Runbook — operator-side ffprobe/gstreamer commands for diagnosing feed issues

TanStack Table + DataView

The only sanctioned way to render a list. Never hand-roll a <table>.

Low-latency player

In-flight custom-chrome wrapper + @ffmpeg/ffmpeg engine for sub-second latency, with HUD + KLV demux.

On this page

Why hls.js (and not native HLS)Where it lives in src/Player configSnapshot-poster fallbackError recoveryVisibility-gated tear-downWhat hls.js does not doExtension pointsSee also