Per-app config.yaml
This content is for the 1.0 version. Switch to the latest version for up-to-date documentation.
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).
Editing it
Section titled “Editing it”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.
Two endpoint families cover it:
- Global shape —
PATCH /apps/{name}:displayName,roomPrefix,recordingEnabled,callbackUrl,callbackSecret,splitMinutes,snapshotSeconds. - Transcoding shape —
GET/PATCH /apps/{app}/config:adaptive,layers,rtmpTranscode,hwaccel,features. - Presets —
GET /apps/{app}/presets+POST /apps/{app}/presets/{name}/applyapply a whole delivery/quality profile (low-latency|high-quality-recording|mass-audience-HLS) as a credential-safe deep-merge, then hot-reload.
curl -s -X PATCH $BASE/apps/demo -H "Authorization: Bearer $TOKEN" \ -H 'Content-Type: application/json' -d '{"splitMinutes":30,"snapshotSeconds":60}'
curl -s -X PATCH $BASE/apps/demo/config -H "Authorization: Bearer $TOKEN" \ -H 'Content-Type: application/json' \ -d '{"hwaccel":"gpu","layers":[{"name":"high","height":720},{"name":"low","height":360}]}'GET/PUT /apps/:app/config/raw edits the file verbatim, with safety rails: a dry-run
validate-with-diff, timestamped backups, revert-to-backup, and a hot-reload that re-reads the
config and re-inits the app’s S3 client — no process restart, so other apps’ streams keep
running.
# dry-run a change (validate + diff, no write)curl -s -X POST $BASE/apps/demo/config/raw/validate -H "Authorization: Bearer $TOKEN" \ -H 'Content-Type: application/json' -d '{"yaml":"name: demo\nroom_prefix: demo\n..."}'
# write + hot-reloadcurl -s -X PUT $BASE/apps/demo/config/raw -H "Authorization: Bearer $TOKEN" \ -H 'Content-Type: application/json' -d '{"yaml":"name: demo\nroom_prefix: demo\n..."}'
# list / revert to a backupcurl -s $BASE/apps/demo/config/backups -H "Authorization: Bearer $TOKEN"curl -s -X POST $BASE/apps/demo/config/backups/20260630T120000Z/revert -H "Authorization: Bearer $TOKEN"A parse or shape error on PUT returns 400 with the detail and writes nothing. On success
the current file is backed up to config.yaml.bak.<ts> before the new one is written. A
heavier POST /admin/restart (global-scope token only) restarts the whole core process via
systemd — only needed for changes that aren’t hot-reloadable.
Full example
Section titled “Full example”name: livedisplay_name: Liveroom_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)Field reference
Section titled “Field reference”Top-level
Section titled “Top-level”| 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>. |
recording
Section titled “recording”| 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 |
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 |
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. |
webrtc
Section titled “webrtc”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. |
transcoding
Section titled “transcoding”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 |
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.
callbacks
Section titled “callbacks”| 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. |
features
Section titled “features”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
liveis 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).