Skip to content

Per-app config.yaml

Every app has its own apps/<name>/config.yaml — the versionable source of truth for that tenant’s behavior. It’s parsed at load time into an AppConfig object with S3 credentials already resolved. S3 keys never live in the YAML — only *_env reference names; the real values are read from the environment or data/secrets.json (chmod 600).

The app’s Config tab in the dashboard exposes the common fields (recording toggle, S3, webrtc ladder, transcoding, features) as forms, plus a raw-YAML editor for anything not covered by a form.

name: live
display_name: Live
room_prefix: live
recording:
enabled: true
mode: room-composite # room-composite | participant
layout: grid
local_dir: recordings
delete_local_after_upload: true
s3:
provider: wasabi # aws | wasabi | minio
bucket: my-bucket
region: us-east-1
endpoint: https://s3.us-east-1.wasabisys.com # empty for plain AWS
force_path_style: false # true for minio (path-style)
prefix: streamhub/live
access_key_env: APP_LIVE_S3_KEY # credential REFERENCES, not values
secret_key_env: APP_LIVE_S3_SECRET
webrtc:
adaptive: true
layers:
- { name: high, height: 720 }
- { name: med, height: 480 }
- { name: low, height: 240 }
rtmp:
enabled: true
transcode: true # sub-preference; only acts when transcoding.enabled
# Server-side transcoding (master switch + recording/VOD outputs).
# NEW apps ship with enabled: false -> pure passthrough (no re-encode anywhere).
transcoding:
enabled: false # master switch (opt-in)
encoding: h264 # h264 | h264+vp8 (adds a WebM/VP8 alternate per VOD)
vod_adaptive: false # adaptive HLS VOD (master playlist + renditions)
vod_renditions: [] # explicit ladder; empty = derived from webrtc.layers
callbacks:
url: "" # POST signed events here (empty = disabled)
secret: "" # HMAC-SHA256 signing secret
# Optional wave-2 features. All default to sensible off/safe values.
features:
rtmp_password: true # require a password in addition to the stream key
viewer_counter: true # expose subscriber count per stream
chat: true # data channels: chat + emojis
reactions: true # animated reactions
hidden_qc: true # allow hidden QC/recorder participants
adaptive_player: true # player uses adaptive (simulcast/HLS) playback
ws_ingest: # direct WS MJPEG ingest (ESP32-CAM) — optional block
enabled: true # default true; false disables /ingest/ws for the app
max_cameras: 0 # 0 = unlimited concurrent ws-mjpeg cameras
max_fps: 15 # server-side fps cap per camera (excess dropped)
max_frame_kb: 256 # max accepted JPEG frame size (bigger -> close 4413)
Key Type Default Notes
name string App slug (unique). Matches apps.name.
display_name string name Human label. Editable via PATCH /apps/{name}.
room_prefix string name LiveKit room namespace. Rooms become <prefix> or <prefix>-<room>.
Key Type Default Notes
enabled boolean true Toggle recording for the app (also via recordingEnabled in PATCH /apps/{name}).
mode room-composite | participant room-composite Composite the whole room, or a single participant. Used by POST /recording/start.
layout string grid Egress composite layout, e.g. grid, speaker.
local_dir string recordings Subdir under apps/<name>/ for temp MP4s before upload.
delete_local_after_upload boolean true Delete the local file once the S3 upload succeeds.

Resolved into S3Config (multi-provider via @aws-sdk/client-s3).

Key Type Default Notes
provider aws | wasabi | minio Storage backend.
bucket string Target bucket.
region string e.g. us-east-1.
endpoint string empty Full URL for Wasabi/MinIO; empty for plain AWS.
force_path_style boolean false true for MinIO (path-style addressing).
prefix string Key prefix inside the bucket, e.g. streamhub/live.
access_key_env string Name of the env var holding the access key — not the key itself.
secret_key_env string Name of the env var holding the secret key — not the secret itself.

