From 86bae816c18f0dd2c6018557caf7c4e14f3c95db Mon Sep 17 00:00:00 2001 From: Zac Gaetano Date: Fri, 17 Apr 2026 09:42:16 -0400 Subject: [PATCH] =?UTF-8?q?docs(m2):=20WebRTC=20into=20Core=20proper=20?= =?UTF-8?q?=E2=80=94=20design=20spec?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit M2 promotes the M1 standalone PoC into the datarhei Core binary so WebRTC becomes a first-class output alongside RTMP/SRT/HLS, surfaced in the core-ui dashboard. Architecture: new app/webrtc sibling subsystem + two small hooks on restream (ProcessHooks + AppendOutput), reusing the untouched M1 core/webrtc package. WHEP served under /api/v3/process/{id}/whep, inheriting JWT auth. A new "Live (WebRTC)" tab on the process detail view provides the embedded browser player. Covers: purpose, architecture diagram, decision table, components, data flow (enable/subscribe/stop/disable/restart), error handling, testing strategy (unit/integration/e2e), acceptance criteria, rollback, and a seven-milestone sanity breakdown. Co-Authored-By: Claude Opus 4.7 --- ...-dragon-fork-m2-webrtc-core-integration.md | 323 ++++++++++++++++++ 1 file changed, 323 insertions(+) create mode 100644 docs/design/2026-04-17-datarhei-dragon-fork-m2-webrtc-core-integration.md diff --git a/docs/design/2026-04-17-datarhei-dragon-fork-m2-webrtc-core-integration.md b/docs/design/2026-04-17-datarhei-dragon-fork-m2-webrtc-core-integration.md new file mode 100644 index 0000000..8b57f37 --- /dev/null +++ b/docs/design/2026-04-17-datarhei-dragon-fork-m2-webrtc-core-integration.md @@ -0,0 +1,323 @@ +# M2 — WebRTC into datarhei Core proper + +**Status:** Design approved, implementation pending +**Date:** 2026-04-17 +**Author:** Zac (zgaetano@wilddragon.net), Dragon Fork +**Depends on:** M1 (`2026-04-16-datarhei-dragon-fork-m1-webrtc-poc.md`) +**Branch:** `m2-webrtc-core-integration` + +## 1. Purpose + +M1 produced a standalone `cmd/webrtc-poc` binary that proved the Pion-based +WHEP egress path end-to-end on TrueNAS. M2 promotes that work into the +datarhei Core binary so WebRTC becomes a first-class output alongside +RTMP, SRT, and HLS, surfaced in the core-ui dashboard. + +After M2 a user can: + +1. Create or edit a process in core-ui. +2. Toggle a "WebRTC" switch on that process's config. +3. Save → Core restarts the process with an extra RTP output leg. +4. Open the process's "Live (WebRTC)" tab and watch the feed in the + browser with sub-second latency, authenticated by the user's JWT. + +Out of scope for M2 (explicit): +- Public / unauthenticated embeds (handled in M3 via signed URLs). +- A separate "broadcast center" dashboard page (per-process tab is enough). +- Lazy / on-demand Source binding — eager binding only. +- WHIP ingest — that's M4. + +## 2. High-level architecture + +``` + ┌────────────────────────────────────────────┐ + │ datarhei Core │ + │ │ + FFmpeg (per │ ┌──────────────┐ ┌──────────────┐ │ + process, │ │ restream │─────▶│ app/webrtc │ │ + spawned by │──▶│ │◀─────│ (NEW) │ │ + restream) ───┐ │ │ - lifecycle │hooks │ │ │ + │ │ │ - AppendOut │ │ - registry │ │ + │ │ │ - config │ │ - sources │ │ + │ │ │ (now incl. │ │ - PeerFactory│ │ + │ │ │ WebRTC) │ │ - WHEP mux │ │ + │ │ └──────────────┘ └──────┬───────┘ │ + │ │ │ │ + udp:// │ │ ┌──────────────┐ │ │ + 127.0.0.1: └─▶│ │ core/webrtc │◀────uses────┘ │ + rtp │ │ (from M1, │ │ + │ │ unchanged) │ ┌────────────────┐ │ + │ └──────────────┘ │ http/server │ │ + │ │ │ │ + │ │ mounts │ │ + │ │ /api/v3/process│ │ + │ │ /:id/whep │ │ + │ └────────┬───────┘ │ + └────────────────────────────────┼───────────┘ + │ + (DTLS-SRTP over ICE) │ + ▼ + Browser (core-ui + player tab, RTCPeer) +``` + +Three boxes matter: + +- **existing `restream`** — grows two tiny hooks. +- **existing `core/webrtc`** (from M1) — unchanged. +- **new `app/webrtc`** — the glue subsystem. + +## 3. Key decisions (settled during brainstorming) + +| # | Decision | Choice | +|---|----------|--------| +| 1 | Scope | Backend + full UI with embedded player | +| 2 | Stream addressing | `/whep/{processID}` — per-process | +| 3 | HTTP listener | Under Core's `/api/v3` group (inherits JWT) | +| 4 | Viewer auth | JWT only in M2 — public embeds are M3 | +| 5 | FFmpeg wiring | Auto-inject UDP RTP output; re-encode when needed | +| 6 | Enable state | Field on `restream.Config.WebRTC` | +| 7 | UI surface | New "Live (WebRTC)" tab on process detail view | +| 8 | Lifecycle | Eager — Source bound when process starts | +| 9 | Code placement | New `app/webrtc` sibling subsystem (not inside restream) | + +## 4. Components + +### 4.1 Config — `config/data.go` + `restream/app/process.go` + +Per-process: + +```go +// restream/app/process.go — new sibling of ConfigIO on Config +type ConfigWebRTC struct { + Enabled bool // master switch for this process + VideoPT uint8 // default 102 (H.264) + AudioPT uint8 // default 111 (Opus) + ForceTranscode bool // default false — true => always re-encode +} +``` + +Global (Core config, one block): + +```go +// config/data.go +type DataWebRTC struct { + Enable bool // master feature flag; default false for safety + PublicIP string // NAT1To1 / ICE host candidate rewrite (e.g. LAN IP) + NAT1To1IPs []string // advanced: multiple public IPs + UDPMuxPort int // optional: single UDP port for all ICE traffic + // (0 = ephemeral per peer, default) +} +``` + +Registered through the existing `vars.Register` mechanism in `config/config.go`. + +### 4.2 New package — `app/webrtc/` + +| File | Responsibility | +|------|----------------| +| `subsystem.go` | `type WebRTC struct` with `Start()` / `Stop()`; owns the `core/webrtc.Registry` and a single `core/webrtc.PeerFactory`. Implements the same shape as other Core subsystems. | +| `lifecycle.go` | `OnProcessStart(id, cfg)` / `OnProcessStop(id)` callbacks registered with restream. Allocates a UDP port, calls `restream.AppendOutput`, binds a `core/webrtc.Source`, registers it. | +| `portalloc.go` | `Alloc() (int, error)` — binds `:0` on loopback, reads the port, closes the listener, returns the number. Race window is microseconds; `NewSourceOn` re-binds immediately. If the rebind fails (rare: another process grabbed the port in the gap), `OnStart` returns the error, restream aborts the start, operator retries. Tested with 100× tight-loop. | +| `ffmpeg_args.go` | `BuildArgs(cfg ConfigWebRTC, port int) []string` — emits the `-map`, `-c:v`, `-c:a`, `-f rtp`, `udp://127.0.0.1:PORT?pkt_size=1316` fragments. Branches on `ForceTranscode`. | +| `handler.go` | HTTP handler for WHEP — wraps the M1 `core/webrtc.NewWHEPHandler`, but looks up the Source by `processID` path param. Adds `DELETE /api/v3/process/:id/whep/:peerid`. | + +### 4.3 Two additions to `restream` + +1. **Lifecycle callback pair.** Added as fields on the restream manager: + + ```go + type ProcessHook func(id string, cfg *app.Config) error + type ProcessHooks struct { + OnStart ProcessHook // fires after args are assembled, before exec + OnStop ProcessHook // fires after wait() returns + } + ``` + + Single consumer is fine — no event bus yet. `app/webrtc` registers itself at subsystem start. + +2. **`AppendOutput(id string, extra []string) error`** — mutates the *pending* + FFmpeg args for a process that has fired `OnStart` but has not yet exec'd. + Inside `OnStart`, the subsystem calls `AppendOutput` to add the + `-f rtp udp://…` fragment; restream then exec's with the augmented + args. Outside the `OnStart` window `AppendOutput` returns an error — + Core does not mutate running FFmpeg processes. + +These two additions are useful beyond WebRTC (stats consumers, future +sidecar modules), so the surface cost is justified. + +### 4.4 One route in `http/server.go` + +Inside the existing `/api/v3` group (inherits JWT auth): + +```go +api.POST("/process/:id/whep", webrtcHandler.Subscribe) +api.DELETE("/process/:id/whep/:peerid", webrtcHandler.Unsubscribe) +``` + +### 4.5 UI — `core-ui/src/views/Edit/LiveTab.jsx` (new) + +- Shown only when `process.config.webrtc.enabled === true`. +- `