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

9 KiB
Raw Permalink Blame History

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:

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:

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):

  • Layoutsgrid (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 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.

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