repo/services/multistreaming/README.md
Ezequiel C. 187379de4e multistreaming: scenes/composition, installer build support, service updates
- multistreaming (new): RTMP ingest + multi-platform fan-out with pluggable providers (Twitch/YouTube/Kick/custom), zero-knowledge key vaults, Authelia OIDC auth, shared rooms with editor/streamer roles, single-use invites, per-account streaming grants, and scenes & composition (grid/PiP layouts, text/image overlays, per-output audio routing).
- installer: support Dockerfile build in metadata (not just image) and RSA key generation for the Authelia OIDC JWKS.
- authelia: add OIDC provider with portainer + multistreaming clients (public + PKCE).
- services: remove allprox; add nginx-proxy-manager and portainer; update lldap; regenerate catalog.
2026-09-02 21:18:25 +02:00

234 lines
9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# multistreaming
Self-hosted live production dashboard: ingest RTMP feeds (one per person from
OBS) and restream them to one or more streaming platforms — with shared rooms,
role-based collaboration, a zero-knowledge key vault, and scenes & composition
(grid/PiP layouts, text/image overlays, per-output audio routing).
- **Panel**: React + TypeScript + shadcn/ui (Tailwind v4) at
`http://<host>:8080`.
- **Ingest**: RTMP at `rtmp://<host>:1935/live/<streamKey>`.
- **Fan-out**: FFmpeg, `-c copy` remux by default; re-encode only for composed
scenes.
---
## 1. Features
- **Per-person feeds** — every streamer publishes to their own ingest URL with a
unique stream key.
- **Rooms & roles** — share a production with `owner` (full control), `editor`
(edits production, never sees keys), and `streamer` (own accounts + start
streaming) roles via single-use invite links.
- **Pluggable providers** — Twitch, YouTube, Kick, and a custom RTMP escape
hatch, extensible in one file.
- **Zero-knowledge vault** — stream keys are encrypted in the browser
(AES-GCM under a PBKDF2-wrapped vault key); the server stores only ciphertext
and grants keys transiently, in memory, when a streamer clicks *Start
streaming*.
- **Scenes & composition** — named scenes with grid or picture-in-picture
layouts, text and image overlays, and per-output audio routing. Activating a
scene switches the room from passthrough to a composed program.
---
## 2. Architecture
```
OBS ──RTMP──▶ node-media-server (ingest, :1935)
│ publish/unpublish events
index.js ── reconcileAll()
┌───────────┴────────────┐
▼ ▼
passthrough scene (composed)
fanout.js (engine) compose.js → fanout.js
ffmpeg -c copy ffmpeg -filter_complex (xstack/overlay/drawtext)
│ │
└───────────┬────────────┘
platform push URLs (Twitch/YouTube/Kick/custom, resolved from in-memory grants)
```
| Component | Path | Role |
|-----------|------|------|
| `src/index.js` | orchestrator | wires config/store/fanout/server; tracks live feeds; `reconcileAll()` |
| `src/server.js` | REST API | Express routes, role gating, `/api/state` |
| `src/store.js` | persistence | atomic JSON store for users/vaults/rooms/memberships/invites/accounts/feeds/outputs/scenes/sceneOutputs/images |
| `src/fanout.js` | process engine | generic unit-of-work engine: spawn/kill/restart/reconcile FFmpeg children |
| `src/compose.js` | composition | builds FFmpeg filter graphs + argv for scenes |
| `src/vault.js` | crypto | zero-knowledge vault enroll/unlock/recover (WebCrypto) |
| `src/grants.js` | grants | in-memory, TTL-bounded plaintext key grants |
| `src/auth.js` / `src/oidc.js` | auth | sessions + local scrypt / OIDC PKCE |
| `src/providers.js` | providers | pluggable platform definitions |
| `src/config.js` | config | environment → runtime config |
| `web/src/` | frontend | React/shadcn panel |
---
## 3. Directory layout
```
multistreaming/
├── src/ # CommonJS backend
├── web/ # React + TS + shadcn frontend (src/, built to web/dist)
├── docs/SCENES.md # scene & composition design contract
├── Dockerfile # multi-stage: web build → backend deps → runtime (node + ffmpeg)
├── metadata.json # catalog metadata (version, compose, env)
└── package.json
```
---
## 4. Configuration
Environment variables (see also `metadata.json` for the UI descriptions):
| Variable | Default | Purpose |
|----------|---------|---------|
| `AUTH_MODE` | `oidc` | `oidc` (Authelia) or `local` (dev) |
| `OIDC_ISSUER` | — | Authelia root URL (OIDC discovery) |
| `OIDC_CLIENT_ID` | `multistreaming` | OIDC client id |
| `OIDC_REDIRECT_URI` | — | must match Authelia redirect_uris |
| `SESSION_SECRET` | auto | HMAC secret for session cookies (set it to a stable random value) |
| `PUBLIC_HOST` | `''` | host shown in copy-paste ingest URLs |
| `GRANT_TTL_MS` | `21600000` | plaintext-key grant lifetime (6 h) |
| `RTMP_PORT` | `1935` | RTMP ingest port |
| `HTTP_PORT` | `8080` | panel + API port |
| `DATA_DIR` | `./data` | persisted state + `uploads/` |
| `FFMPEG_PATH` | `ffmpeg` | ffmpeg binary |
| `SCENE_WIDTH` / `SCENE_HEIGHT` | `1920` / `1080` | composed canvas |
| `SCENE_FPS` | `30` | composed frame rate |
| `SCENE_VIDEO_BITRATE` | `4500k` | composed video bitrate |
| `SCENE_AUDIO_BITRATE` | `160k` | composed audio bitrate |
| `SCENE_FONT` | DejaVu Sans | drawtext font path (installed in the image) |
| `SCENE_FONT_BOLD` | DejaVu Sans Bold | bold drawtext font path |
| `SCENE_MAX_IMAGE_BYTES` | `1048576` | image overlay upload cap (1 MiB) |
---
## 5. Running
Build and run with the repository's catalog tooling (metadata.json provides the
compose service) or directly:
```bash
docker build -t multistreaming .
docker run -p 1935:1935 -p 8080:8080 \
-v multistreaming_data:/data \
-e AUTH_MODE=oidc \
-e OIDC_ISSUER=https://auth.example.com \
-e OIDC_REDIRECT_URI=https://streaming.example.com/api/auth/oidc/callback \
-e SESSION_SECRET=\
multistreaming
```
- Panel: `http://<host>:8080` (put it behind nginx-proxy-manager for TLS).
- Ingest: `rtmp://<host>:1935/live/<streamKey>` — RTMP bypasses the HTTP proxy,
so keep port 1935 on a trusted network.
---
## 6. Ingest URL model
Each **feed** has a unique `streamKey`. Point OBS at:
```
rtmp://<PUBLIC_HOST>:1935/live/<streamKey>
```
(`PUBLIC_HOST` only affects the copy-paste URL shown in the UI; ingest always
hits the container's port 1935.) The `live` app name is fixed (`ingestApp`).
When a feed publishes, the orchestrator fans it out to its enabled outputs (or,
if a scene is active, feeds the composed program).
---
## 7. Providers
Providers are defined in `src/providers.js`. Each extends the `Provider` base
class and implements:
- `fields` — the fields the UI asks the user for (`name`, `label`, `secret?`,
`required?`, `placeholder?`, `help?`),
- `defaultUrl` — the platform's RTMP server URL,
- `pushUrl(config)` — how the key is appended (default: `url + "/" + key`),
- `describe(config)` / `validate(config)` — copy-safe summary + validation.
To add a provider, add a derived class and register it in `PROVIDERS`:
```js
class NewPlatformProvider extends Provider {
constructor() {
super({
id: "newplatform",
name: "New Platform",
defaultUrl: "rtmp://ingest.newplatform.example/live",
fields: [{ name: "key", label: "Stream key", secret: true, required: true }],
});
}
}
// …then push `new NewPlatformProvider()` into PROVIDERS.
```
The `custom` provider is a free-form RTMP endpoint for anything not explicitly
supported.
---
## 8. Scenes & composition
A **scene** is a named composition for a room (see `docs/SCENES.md` for the full
contract):
- **Layouts** — `grid` (24 columns, up to 6 slots) or `pip` (main + a
picture-in-picture window). Each slot assigns a feed (or stays empty). Only
*live* feeds are laid out; empty/offline slots are skipped and a single live
feed is promoted to fullscreen.
- **Overlays** — text (position, size, color, bold) and images (position +
size, ≤ 1 MiB, PNG/JPEG/WebP).
- **Audio routing** — each scene **output** (destination) chooses `program`
(mixed audio of all live feeds), `silent`, or a specific `feed`'s audio.
- **Activation** — activating a scene switches the room into composed mode
(per-feed passthrough pauses); deactivating restores passthrough.
Composed video is re-encoded (H.264 + AAC) per destination so each output can
carry different audio. The FFmpeg filter graph is generated entirely by
`src/compose.js`; no scene data ever stores a stream key.
---
## 9. Security
See [SECURITY.md](./SECURITY.md) for the full threat model. Highlights:
- Stream keys are encrypted in the browser; the server persists only
`secretCiphertext` + `serverWrapped`.
- Plaintext keys exist server-side only as short-lived, in-memory grants.
- FFmpeg logs redact destination URLs; overlay text cannot inject FFmpeg
arguments; image uploads are size/type-limited.
- Roles and `maskAccount` ensure editors/streamers never see others' keys.
The honest limit: the machine that pushes a stream must hold the key at push
time — if the host is compromised during a stream, live keys are readable.
---
## 10. Development
No runtime tests are run by design; verification is static.
```bash
# backend syntax check
node --check src/index.js && node --check src/fanout.js && \
node --check src/server.js && node --check src/store.js && \
node --check src/compose.js && node --check src/config.js
# frontend typecheck + build
cd web && npm install && npm run build
```
`npm run build` runs `tsc -b && vite build` and emits `web/dist`, which the
backend serves at `/`. A `npm run dev` Vite server may be used for manual web
inspection during development.