The adaptive/transcoding ladder, editable via GET/PATCH /apps/{app}/config and read by GET /apps/{app}/transcoding/layers.

Key Type Default Notes
adaptive boolean true Enable adaptive (simulcast) WebRTC delivery.
layers list of { name, height } [{high,720},{med,480},{low,240}] Rendition ladder. name is a short slug; height 1..4320 (width derived from the source aspect ratio). 1..8 entries; a PATCH layers replaces the whole ladder.
Key Type Default Notes
enabled boolean true Allow RTMP ingress for the app.
transcode boolean true Sub-preference for multi-layer transcoding on RTMP/URL ingress. Only takes effect when transcoding.enabled is true — with the master switch off, ingress is always passthrough.

Server-side transcoding master switch plus recording/VOD output targets, editable via PATCH /apps/{app}/config (transcodingEnabled, encoding, vodAdaptive, vodRenditions).

Key Type Default Notes
enabled boolean false Master switch. A new app starts pure passthrough: RTMP ingress isn’t re-encoded and each recording is a single H.264 MP4. Everything below is inert until this is true.
encoding h264 | h264+vp8 h264 Recording output target. h264 = the egress-native MP4 only. h264+vp8 additionally generates a WebM/VP8 (+Opus) alternate per recording via an ffmpeg post-transcode job (LiveKit egress can’t emit VP8 itself).
vod_adaptive boolean false Generate an adaptive HLS VOD per recording: one H.264 rendition per ladder step plus a master .m3u8, uploaded to the app’s S3 and stored as VOD variants (the base MP4 VOD is untouched).
vod_renditions list of { height, bitrate_kbps } [] Explicit VOD ladder. Empty = derived from webrtc.layers heights with default bitrates. Invalid entries are dropped, duplicates deduped, sorted highest-first, capped at 5.

The per-app hwaccel setting (auto | gpu | cpu, see PATCH /apps/{app}/config) chooses whether transcoding uses GPU acceleration when available. auto (default) uses the GPU if the node has one, gpu forces it (falling back to CPU if none), cpu always uses software encoding. GET /system/gpu reports what the current node detected.

Key Type Default Notes
url string "" Outbound webhook URL. Empty = callbacks disabled. Editable via PATCH /apps/{name} (callbackUrl).
secret string "" Shared secret for the X-StreamHub-Signature header (HMAC-SHA256). Editable via callbackSecret.

All optional, per-app, with safe-off defaults.

Key Type Default Effect
rtmp_password boolean false Each RTMP ingress also issues a stream_password; a push is accepted only if the key and password match.
viewer_counter boolean false Per room/stream subscriber count (publishers and hidden/QC excluded), live on GET /apps/{app}/streams/{id} and in events.
chat boolean false Chat widget over the LiveKit chat data-channel topic. Fires the chat_message callback.
reactions boolean false Animated reactions over the reaction data-channel topic. Fires the reaction callback.
hidden_qc boolean false Allows minting hidden QC/recorder tokens (hidden: true, recorder: true) that subscribe to all media but stay invisible and uncounted.
adaptive_player boolean false The associated player uses adaptive playback: simulcast for live, HLS renditions for VOD when available.
ws_ingest object enabled Direct WebSocket MJPEG ingest for ESP32-CAM-class devices (wss://<domain>/ingest/ws). Sub-keys: enabled (bool, default true), max_cameras (int, 0 = unlimited), max_fps (int, default 15 — excess frames dropped server-side), max_frame_kb (int, default 256 — an oversized frame closes the socket with code 4413).
  • The default app live is created at boot from this template.
  • Editing the YAML directly on disk works, but the canonical path is the API (PATCH /apps/{name}, PATCH /apps/{app}/config, or the raw editor above) — it keeps the in-memory config and DB consistent and re-resolves S3 credentials.
  • Errors never crash the process; invalid config falls back to safe defaults where possible and is logged (queryable via GET /logs).