- 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.
234 lines
9 KiB
Markdown
234 lines
9 KiB
Markdown
# 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` (2–4 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.
|