# 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`. - `