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.
This commit is contained in:
Ezequiel C. 2026-09-02 21:18:25 +02:00
parent bb754cdd8c
commit 187379de4e
106 changed files with 21391 additions and 286 deletions

View file

@ -0,0 +1,234 @@
# 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.