Skip to content

Configuration Guide

Complete reference for the bilbycast-edge JSON configuration file. This guide covers every field, validation rule, and common configuration patterns.



bilbycast-edge reads its configuration from two JSON files:

  • config.json — Operational configuration (specified by --config, default: ./config.json). Contains server settings, flow definitions (including user-configured parameters like SRT passphrases, RTSP credentials, RTMP stream keys, bearer tokens, HLS auth tokens), and tunnel routing.
  • secrets.json — Infrastructure credentials (auto-derived: same directory as config.json). Contains manager auth secrets, tunnel encryption keys, API auth config (JWT secret, client credentials), TLS cert/key paths. Written with 0600 permissions on Unix.

If neither file exists at startup, an empty default configuration is used. Both files are loaded and merged into a single in-memory config, then validated at startup. Changes made through the API or manager commands are automatically persisted — flow configs and operational fields to config.json, infrastructure secrets to secrets.json — using atomic writes (write to temp file, then rename).

Migration: If upgrading from a version that used a single config.json with secrets, the node automatically splits them on first startup.


{
"version": 1,
"device_name": "Studio-A Encoder",
"setup_enabled": true,
"server": {
"listen_addr": "0.0.0.0",
"listen_port": 8080,
"tls": {
"cert_path": "/etc/bilbycast/cert.pem",
"key_path": "/etc/bilbycast/key.pem"
},
"auth": {
"enabled": true,
"jwt_secret": "a-cryptographically-random-string-of-at-least-32-characters",
"token_lifetime_secs": 3600,
"public_metrics": true,
"clients": [
{
"client_id": "admin",
"client_secret": "admin-secret-here",
"role": "admin"
},
{
"client_id": "grafana",
"client_secret": "grafana-secret-here",
"role": "monitor"
}
]
}
},
"monitor": {
"listen_addr": "0.0.0.0",
"listen_port": 9090
},
"flows": [
{
"id": "main-feed",
"name": "Main Program Feed",
"enabled": true,
"input": {
"type": "rtp",
"bind_addr": "239.1.1.1:5000",
"interface_addr": "192.168.1.100",
"fec_decode": {
"columns": 10,
"rows": 10
},
"allowed_sources": ["10.0.0.1", "10.0.0.2"],
"allowed_payload_types": [33],
"max_bitrate_mbps": 100.0,
"tr07_mode": true
},
"outputs": [
{
"type": "rtp",
"id": "rtp-local",
"name": "Local Playout",
"dest_addr": "192.168.1.50:5004",
"interface_addr": "192.168.1.100",
"fec_encode": {
"columns": 10,
"rows": 10
},
"dscp": 46
},
{
"type": "srt",
"id": "srt-remote",
"name": "Remote Site via SRT",
"mode": "caller",
"local_addr": "0.0.0.0:0",
"remote_addr": "203.0.113.10:9000",
"latency_ms": 500,
"passphrase": "my-encryption-passphrase",
"aes_key_len": 32
},
{
"type": "rtmp",
"id": "twitch-out",
"name": "Twitch Stream",
"dest_url": "rtmp://live.twitch.tv/app",
"stream_key": "live_123456789_abcdefghijklmnop",
"reconnect_delay_secs": 5,
"max_reconnect_attempts": 10
}
]
}
]
}

FieldTypeRequiredDefaultDescription
versionintegerYes-Schema version. Currently must be 1.
node_idstringNoAuto-generatedPersistent UUID v4 identifying this edge node. Auto-generated on first startup and saved to config. Used as the NMOS IS-04 Node ID.
device_namestringNonullOptional human-readable label for this edge node (e.g. “Studio-A Encoder”). Max 256 characters.
setup_enabledbooleanNotrueWhen true, the browser-based setup wizard is accessible at /setup. Set to false to disable after provisioning.
serverobjectYes-API server configuration.
monitorobjectNonullWeb monitoring dashboard configuration.
managerobjectNonullManager WebSocket connection configuration. See Manager Configuration.
flowsarrayNo[]List of flow configurations. See Flow Configuration.
tunnelsarrayNo[]List of IP tunnel configurations. See Tunnel Configuration.

The server object controls the API server listener.

{
"server": {
"listen_addr": "0.0.0.0",
"listen_port": 8080,
"tls": { ... },
"auth": { ... }
}
}
FieldTypeRequiredDefaultDescription
listen_addrstringYes"0.0.0.0"IP address to bind the API server to. Use "0.0.0.0" for all interfaces or a specific IP.
listen_portintegerYes8080TCP port for the API server.
tlsobjectNonullTLS configuration for HTTPS (tls feature enabled by default).
authobjectNonullOAuth 2.0 / JWT authentication configuration. When absent or enabled: false, all endpoints are open.

Optional sub-object of server. The tls feature is enabled by default.

{
"tls": {
"cert_path": "/etc/bilbycast/cert.pem",
"key_path": "/etc/bilbycast/key.pem"
}
}
FieldTypeRequiredDescription
cert_pathstringYesPath to PEM-encoded TLS certificate file (or fullchain). Cannot be empty.
key_pathstringYesPath to PEM-encoded TLS private key file. Cannot be empty.

If TLS is configured but the binary was built without the tls feature, a warning is logged and the server starts without TLS.


Optional sub-object of server. See the Security Guide for detailed usage.

{
"auth": {
"enabled": true,
"jwt_secret": "at-least-32-characters-of-random-data",
"token_lifetime_secs": 3600,
"public_metrics": true,
"clients": [
{
"client_id": "admin",
"client_secret": "strong-secret",
"role": "admin"
}
]
}
}
FieldTypeRequiredDefaultDescription
enabledbooleanYes-Master switch. When false, all endpoints are open.
jwt_secretstringYes (if enabled)-HMAC-SHA256 signing secret. Must be >= 32 characters.
token_lifetime_secsintegerNo3600JWT token lifetime in seconds.
public_metricsbooleanNotrueWhether /metrics and /health are accessible without auth.
clientsarrayYes (if enabled)-Registered OAuth clients. At least one required.

Client fields:

FieldTypeRequiredDescription
client_idstringYesUnique client identifier. Cannot be empty.
client_secretstringYesClient authentication secret. Cannot be empty.
rolestringYesMust be "admin" or "monitor".

Optional top-level object. When present, bilbycast-edge starts a second HTTP server serving a self-contained HTML monitoring dashboard.

{
"monitor": {
"listen_addr": "0.0.0.0",
"listen_port": 9090
}
}
FieldTypeRequiredDescription
listen_addrstringYesIP address for the dashboard server.
listen_portintegerYesTCP port for the dashboard. Must differ from server.listen_port if the same listen_addr is used.

Validation: The monitor address must differ from the API server address (same IP + same port is rejected).


Optional connection to a bilbycast-manager instance for centralized monitoring and remote control. All communication uses an outbound WebSocket connection from the edge to the manager — no inbound connections are required, making this work behind NAT and firewalls.

{
"manager": {
"enabled": true,
"urls": ["wss://manager-host:8443/ws/node"],
"accept_self_signed_cert": false,
"cert_fingerprint": "ab:cd:ef:01:23:45:67:89:..."
}
}
FieldTypeRequiredDefaultDescription
enabledbooleanNofalseEnable the manager connection.
urlsarray of stringYes (if enabled)-Ordered list of manager WebSocket URLs (1-16 entries), each wss:// (TLS required). Example: ["wss://manager-host:8443/ws/node"]. For an HA-paired manager cluster, list both hostnames — the edge tries them in order and rotates on WebSocket close with a 5-second backoff. Each entry max 2048 chars.
accept_self_signed_certbooleanNofalseAccept self-signed TLS certificates from the manager. Dev/testing only — disables all TLS validation. Requires BILBYCAST_ALLOW_INSECURE=1 environment variable as a safety guard.
cert_fingerprintstringNonullSHA-256 fingerprint of the manager’s TLS certificate for certificate pinning. Format: hex with colons, e.g. "ab:cd:ef:01:23:...". When set, connections to servers presenting a different certificate are rejected, even if the certificate is CA-signed. Protects against compromised CAs. The server’s fingerprint is logged on first connection.
registration_tokenstringNonullOne-time registration token from the manager. Used on first connection only. After successful registration, the token is cleared and replaced by node_id + node_secret. Stored in secrets.json.
node_idstringNonullPersistent node ID assigned by the manager during registration. Saved automatically.
node_secretstringNonullPersistent node secret assigned by the manager during registration. Stored in secrets.json (encrypted at rest).
  1. Create a node in the manager UI — you receive a one-time registration token.
  2. Provide the token via the setup wizard (http://<edge-ip>:8080/setup) or in secrets.json.
  3. Start the edge. It connects to the manager, sends the token, and receives node_id + node_secret.
  4. Credentials are saved automatically: node_id to config.json, node_secret to secrets.json.
  5. The registration token is cleared. Future connections use node_id + node_secret.
  6. If the connection drops, the edge auto-reconnects with exponential backoff (1s to 60s).
  • url must start with wss:// (plaintext ws:// is rejected).
  • url max 2048 characters.
  • registration_token max 4096 characters.
  • accept_self_signed_cert: true is rejected unless BILBYCAST_ALLOW_INSECURE=1 is set.

IP tunnels create encrypted point-to-point links between edge nodes, either through a bilbycast-relay server (for NAT traversal) or directly via QUIC (when one edge has a public IP).

Both edges connect outbound to a bilbycast-relay server. The relay pairs them by tunnel UUID and forwards traffic. End-to-end encryption ensures the relay cannot read payloads.

relay_addrs is an ordered list: index 0 is the primary, and an optional second entry is a backup. When the primary becomes unreachable, the edge automatically fails over to the backup; when the primary recovers, an RTT-gated probe fails back. See Redundant Relay Failover.

{
"tunnels": [
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"name": "Stadium to Studio",
"protocol": "udp",
"mode": "relay",
"direction": "egress",
"local_addr": "0.0.0.0:9000",
"relay_addrs": [
"relay-primary.example.com:4433",
"relay-backup.example.com:4433"
],
"tunnel_encryption_key": "0123456789abcdef...",
"tunnel_bind_secret": "fedcba9876543210..."
}
]
}

The legacy single-field "relay_addr": "host:port" form is still accepted on load and migrated into relay_addrs[0] automatically; new configs should use relay_addrs.

One edge has a public IP. Direct QUIC connection between edges — no relay needed.

{
"tunnels": [
{
"id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
"name": "Direct Link",
"protocol": "tcp",
"mode": "direct",
"direction": "ingress",
"local_addr": "127.0.0.1:9000",
"direct_listen_addr": "0.0.0.0:4433",
"tunnel_psk": "abcdef0123456789..."
}
]
}
FieldTypeRequiredDefaultDescription
idstringYes-Unique tunnel identifier. Must be a valid UUID. Both edges in a tunnel pair must use the same ID.
namestringYes-Human-readable name.
enabledbooleanNotrueWhether the tunnel is active.
protocolstringYes-"tcp" (reliable, ordered — QUIC streams) or "udp" (unreliable — QUIC datagrams, best for SRT and media).
modestringYes-"relay" (via relay server) or "direct" (QUIC peer-to-peer).
directionstringYes-"ingress" (receives tunnel traffic, forwards to local_addr) or "egress" (listens on local_addr, sends into tunnel).
local_addrstringYes-For egress: listen address for local traffic to tunnel (e.g. "0.0.0.0:9000"). For ingress: forward destination for received traffic (e.g. "127.0.0.1:9000").
relay_addrsstring[]Relay mode[]Ordered list of relay server QUIC addresses (e.g. ["relay1:4433", "relay2:4433"]). Index 0 is the primary; a second entry enables automatic primary↔backup failover. Max 2 entries. Required for relay mode.
relay_addrstringNonullLegacy. Single relay address. Accepted on load for backward compatibility and migrated into relay_addrs[0]. Prefer relay_addrs in new configs.
max_rtt_failback_increase_msintegerNo50When the active backup is in use and the primary recovers, failback is refused if the primary’s measured QUIC RTT exceeds the backup’s by more than this many ms. Prevents flapping back to a degraded primary.
tunnel_encryption_keystringRelay modenullEnd-to-end ChaCha20-Poly1305 encryption key. Hex-encoded, exactly 64 chars (32 bytes). Required for relay mode. Both edges must share the same key. Stored in secrets.json.
tunnel_bind_secretstringNonullHMAC-SHA256 bind authentication secret. Hex-encoded, exactly 64 chars. Proves authorization to bind on the relay. Stored in secrets.json.
peer_addrstringDirect egressnullRemote peer QUIC address (e.g. "203.0.113.50:4433"). Required for direct mode, egress direction.
direct_listen_addrstringDirect ingressnullQUIC listen address (e.g. "0.0.0.0:4433"). Required for direct mode, ingress direction.
tunnel_pskstringNonullPre-shared key for direct mode authentication. Hex-encoded, 64 chars. Both edges must share the same PSK. Stored in secrets.json.
tls_cert_pemstringNoAuto-generatedTLS certificate PEM for direct mode listener. Auto-generated if absent. Stored in secrets.json.
tls_key_pemstringNoAuto-generatedTLS private key PEM for direct mode listener. Stored in secrets.json.
  • id must be a valid UUID.
  • relay_addrs (or legacy relay_addr) required when mode is "relay"; at least one, at most two entries; each 1–256 chars; duplicates rejected.
  • tunnel_encryption_key required for relay mode; must be exactly 64 hex characters.
  • tunnel_bind_secret must be exactly 64 hex characters if present.
  • peer_addr required for direct mode egress.
  • direct_listen_addr required for direct mode ingress.
  • tunnel_psk must be exactly 64 hex characters if present.
  • All address fields must be valid socket addresses.

When relay_addrs contains a second entry, the edge provides automatic primary↔backup failover:

  • Detection. The QUIC transport uses a 5 s keep-alive interval and a 25 s max-idle timeout, so a dead relay is detected after ~25 s of silence. This window is sized to tolerate Starlink satellite handovers and mobile cell-handoffs without flapping.
  • Failover. Once the primary is detected down, the edge reconnects and walks to the next relay in relay_addrs. Each reconnect attempt is bounded to 6 s so a dead primary cannot stall the loop behind the transport timeout. Expected end-to-end failover budget is ~30–40 s on WAN links (both edges detect independently; the slower side sets total latency).
  • Waiting convergence. If the two edges initially land on different relays, the first-to-bind sees Waiting; after 10 s it steps forward to the next relay so the pair converges on the same one.
  • Failback. A background probe (every 60 s) measures the primary’s QUIC RTT. When the primary’s RTT is within max_rtt_failback_increase_ms (default 50 ms) of the currently-active backup, traffic fails back to the primary. This RTT gate prevents returning to a degraded primary that is reachable but slow.
  • Event visibility. Each failover emits a Warning event to the manager with from_relay_addr, to_relay_addr, from_idx, to_idx details.

Tunnel-level failover is not hitless — expect a ~30 s gap on the tunneled flow during failover. For hitless redundancy within a flow, use SMPTE 2022-7 dual-leg or SRT bonding end-to-end; tunnel-level redundancy only protects against relay-server failure. A tunnel with a single relay_addrs entry will simply reconnect to that same address until it returns.


Each flow defines one input source fanning out to one or more output destinations.

{
"id": "main-feed",
"name": "Main Program Feed",
"enabled": true,
"input": { ... },
"outputs": [ ... ]
}
FieldTypeRequiredDefaultDescription
idstringYes-Unique identifier. Cannot be empty. Must be unique across all flows.
namestringYes-Human-readable display name. Cannot be empty.
enabledbooleanNotrueWhether to auto-start this flow on startup or creation.
media_analysisbooleanNotrueEnable media content analysis (codec, resolution, frame rate detection).
thumbnailbooleanNotrueEnable thumbnail generation (requires ffmpeg).
thumbnail_program_numberintegerNonullWhen the input is an MPTS, render the thumbnail from this MPEG-TS program only. null lets ffmpeg pick the first program it finds. Must be > 0 if set. See MPTS → SPTS filtering.
bandwidth_limitobjectNonullPer-flow bandwidth monitoring (RP 2129). See Bandwidth Limit.
inputobjectYes-Input source configuration (RTP, UDP, SRT, RTMP, RTSP, WebRTC, or WHEP).
outputsarrayYes-Output destination configurations. Can be empty. Output IDs must be unique within the flow.
assemblyobjectNonullOptional PID-bus assembly block. null (or "kind": "passthrough") = forward the active input verbatim (default). Set "kind": "spts" / "mpts" to build a fresh MPEG-TS from elementary streams pulled off any of the flow’s inputs. See Flow Assembly (PID Bus).

Optional per-flow bandwidth monitoring for SMPTE RP 2129 trust boundary enforcement. Monitors the flow’s input bitrate and takes action when it exceeds the configured limit for the grace period. Works with all input types (RTP, UDP, SRT, RTMP, RTSP, WebRTC).

{
"bandwidth_limit": {
"max_bitrate_mbps": 25.0,
"action": "alarm",
"grace_period_secs": 5
}
}
FieldTypeRequiredDefaultDescription
max_bitrate_mbpsfloatYes-Expected maximum bitrate in Mbps. Must be positive and at most 10000 (10 Gbps).
actionstringYes-"alarm": raise warning event + flag on dashboard. "block": drop all packets until bandwidth normalizes.
grace_period_secsintegerNo5Seconds the bitrate must continuously exceed the limit before triggering (1-60).

Alarm action: Emits a warning event and flags the flow on the dashboard. The flow continues operating. An info event is emitted when bitrate returns to normal.

Block action: Gates the flow — drops all incoming packets while bandwidth exceeds the limit. The flow stays alive and automatically resumes when bandwidth normalizes via a probe-and-check mechanism. Blocked packets are counted in packets_filtered.


The input object uses a type discriminator field to determine which input variant is used: rtp, udp, srt, rtmp, rtsp, webrtc, or whep.

Receives RTP-wrapped MPEG-TS packets (SMPTE ST 2022-2). Requires valid RTP v2 headers. Supports unicast, multicast, IPv4, and IPv6. For raw TS without RTP headers, use the UDP input type.

{
"type": "rtp",
"bind_addr": "239.1.1.1:5000",
"interface_addr": "192.168.1.100",
"fec_decode": {
"columns": 10,
"rows": 10
},
"allowed_sources": ["10.0.0.1"],
"allowed_payload_types": [33],
"max_bitrate_mbps": 100.0,
"tr07_mode": true
}
FieldTypeRequiredDefaultDescription
typestringYes-Must be "rtp".
bind_addrstringYes-Local socket address to bind (ip:port). For multicast, use the group address (e.g., "239.1.1.1:5000"). For unicast, use "0.0.0.0:5000". IPv6: "[::]:5000" or "[ff7e::1]:5000".
interface_addrstringNonullNetwork interface IP for multicast group join. Required for multicast on multi-homed hosts. Must be the same address family as bind_addr.
fec_decodeobjectNonullSMPTE 2022-1 FEC decode parameters. See FEC Configuration.
tr07_modebooleanNonullEnable VSF TR-07 mode to detect and report JPEG XS streams in the transport stream.
allowed_sourcesarray of stringsNonullSource IP allow-list (RP 2129 C5). Only RTP packets from these source IPs are accepted. Each entry must be a valid IP address. When null, all sources are allowed.
allowed_payload_typesarray of integersNonullRTP payload type allow-list (RP 2129 U4). Only packets with these PT values (0-127) are accepted. When null, all payload types are allowed.
max_bitrate_mbpsfloatNonullMaximum ingress bitrate in megabits per second (RP 2129 C7). Excess packets are dropped. Must be positive. When null, no rate limiting is applied.

Validation rules:

  • bind_addr must be a valid ip:port socket address.
  • interface_addr must be a valid IP address (no port) in the same address family as bind_addr.
  • allowed_payload_types values must be 0-127.
  • max_bitrate_mbps must be positive.

Receives raw UDP datagrams without requiring RTP headers. Suitable for raw MPEG-TS over UDP from OBS, ffmpeg (-f mpegts udp://), srt-live-transmit, VLC, or any source that sends plain TS.

{
"type": "udp",
"bind_addr": "0.0.0.0:5000",
"interface_addr": "192.168.1.100"
}
FieldTypeRequiredDefaultDescription
typestringYes-Must be "udp".
bind_addrstringYes-Local socket address to bind (ip:port). For multicast, use the group address.
interface_addrstringNonullNetwork interface IP for multicast group join. Must be the same address family as bind_addr.

Validation rules:

  • bind_addr must be a valid ip:port socket address.
  • interface_addr must be a valid IP address in the same address family as bind_addr.

Receives RTP encapsulated in SRT. Supports caller, listener, and rendezvous modes with optional encryption and SMPTE 2022-7 redundancy.

{
"type": "srt",
"mode": "listener",
"local_addr": "0.0.0.0:9000",
"remote_addr": null,
"latency_ms": 500,
"passphrase": "my-encryption-key",
"aes_key_len": 32,
"crypto_mode": "aes-gcm",
"redundancy": {
"mode": "listener",
"local_addr": "0.0.0.0:9001",
"latency_ms": 500,
"passphrase": "my-encryption-key",
"aes_key_len": 32,
"crypto_mode": "aes-gcm"
}
}
FieldTypeRequiredDefaultDescription
typestringYes-Must be "srt".
modestringYes-SRT connection mode: "caller", "listener", or "rendezvous". See SRT Connection Modes.
local_addrstringYes-Local socket address to bind (ip:port).
remote_addrstringConditionalnullRemote address to connect to. Required for caller and rendezvous modes.
latency_msintegerNo120SRT receive latency buffer in milliseconds. Higher values provide more resilience to network jitter at the cost of increased delay.
passphrasestringNonullAES encryption passphrase. Must be 10-79 characters. When null, encryption is disabled.
aes_key_lenintegerNo16AES key length in bytes: 16 (AES-128), 24 (AES-192), or 32 (AES-256). Only meaningful if passphrase is set.
crypto_modestringNonullCipher mode: "aes-ctr" (default) or "aes-gcm" (authenticated encryption). AES-GCM requires libsrt >= 1.5.2 on the peer and only supports AES-128/256 (not AES-192).
redundancyobjectNonullSMPTE 2022-7 redundancy configuration for a second SRT leg. See SRT Redundancy.

Validation rules:

  • local_addr must be a valid socket address.
  • remote_addr is required for caller and rendezvous modes and must be a valid socket address.
  • passphrase must be 10-79 characters.
  • aes_key_len must be 16, 24, or 32.
  • crypto_mode must be "aes-ctr" or "aes-gcm". AES-GCM with aes_key_len 24 is rejected.

Accepts incoming RTMP publish connections from OBS, ffmpeg, Wirecast, etc.

{
"type": "rtmp",
"listen_addr": "0.0.0.0:1935",
"app": "live",
"stream_key": "my_secret_key"
}

Pulls H.264 or H.265/HEVC video and AAC audio from RTSP sources (IP cameras, media servers). Uses the retina RTSP client with automatic reconnection. Produces MPEG-TS with proper PAT/PMT program tables. Audio-only streams are supported (PAT/PMT are emitted even without video).

{
"type": "rtsp",
"rtsp_url": "rtsp://camera.local:554/stream1",
"username": "admin",
"password": "secret",
"transport": "tcp"
}
FieldTypeRequiredDefaultDescription
typestringYes-Must be "rtsp".
rtsp_urlstringYes-RTSP source URL. Must start with rtsp:// or rtsps://.
usernamestringNonullRTSP authentication username (Digest or Basic).
passwordstringNonullRTSP authentication password.
transportstringNo"tcp""tcp" (interleaved, reliable) or "udp" (lower latency).
timeout_secsintegerNo10Connection timeout in seconds.
reconnect_delay_secsintegerNo5Delay between reconnection attempts on failure.

Accepts WebRTC contributions from publishers (OBS, browsers) via the WHIP protocol (RFC 9725). The webrtc feature is enabled by default.

{
"type": "webrtc",
"bearer_token": "my-auth-token"
}

Publishers POST an SDP offer to /api/v1/flows/{flow_id}/whip and receive an SDP answer. The Bearer token (if configured) must be included in the Authorization header.

FieldTypeRequiredDefaultDescription
typestringYes-Must be "webrtc".
bearer_tokenstringNonullRequired from WHIP publishers for authentication.
video_onlybooleanNofalseIgnore audio tracks from publisher.
public_ipstringNonullPublic IP to advertise in ICE candidates (for NAT traversal).
stun_serverstringNonullSTUN server URL for ICE candidate gathering.

Pulls media from an external WHEP server. The edge acts as a WHEP client. The webrtc feature is enabled by default.

{
"type": "whep",
"whep_url": "https://server.example.com/whep/stream",
"bearer_token": "optional-token"
}
FieldTypeRequiredDefaultDescription
typestringYes-Must be "whep".
whep_urlstringYes-WHEP endpoint URL to pull from.
bearer_tokenstringNonullBearer token for WHEP authentication.
video_onlybooleanNofalseReceive only video (ignore audio).

Generates a synthetic colour-bars-and-tone test pattern as an MPEG-TS stream with H.264 video and AAC audio. Useful for end-to-end pipeline tests, smoke-testing newly-deployed flows, and exercising downstream gear without a real source.

{
"type": "test_pattern",
"id": "in-test",
"name": "Test pattern",
"width": 1280,
"height": 720,
"fps": 25,
"video_bitrate_kbps": 2000,
"audio_enabled": true,
"tone_hz": 1000.0,
"tone_dbfs": -20.0,
"av_sync_marker": false
}
FieldTypeRequiredDefaultDescription
typestringYesMust be "test_pattern".
widthintegerNo1280Video width in pixels. Must be divisible by 2.
heightintegerNo720Video height in pixels. Must be divisible by 2.
fpsintegerNo25Frame rate. Range 1–60.
video_bitrate_kbpsintegerNo2000Target video bitrate in kbit/s.
audio_enabledbooleanNotrueWhen false, emits a video-only TS.
tone_hznumberNo1000.0Audio tone frequency. Range 50–8000.
tone_dbfsnumberNo-20.0Audio level in dBFS (negative). -20 dBFS is the broadcast reference.
av_sync_markerbooleanNofalseA/V-sync test mode (EBU R 49 / SMPTE 2-pop style). When true, the tone gates into a ~80 ms burst on the timecode second boundary and a luma flash patch appears next to the timecode on the same frames. Offset between audible pip and visible flash reads off directly as A/V skew. Requires audio_enabled = true.

Requires the edge build to include the media-codecs and fdk-aac features (both on by default).

Receives a media flow over the bilbycast multi-path bonding stack — the protocol that replaces appliances like Peplink/SpeedFusion with a media-aware bonded transport. Multiple network paths are aggregated for throughput and failover; per-packet sequencing reorders into a single ordered stream at this end.

{
"type": "bonded",
"id": "in-bonded",
"name": "Bonded receive",
"local_addr": "0.0.0.0:5500",
"psk": "<32-byte hex>"
}

The full Bonded protocol — path adapters, link selection, latency budgets — is covered in Bonding. The fields on the input config track the protocol’s configuration knobs; the bonded sender at the other end uses the matching Bonded Output.

Plays back a recording (or a single clip from a recording) onto a flow’s broadcast channel as if it were a live source. Paced by PCR — only available when the edge was built with the replay feature (default on).

{
"type": "replay",
"id": "in-replay",
"name": "Replay",
"recording_id": "record-flow",
"clip_id": null,
"start_paused": true,
"loop_playback": false
}
FieldTypeRequiredDefaultDescription
typestringYesMust be "replay".
recording_idstringYesRecording subdirectory under the replay root.
clip_idstringNonullWhen set, only that clip’s [in_pts, out_pts] range plays.
start_pausedbooleanNotrueWhen true, the input idles on flow start until a play_clip / cue_clip command activates playback.
loop_playbackbooleanNofalseWhen true, restart at the beginning on EOF.

Phase 1 supports 1.0× forward playback only. Full operator workflow: Replay and Replay (operator UI).


Each output has a type discriminator. All outputs share id and name fields.

Sends RTP-wrapped MPEG-TS packets to a unicast or multicast destination. Supports SMPTE 2022-1 FEC encoding.

{
"type": "rtp",
"id": "rtp-out-1",
"name": "Local Playout",
"dest_addr": "192.168.1.50:5004",
"bind_addr": "192.168.1.100:0",
"interface_addr": "192.168.1.100",
"fec_encode": {
"columns": 10,
"rows": 10
},
"dscp": 46
}
FieldTypeRequiredDefaultDescription
typestringYes-Must be "rtp".
idstringYes-Unique output ID within the flow. Cannot be empty.
namestringYes-Human-readable display name.
dest_addrstringYes-Destination socket address (ip:port). For multicast, use the group address (e.g., "239.1.2.1:5004"). IPv6: "[::1]:5004".
bind_addrstringNo"0.0.0.0:0"Source bind address. Use to control the source IP/port of outgoing packets. Must be same address family as dest_addr.
interface_addrstringNonullNetwork interface IP for multicast send. Must be same address family as dest_addr.
fec_encodeobjectNonullSMPTE 2022-1 FEC encode parameters. See FEC Configuration.
dscpintegerNo46DSCP value for QoS marking (RP 2129 C10). Range 0-63. Default 46 = Expedited Forwarding (RFC 4594).
program_numberintegerNonullMPTS → SPTS program filter. null = full MPTS passthrough; Some(N) = forward only program N as a rewritten single-program TS. Applied before FEC, so the receiver’s FEC protects the filtered SPTS. Must be > 0. See MPTS → SPTS filtering.

Validation rules:

  • id cannot be empty.
  • dest_addr, bind_addr, and interface_addr must all use the same address family.
  • dscp must be 0-63.
  • program_number must be > 0 if set (program_number 0 is reserved for the NIT).

Sends raw MPEG-TS over UDP without RTP headers. Datagrams are TS-aligned (7×188 = 1316 bytes). If the input is RTP-wrapped, RTP headers are automatically stripped. Compatible with ffplay, VLC, and standard IP/TS multicast receivers.

{
"type": "udp",
"id": "udp-out-1",
"name": "Local Playout (raw TS)",
"dest_addr": "192.168.1.50:5004",
"dscp": 46
}
FieldTypeRequiredDefaultDescription
typestringYes-Must be "udp".
idstringYes-Unique output ID within the flow.
namestringYes-Human-readable display name.
dest_addrstringYes-Destination socket address (ip:port). For multicast, use the group address.
bind_addrstringNo"0.0.0.0:0"Source bind address. Must be same address family as dest_addr.
interface_addrstringNonullNetwork interface IP for multicast send.
dscpintegerNo46DSCP value for QoS marking. Range 0-63.
program_numberintegerNonullMPTS → SPTS program filter. null = full MPTS passthrough; Some(N) = forward only program N as a rewritten single-program TS. Must be > 0. See MPTS → SPTS filtering.

Validation rules:

  • id cannot be empty.
  • dest_addr must be a valid socket address.
  • dscp must be 0-63.
  • program_number must be > 0 if set.

Sends RTP encapsulated in SRT.

{
"type": "srt",
"id": "srt-out-1",
"name": "Remote Site",
"mode": "caller",
"local_addr": "0.0.0.0:0",
"remote_addr": "203.0.113.10:9000",
"latency_ms": 500,
"passphrase": "encryption-key-here",
"aes_key_len": 32,
"redundancy": {
"mode": "caller",
"local_addr": "0.0.0.0:0",
"remote_addr": "203.0.113.11:9000",
"latency_ms": 500,
"passphrase": "encryption-key-here",
"aes_key_len": 32
}
}
FieldTypeRequiredDefaultDescription
typestringYes-Must be "srt".
idstringYes-Unique output ID within the flow. Cannot be empty.
namestringYes-Human-readable display name.
modestringYes-SRT connection mode: "caller", "listener", or "rendezvous".
local_addrstringYes-Local socket address to bind. Use "0.0.0.0:0" for caller mode (ephemeral port).
remote_addrstringConditionalnullRemote address. Required for caller and rendezvous.
latency_msintegerNo120SRT send latency in milliseconds.
passphrasestringNonullAES encryption passphrase (10-79 characters).
aes_key_lenintegerNo16AES key length: 16, 24, or 32.
crypto_modestringNonullCipher mode: "aes-ctr" (default) or "aes-gcm".
redundancyobjectNonullSMPTE 2022-7 redundancy for a second SRT output leg.
program_numberintegerNonullMPTS → SPTS program filter. null = full MPTS passthrough; Some(N) = forward only program N as a rewritten single-program TS. Applied once and mirrored to both legs when 2022-7 is enabled. Must be > 0. See MPTS → SPTS filtering.

Publishes to an RTMP/RTMPS server (e.g., Twitch, YouTube Live, Facebook Live). Demuxes H.264 and AAC from MPEG-2 TS and muxes into FLV.

{
"type": "rtmp",
"id": "twitch",
"name": "Twitch Stream",
"dest_url": "rtmp://live.twitch.tv/app",
"stream_key": "live_123456789_abcdefghijklmnop",
"reconnect_delay_secs": 5,
"max_reconnect_attempts": 10
}
FieldTypeRequiredDefaultDescription
typestringYes-Must be "rtmp".
idstringYes-Unique output ID. Cannot be empty.
namestringYes-Human-readable display name.
dest_urlstringYes-RTMP server URL. Must start with rtmp:// or rtmps://. RTMPS requires the tls feature (enabled by default).
stream_keystringYes-Stream key for authentication with the RTMP server. Cannot be empty.
reconnect_delay_secsintegerNo5Seconds to wait before reconnecting after a failure. Must be > 0.
max_reconnect_attemptsintegerNonull (unlimited)Maximum reconnection attempts. When null, reconnects indefinitely.
program_numberintegerNonullMPTS program selector. null = lock onto the lowest program_number in the PAT (deterministic default); Some(N) = extract elementary streams from program N only. RTMP is single-program by spec, so this only changes which program is published. Must be > 0. See MPTS → SPTS filtering.

Limitations:

  • Output only. RTMP input is not supported.
  • Only H.264 video and AAC audio are supported (no HEVC/VP9).

Segments MPEG-2 TS data and uploads via HTTP for HLS ingest (e.g., YouTube HLS).

{
"type": "hls",
"id": "youtube-hls",
"name": "YouTube HLS",
"ingest_url": "https://a.upload.youtube.com/http_upload_hls?cid=xxxx&copy=0&file=index.m3u8",
"segment_duration_secs": 2.0,
"auth_token": "ya29.a0ARrdaM...",
"max_segments": 5
}
FieldTypeRequiredDefaultDescription
typestringYes-Must be "hls".
idstringYes-Unique output ID. Cannot be empty.
namestringYes-Human-readable display name.
ingest_urlstringYes-HLS ingest base URL. Must start with http:// or https://.
segment_duration_secsfloatNo2.0Target segment duration in seconds. Range: 0.5-10.0.
auth_tokenstringNonullBearer token sent with each HTTP upload request.
max_segmentsintegerNo5Maximum segments in the rolling playlist. Range: 1-30.
program_numberintegerNonullMPTS → SPTS program filter. null = each segment carries the full MPTS; Some(N) = each segment carries only program N as a rewritten single-program TS. Must be > 0. See MPTS → SPTS filtering.

Limitations:

  • Output only. Segment-based transport inherently adds 1-4 seconds of latency.

Pushes fragmented MP4 (ISO BMFF) segments to an HTTP(S) ingest endpoint with parallel HLS (.m3u8) and MPEG-DASH (.mpd) manifests built off the same segment set. Supports whole-segment PUT (standard CMAF) and chunked-transfer streaming PUT (low-latency CMAF), plus ClearKey CENC encryption with optional Widevine / PlayReady / FairPlay PSSH passthrough.

Standard CMAF — HLS + DASH, AAC passthrough, unencrypted:

{
"type": "cmaf",
"id": "cdn-primary",
"name": "CDN primary push",
"ingest_url": "https://ingest.cdn.example.com/live/channel1",
"auth_token": "Bearer-xyz",
"segment_duration_secs": 4.0,
"max_segments": 6,
"manifests": ["hls", "dash"]
}

LL-CMAF — HLS-only, FairPlay CBCS encryption:

{
"type": "cmaf",
"id": "ll-ios",
"name": "Low-latency iOS",
"ingest_url": "https://ll.cdn.example.com/live/ios",
"low_latency": true,
"chunk_duration_ms": 333,
"segment_duration_secs": 2.0,
"manifests": ["hls"],
"encryption": {
"scheme": "cbcs",
"key_id": "0123456789abcdef0123456789abcdef",
"key": "fedcba9876543210fedcba9876543210"
}
}

DASH-only — HEVC re-encode at 5 Mbps, ClearKey CENC + Widevine PSSH passthrough:

{
"type": "cmaf",
"id": "uhd-dash",
"name": "UHD DASH egress",
"ingest_url": "https://ingest.cdn.example.com/live/uhd",
"segment_duration_secs": 4.0,
"manifests": ["dash"],
"video_encode": { "codec": "x265", "bitrate_kbps": 5000, "preset": "medium", "profile": "main10" },
"audio_encode": { "codec": "he_aac_v1", "bitrate_kbps": 64 },
"encryption": {
"scheme": "cenc",
"key_id": "0123456789abcdef0123456789abcdef",
"key": "fedcba9876543210fedcba9876543210",
"pssh_boxes": ["<widevine-pssh-hex>", "<playready-pssh-hex>"]
}
}
FieldTypeRequiredDefaultDescription
typestringYes-Must be "cmaf".
idstringYes-Unique output ID. Cannot be empty.
namestringYes-Human-readable display name.
ingest_urlstringYes-CMAF ingest base URL. Must start with http:// or https://, max 2048 chars, no control characters.
auth_tokenstringNonullBearer token sent as Authorization: Bearer <token> on every PUT. Max 4096 chars, no control/whitespace characters.
segment_duration_secsfloatNo2.0Target closed-GoP segment duration in seconds. Range 1.0-10.0. Used as the GoP alignment target when video_encode is set.
max_segmentsintegerNo5Rolling playlist depth. Range 1-30.
low_latencybooleanNofalsefalse = standard CMAF (whole-segment PUT). true = LL-CMAF (chunked-transfer PUT + #EXT-X-PART in HLS and availabilityTimeOffset in DASH).
chunk_duration_msintegerNo500Sub-segment chunk cadence for LL-CMAF. Range 100-2000. Only meaningful when low_latency = true; ignored otherwise.
manifestsarray<string>No["hls", "dash"]Which manifests to publish. Non-empty subset of ["hls", "dash"]. Use ["hls"] for Apple-only, ["dash"] for Widevine/PlayReady-centric CDNs, both for maximum reach.
encryptionobjectNonullClearKey CENC encryption block — see below. Omit for clear (unencrypted) output.
audio_encodeobjectNonullOptional AAC re-encode (codec: aac_lc, he_aac_v1, he_aac_v2). Omit for AAC passthrough. MP2/AC-3/Opus are rejected — CMAF audio is AAC-family only.
transcodeobjectNonullOptional PCM channel-shuffle / sample-rate / bit-depth conversion, sits between the AAC decoder and the target encoder. Only effective when audio_encode is set; rejected at validation otherwise.
video_encodeobjectNonullOptional re-encode (same schema as ST 2110 video inputs: x264, x265, h264_nvenc, hevc_nvenc). HEVC (x265, hevc_nvenc) requires manifests: ["dash"] — HLS fMP4 HEVC client support is inconsistent. Omit for passthrough.
program_numberintegerNonullMPTS → SPTS program filter. null = source must already be SPTS (CMAF is inherently single-program). Some(N) = filter to program N before segmenting. Must be > 0.

Encryption block (encryption):

FieldTypeRequiredDefaultDescription
schemestringYes-"cenc" (AES-CTR — Widevine/PlayReady standard, pairs with DASH) or "cbcs" (AES-CBC with 1:9 pattern — FairPlay / Apple standard, pairs with HLS).
key_idstringYes-Key identifier. Exactly 32 hex characters (16 bytes).
keystringYes-AES-128 content key. Exactly 32 hex characters (16 bytes).
pssh_boxesarray<string>No[]Pre-built Widevine / PlayReady / FairPlay pssh boxes for commercial DRM passthrough. Each entry is a hex-encoded pssh ISO-BMFF box (32-4096 bytes, fourcc pssh at bytes 4-7). The edge copies each entry verbatim into the init segment’s moov alongside the ClearKey pssh box it emits automatically.

How encryption works on the wire:

  • CMAF uses ISO/IEC 23001-7 Common Encryption with subsample encryption: video NAL prefixes and parameter sets stay in the clear (~first 32 bytes per NAL), the rest is encrypted under the chosen scheme.
  • Each segment carries senc (sample encryption), saio (sample auxiliary info offsets), saiz (sample auxiliary info sizes), and tenc (track encryption) boxes.
  • The init segment’s moov carries one or more pssh boxes — the edge-emitted ClearKey pssh plus any operator-supplied commercial-DRM boxes.
  • Commercial DRM license servers (Widevine, PlayReady, FairPlay) are operator-managed; bilbycast does not proxy license requests.

Limitations:

  • Output only. Players are browsers, iOS/tvOS/Android apps, smart TVs, STBs — the edge does not ingest CMAF.
  • HEVC (x265, hevc_nvenc) on HLS is rejected client-side by many Apple devices. The edge does not reject the combination — operators who need HEVC should emit manifests: ["dash"].
  • transcode requires audio_encode; rejected at validation when set alone.

Reference: bilbycast-edge/docs/cmaf.md in the repo covers the ISO-BMFF box writer, LL-CMAF threading model, DASH MPD profile (dynamic, availabilityStartTime, minimumUpdatePeriod, timeShiftBufferDepth, SegmentTemplate), HEVC hvc1 vs hev1 signalling, and the CENC subsample algorithm.

Supports two modes: WHIP client (push to external endpoint) and WHEP server (serve viewers). The webrtc feature is enabled by default.

WHIP Client mode — push to an external WHIP endpoint:

{
"type": "webrtc",
"id": "whip-push",
"name": "Push to CDN",
"mode": "whip_client",
"whip_url": "https://whip.example.com/ingest/stream1",
"bearer_token": "my-auth-token"
}

WHEP Server mode — serve browser viewers:

{
"type": "webrtc",
"id": "whep-serve",
"name": "Browser Viewers",
"mode": "whep_server",
"max_viewers": 20,
"bearer_token": "viewer-auth-token"
}

Viewers POST an SDP offer to /api/v1/flows/{flow_id}/whep and receive an SDP answer.

FieldTypeRequiredDefaultDescription
typestringYes-Must be "webrtc".
idstringYes-Unique output ID.
namestringYes-Human-readable display name.
modestringNo"whip_client""whip_client" (push to endpoint) or "whep_server" (serve viewers).
whip_urlstringWHIP only-WHIP endpoint URL. Required for whip_client mode.
bearer_tokenstringNonullBearer token for authentication.
max_viewersintegerNo10Max concurrent viewers (WHEP server mode only, 1-100).
public_ipstringNonullPublic IP for ICE candidates (NAT traversal).
video_onlybooleanNofalseOnly send video. When set, any audio_encode block is rejected at validation (an audio MID is required in the SDP to carry Opus).
program_numberintegerNonullMPTS program selector. null = lock onto the lowest program_number in the PAT (deterministic default); Some(N) = extract elementary streams from program N only. WebRTC is single-program by spec, so this only changes which program is sent. Must be > 0. See MPTS → SPTS filtering.
audio_encodeobjectNonullOptional Phase B audio_encode block (codec: opus). When absent, Opus sources are carried natively and AAC sources automatically fall back to video-only because WebRTC does not carry AAC. When present, the input AAC is decoded via the Phase A engine::audio_decode::AacDecoder (FDK AAC by default, supporting AAC-LC/HE-AAC v1/v2/multichannel) and re-encoded as Opus via the engine::audio_encode::AudioEncoder (ffmpeg subprocess for Opus) — see Audio Gateway — audio_encode.

Audio: Opus passthrough by default — Opus flows natively on WebRTC paths. AAC contribution sources need an audio_encode: { codec: "opus" } block to be carried as Opus; without it, AAC sources fall back to video-only.

Plays the flow’s video to a locally-attached HDMI / DisplayPort connector and (optionally) routes its audio to an ALSA device. Linux-only and gated on the display Cargo feature (on by default in every release tarball).

{
"type": "display",
"id": "out-confidence",
"name": "Green-room HDMI",
"device": "HDMI-A-1",
"audio_device": "hw:0,3"
}
FieldTypeRequiredDefaultDescription
typestringYesMust be "display".
devicestringYesKMS connector name from the edge’s display enumeration: "HDMI-A-1", "DP-2", "DVI-D-1". Validated against ^[A-Z][A-Z0-9-]{0,63}$.
audio_devicestringNonullALSA device id ("hw:0,3", "plughw:0,3", "default", "sysdefault", "pulse"). Omit for video-only.
program_numberintegerNonullMPTS program filter (1-based; 0 reserved). null selects the lowest program in the active input’s PAT.
audio_track_indexintegerNonullAudio elementary-stream index within the chosen program. Must be < 16.
audio_channel_pairarrayNo[0, 1]Stereo pair to render from decoded multichannel audio. Both indices < 8 and not equal.
resolutionstringNonull"auto" or "WIDTHxHEIGHT" (e.g. "1920x1080").
refresh_hzintegerNonullRefresh rate in Hz. Range 1–240. null uses the connector’s preferred mode.
sync_modestringNo"vsync_to_display"v1 only accepts "vsync_to_display".

Connectors are enumerated at edge startup and surfaced in HealthPayload.display_devices — the manager UI populates the Device dropdown from this list. HDMI hotplug discovery is startup-only in v1; new cables require restarting the edge.

Full reference, including A/V sync, supported codecs, capacity budget, and the display_* event catalogue: Display Output.

Sends a media flow over the bilbycast multi-path bonding stack — the bonded transport that replaces appliances like Peplink/SpeedFusion with a media-aware multi-path egress. Multiple network paths are aggregated for throughput and failover.

{
"type": "bonded",
"id": "out-bonded",
"name": "Bonded send",
"remote_addr": "203.0.113.10:5500",
"psk": "<32-byte hex>"
}

The full Bonded protocol — path adapters, link selection, latency budgets, FEC — is covered in Bonding. At the receiving end, use a matching Bonded Input.


Continuous flow recording to disk is a per-flow attribute (recording) rather than an output type — the writer is a sibling subscriber on the broadcast channel, not an egress. It can never block live outputs.

"flows": [{
"id": "record-flow",
"name": "Record live SRT to disk",
"enabled": true,
"input_ids": ["live-srt-in"],
"output_ids": [],
"recording": {
"enabled": true,
"storage_id": "record-flow",
"segment_seconds": 10,
"retention_seconds": 86400,
"max_bytes": 53687091200,
"pre_buffer_seconds": null
}
}]
FieldTypeRequiredDefaultDescription
enabledbooleanNotrueWhen false, the writer is built but doesn’t subscribe — useful for cron-armed recording via routines.
storage_idstringNoflow idSubdirectory under the replay root. Alphanumeric + ._-, ≤ 64 chars.
segment_secondsintegerNo10Wall-clock segment roll cadence. Range [2, 60].
retention_secondsintegerNo86400Oldest-first prune by mtime. 0 = unlimited.
max_bytesintegerNo53687091200Oldest-first prune by total size. 0 = unlimited.
pre_buffer_secondsintegerNonullWhen set, the writer auto-arms in pre-buffer mode and rolls segments under the matching retention so an operator pressing Start later picks up the last N seconds of pre-roll. Range [1, 300].

A flow with output_ids: [] and recording.enabled: true is a monitor-only recorder — recommended for compliance recording.

Storage root resolution order: BILBYCAST_REPLAY_DIR$XDG_DATA_HOME/bilbycast/replay/$HOME/.bilbycast/replay/./replay/. Per-recording cap via max_bytes; no global root cap.

Full reference, including playback as an input, error catalogue, and Phase 2 / 1.5 features: Replay.


Optional top-level resource_limits block. When set, the edge samples CPU and RAM usage on a periodic tick and emits Warning / Critical events under category system_resources when thresholds are exceeded. Optionally gates new flow creation when resources are critical.

{
"version": 2,
"resource_limits": {
"cpu_warning_percent": 80,
"cpu_critical_percent": 95,
"ram_warning_percent": 80,
"ram_critical_percent": 95,
"critical_action": "alarm",
"grace_period_secs": 10
},
"inputs": [],
"outputs": [],
"flows": []
}
FieldTypeDefaultDescription
cpu_warning_percentnumber80CPU usage warning threshold (0–100).
cpu_critical_percentnumber95CPU usage critical threshold.
ram_warning_percentnumber80RAM usage warning threshold (0–100).
ram_critical_percentnumber95RAM usage critical threshold.
critical_actionstring"alarm"What to do when any metric is critical. "alarm" — events only, flows continue. "gate_flows" — additionally reject new flow creation while any metric is critical.
grace_period_secsinteger10Seconds the metric must continuously exceed the threshold before the event fires (debounce).

Omit the block to disable system-resource alarms entirely. The edge’s resource-budget probe (advertised on HealthPayload.resource_budget) is independent — that’s a one-shot hardware-capability snapshot at startup, not a runtime metric.


Flow Assembly (PID Bus — SPTS / MPTS from N inputs)

Section titled “Flow Assembly (PID Bus — SPTS / MPTS from N inputs)”

A flow can optionally carry an assembly block that tells the runtime to stop forwarding one input verbatim and instead build a fresh MPEG-TS from elementary streams pulled off any of the flow’s inputs. Every output type (UDP, RTP, SRT, RIST, RTMP/RTMPS, HLS, CMAF / CMAF-LL, WebRTC) consumes the assembled TS unchanged — no output-type gate.

Three kind values:

KindProgramsPCR
passthroughmust be emptynone — forwards the active input verbatim (same behaviour as assembly = null)
sptsexactly oneflow-level or program-level pcr_source
mptsone or more, unique program_number per programevery program needs an effective pcr_source

Slot sources: pid (explicit PID off a named input), essence (first video / audio / subtitle / data ES off a named input, resolved against the input’s live PSI catalogue), or hitless (primary-preference pre-bus merger with 200 ms stall timer — not 2022-7 seq-aware).

PCM / AES3 inputs (ST 2110-30, ST 2110-31, rtp_audio) become TS carriers by setting audio_encode on the inputaac_lc / he_aac_v1 / he_aac_v2 / s302m (ST 2110-31 must use s302m).

The plan is hot-swappable at runtime — UpdateFlowAssembly replaces the running plan, unchanged slots keep their bus fan-ins (no packet gap), PMT version_number bumps mod 32 for changed programs, PAT only when the program set changes, and PSI is re-emitted immediately so receivers see the new PMT before any packet lands on a new out_pid. Transitions across the passthrough boundary (passthrough ↔ spts/mpts) are rejected — use a full UpdateFlow.

Full reference, examples, validation rules, and monitoring: Flow Assembly (PID Bus).


All outputs — and the thumbnail generator — accept an optional program_number selector for down-selecting an MPTS (Multi-Program Transport Stream) input to a single program. Whether the filter rewrites TS bytes or just picks which elementary streams to extract depends on the output type.

Outputprogram_number = null (default)program_number = N
UDP / RTP / SRT / HLS (TS-native)full MPTS passthrough (current behaviour)PAT rewritten to a single-program form; only program N’s PMT, ES, and PCR PIDs survive. FEC (2022-1) and hitless redundancy (2022-7) operate on the filtered bytes.
RTMP / WebRTC (re-muxing)lock onto the lowest program_number in the PAT (deterministic — replaces the old “first PMT seen” race)extract elementary streams from program N’s PMT only
Thumbnail generator (thumbnail_program_number on FlowConfig)ffmpeg picks the first program it findsTS is pre-filtered so ffmpeg only sees program N
  • program_number is per-output. One flow can run three outputs in parallel — one forwarding full MPTS to an archive, one filtered to program 1, and another to program 2 — all sharing the same broadcast channel.
  • program_number = 0 is rejected at config load and on manager commands. Program number 0 is reserved for the NIT in the MPEG-TS specification and never identifies a real program.
  • Disappearing programs (selected program not in the PAT, or a PAT version bump removes it): the output emits nothing until the program reappears. The filter automatically recovers on the next PAT that re-advertises the target.
  • SPTS inputs are unaffected — there’s only one program, so program_number = 1 (or whatever it is) filters to the same stream that was already there.

Example — 2-program MPTS fanning out to three destinations

Section titled “Example — 2-program MPTS fanning out to three destinations”
{
"id": "mpts-flow",
"name": "Dual-program feed",
"thumbnail_program_number": 1,
"input": { "type": "udp", "bind_addr": "0.0.0.0:5020" },
"outputs": [
{
"type": "udp", "id": "archive", "name": "Archive full MPTS",
"dest_addr": "10.0.0.5:6000"
},
{
"type": "udp", "id": "prog1-viewer", "name": "Program 1 → ffplay",
"dest_addr": "127.0.0.1:6001",
"program_number": 1
},
{
"type": "rtmp", "id": "prog2-rtmp", "name": "Program 2 → CDN",
"dest_url": "rtmp://live.example.com/app",
"stream_key": "my-key",
"program_number": 2
}
]
}

The archive receives the full MPTS. The prog1-viewer UDP output sends only program 1 as a rewritten SPTS (PAT lists one entry, program 1’s PMT + ES PIDs). The RTMP output publishes program 2’s elementary streams. The manager UI thumbnail shows a frame from program 1.


Forward Error Correction parameters used by fec_decode (on RTP inputs) and fec_encode (on RTP outputs).

{
"columns": 10,
"rows": 10
}
FieldTypeRequiredRangeDescription
columnsintegerYes1-20L parameter: number of columns in the FEC matrix.
rowsintegerYes4-20D parameter: number of rows in the FEC matrix.

The FEC matrix protects columns x rows media packets with columns + rows parity packets. Larger matrices provide better protection at the cost of higher latency and bandwidth overhead.

Common configurations:

  • 5 x 5 — Low overhead, moderate protection
  • 10 x 10 — Good balance of overhead and protection
  • 20 x 20 — Maximum protection, higher latency

Both SRT input and SRT output support SMPTE 2022-7 hitless redundancy via a second SRT leg. The parent SRT config defines leg 1; the redundancy block defines leg 2.

For input: packets from both legs are merged using RTP sequence numbers, providing seamless failover if one path fails.

For output: packets are duplicated and sent on both legs simultaneously.

{
"redundancy": {
"mode": "listener",
"local_addr": "0.0.0.0:9001",
"remote_addr": null,
"latency_ms": 500,
"passphrase": "encryption-key",
"aes_key_len": 32
}
}
FieldTypeRequiredDefaultDescription
modestringYes-SRT mode for leg 2: "caller", "listener", or "rendezvous".
local_addrstringYes-Local bind address for leg 2.
remote_addrstringConditionalnullRemote address for leg 2 (required for caller/rendezvous).
latency_msintegerNo120SRT latency for leg 2.
passphrasestringNonullAES encryption passphrase for leg 2 (10-79 characters).
aes_key_lenintegerNo16AES key length for leg 2 (16, 24, or 32).
crypto_modestringNonullCipher mode for leg 2: "aes-ctr" or "aes-gcm".

Legs can use different SRT modes, different ports, different latency values, and even different encryption settings (though using the same settings is recommended for simplicity).


ModeInitiatorremote_addr requiredUse case
callerThis endpoint connects to a remote listenerYesSending to a known destination. Most common for outputs.
listenerThis endpoint waits for incoming connectionsNoAccepting streams from remote callers. Most common for inputs (ingest servers).
rendezvousBoth sides connect simultaneouslyYesNAT traversal. Both sides must use rendezvous mode and know each other’s address.

Command-line arguments override values from the config file. This is useful for deployment automation and containerization.

bilbycast-edge [OPTIONS]
Options:
-c, --config <PATH> Path to configuration file [default: ./config.json]
-p, --port <PORT> Override API listen port
-b, --bind <ADDRESS> Override API listen address
--monitor-port <PORT> Override monitor dashboard port
-l, --log-level <LEVEL> Log level: trace, debug, info, warn, error [default: info]
-h, --help Print help
-V, --version Print version
ArgumentConfig field overriddenExample
--portserver.listen_port--port 9443
--bindserver.listen_addr--bind 127.0.0.1
--monitor-portmonitor.listen_port--monitor-port 9091
--log-level(runtime only, not in config)--log-level debug

The log level can also be set via the RUST_LOG environment variable, which takes precedence over the --log-level argument when set. Supports fine-grained filtering (e.g., RUST_LOG=bilbycast_edge=debug,tower_http=info).

Examples:

Terminal window
# Use a specific config file
bilbycast-edge --config /etc/bilbycast/production.json
# Override port for containerized deployment
bilbycast-edge --config config.json --port 443 --bind 0.0.0.0
# Debug logging
bilbycast-edge --config config.json --log-level debug
# Fine-grained logging via environment
RUST_LOG=bilbycast_edge=debug,tower_http=info bilbycast-edge --config config.json

bilbycast-edge automatically persists configuration changes to disk when flows are modified through the API. Flow configs (including user parameters like SRT passphrases, RTSP credentials, RTMP keys) go to config.json, infrastructure secrets go to secrets.json:

  • Create flow (POST /api/v1/flows) — Appends the new flow and saves (flow parameters stay in config.json).
  • Update flow (PUT /api/v1/flows/{id}) — Replaces the flow in-place and saves.
  • Delete flow (DELETE /api/v1/flows/{id}) — Removes the flow and saves.
  • Add output (POST /api/v1/flows/{id}/outputs) — Appends the output and saves.
  • Remove output (DELETE /api/v1/flows/{id}/outputs/{oid}) — Removes the output and saves.
  • Replace config (PUT /api/v1/config) — Replaces the entire config and saves.
  • Get config (GET /api/v1/config) — Returns the config with infrastructure secrets stripped. Flow parameters (passphrases, credentials, keys) are included in the response.

All config saves use an atomic write strategy: both config.json and secrets.json are written to temporary files (.json.tmp), then atomically renamed to the target paths. This prevents corruption if the process is interrupted during a write. secrets.json is written with 0600 permissions (owner-only) on Unix.

If the config file does not exist when bilbycast-edge starts, an empty default configuration is used:

{
"version": 1,
"server": {
"listen_addr": "0.0.0.0",
"listen_port": 8080
},
"flows": []
}

Use POST /api/v1/config/reload to re-read both config.json and secrets.json from disk. This is useful after manual edits or after deploying new config files via external tooling (e.g., Ansible, Chef).


Minimal: RTP receive and forward (no auth)

Section titled “Minimal: RTP receive and forward (no auth)”
{
"version": 1,
"server": {
"listen_addr": "0.0.0.0",
"listen_port": 8080
},
"flows": [
{
"id": "passthrough",
"name": "RTP Passthrough",
"enabled": true,
"input": {
"type": "rtp",
"bind_addr": "0.0.0.0:5000"
},
"outputs": [
{
"type": "rtp",
"id": "out-1",
"name": "Forwarded Output",
"dest_addr": "192.168.1.50:5004"
}
]
}
]
}

Multicast receive with FEC and trust boundary filters

Section titled “Multicast receive with FEC and trust boundary filters”
{
"version": 1,
"server": {
"listen_addr": "0.0.0.0",
"listen_port": 8080
},
"flows": [
{
"id": "multicast-feed",
"name": "Multicast with FEC and Trust Boundary",
"enabled": true,
"input": {
"type": "rtp",
"bind_addr": "239.1.1.1:5000",
"interface_addr": "10.0.0.100",
"fec_decode": {
"columns": 10,
"rows": 10
},
"allowed_sources": ["10.0.0.1"],
"allowed_payload_types": [33],
"max_bitrate_mbps": 50.0,
"tr07_mode": true
},
"outputs": [
{
"type": "rtp",
"id": "local-out",
"name": "Local Multicast Output",
"dest_addr": "239.1.2.1:5004",
"interface_addr": "10.0.0.100",
"fec_encode": {
"columns": 10,
"rows": 10
},
"dscp": 46
}
]
}
]
}
{
"version": 1,
"server": {
"listen_addr": "0.0.0.0",
"listen_port": 8080
},
"flows": [
{
"id": "srt-redundant",
"name": "SRT with Hitless Redundancy",
"enabled": true,
"input": {
"type": "srt",
"mode": "listener",
"local_addr": "0.0.0.0:9000",
"latency_ms": 500,
"passphrase": "my-secure-passphrase-1234",
"aes_key_len": 32,
"redundancy": {
"mode": "listener",
"local_addr": "0.0.0.0:9001",
"latency_ms": 500,
"passphrase": "my-secure-passphrase-1234",
"aes_key_len": 32
}
},
"outputs": [
{
"type": "srt",
"id": "srt-out",
"name": "SRT Redundant Output",
"mode": "caller",
"local_addr": "0.0.0.0:0",
"remote_addr": "203.0.113.10:9000",
"latency_ms": 500,
"passphrase": "output-passphrase-1234567",
"aes_key_len": 32,
"redundancy": {
"mode": "caller",
"local_addr": "0.0.0.0:0",
"remote_addr": "203.0.113.11:9000",
"latency_ms": 500,
"passphrase": "output-passphrase-1234567",
"aes_key_len": 32
}
}
]
}
]
}

Multi-output: RTP to SRT, RTMP, and HLS simultaneously

Section titled “Multi-output: RTP to SRT, RTMP, and HLS simultaneously”
{
"version": 1,
"server": {
"listen_addr": "0.0.0.0",
"listen_port": 8080
},
"flows": [
{
"id": "multi-output",
"name": "Multi-Output Fan-Out",
"enabled": true,
"input": {
"type": "rtp",
"bind_addr": "239.1.1.1:5000",
"interface_addr": "192.168.1.100"
},
"outputs": [
{
"type": "rtp",
"id": "local",
"name": "Local Playout",
"dest_addr": "192.168.1.50:5004"
},
{
"type": "srt",
"id": "remote-srt",
"name": "Remote Site SRT",
"mode": "caller",
"local_addr": "0.0.0.0:0",
"remote_addr": "203.0.113.10:9000",
"latency_ms": 300
},
{
"type": "rtmp",
"id": "twitch",
"name": "Twitch",
"dest_url": "rtmp://live.twitch.tv/app",
"stream_key": "live_xxxxxxxxxxxx"
},
{
"type": "hls",
"id": "youtube-hls",
"name": "YouTube HLS",
"ingest_url": "https://a.upload.youtube.com/http_upload_hls?cid=xxxx",
"segment_duration_secs": 2.0
}
]
}
]
}

Full production config with TLS + auth + monitoring

Section titled “Full production config with TLS + auth + monitoring”
{
"version": 1,
"server": {
"listen_addr": "0.0.0.0",
"listen_port": 8443,
"tls": {
"cert_path": "/etc/bilbycast/cert.pem",
"key_path": "/etc/bilbycast/key.pem"
},
"auth": {
"enabled": true,
"jwt_secret": "K7nXp2qR8vF3mBwYd0hL5jZ1tA6gCeHsN9uIoP4xWkQrJfMaVbDcEiGyTlUwSzO",
"token_lifetime_secs": 3600,
"public_metrics": true,
"clients": [
{
"client_id": "ops-admin",
"client_secret": "admin-secret-change-me",
"role": "admin"
},
{
"client_id": "grafana",
"client_secret": "grafana-read-secret",
"role": "monitor"
}
]
}
},
"monitor": {
"listen_addr": "0.0.0.0",
"listen_port": 9090
},
"flows": [
{
"id": "main-feed",
"name": "Main Program Feed",
"enabled": true,
"input": {
"type": "rtp",
"bind_addr": "239.1.1.1:5000",
"interface_addr": "10.0.0.100",
"fec_decode": {
"columns": 10,
"rows": 10
}
},
"outputs": [
{
"type": "rtp",
"id": "local-playout",
"name": "Local Playout",
"dest_addr": "10.0.0.50:5004",
"dscp": 46
},
{
"type": "srt",
"id": "remote-site",
"name": "Remote Site",
"mode": "caller",
"local_addr": "0.0.0.0:0",
"remote_addr": "203.0.113.10:9000",
"latency_ms": 500,
"passphrase": "secure-transport-key-1234",
"aes_key_len": 32
}
]
}
]
}
{
"version": 1,
"server": {
"listen_addr": "0.0.0.0",
"listen_port": 8080
},
"flows": [
{
"id": "ipv6-mcast",
"name": "IPv6 Multicast Flow",
"enabled": true,
"input": {
"type": "rtp",
"bind_addr": "[ff7e::1]:5000",
"interface_addr": "::1"
},
"outputs": [
{
"type": "rtp",
"id": "ipv6-out",
"name": "IPv6 Output",
"dest_addr": "[ff7e::2]:5004",
"interface_addr": "::1"
}
]
}
]
}

bilbycast-edge supports SMPTE ST 2110-30 (linear PCM L16/L24), ST 2110-31 (AES3 transparent for Dolby E and similar), and ST 2110-40 (RFC 8331 ancillary data including SCTE-104, SMPTE 12M timecode, and CEA-608/708 captions). Video essences (ST 2110-22 JPEG XS, ST 2110-20 uncompressed) are reserved for Phase 2 and Phase 3.

PTP integration is best-effort and reads from an external ptp4l daemon’s management Unix socket — no PTP daemon ships in the edge. SMPTE 2022-7 Red/Blue dual-network operation is opt-in via the redundancy block on each ST 2110 input/output.

Both fields are optional and backward-compatible — existing configs deserialize unchanged.

FieldTypePurpose
clock_domainu8IEEE 1588 PTP domain (0–127). Setting this on a flow makes the edge spawn a PTP state reporter and surface lock state through FlowStats.ptp_state.
flow_group_idstringLogical bundle id; multiple essence flows on a single edge can share a group so the manager treats them as one unit.
{
"id": "studio-a-stereo",
"name": "Studio A — stereo",
"enabled": true,
"clock_domain": 0,
"input": {
"type": "st2110_30",
"bind_addr": "239.0.0.10:5000",
"interface_addr": "10.0.0.5",
"sample_rate": 48000,
"bit_depth": 24,
"channels": 2,
"packet_time_us": 1000,
"payload_type": 97,
"redundancy": {
"addr": "239.1.0.10:5000",
"interface_addr": "10.1.0.5"
}
},
"outputs": []
}

type: "st2110_31" uses an identical struct — only the depacketizer label changes. AES3 transparency preserves user bits, channel status, validity, and parity bits.

{
"type": "st2110_30",
"id": "monitor-out",
"name": "Loopback to monitor",
"dest_addr": "239.2.0.10:5000",
"dscp": 46,
"sample_rate": 48000,
"bit_depth": 24,
"channels": 2,
"packet_time_us": 1000,
"payload_type": 97,
"redundancy": {
"addr": "239.3.0.10:5000",
"interface_addr": "10.1.0.5"
}
}
{
"id": "anc-flow",
"name": "ANC (timecode + SCTE-104)",
"enabled": true,
"clock_domain": 0,
"input": {
"type": "st2110_40",
"bind_addr": "239.0.0.20:5000",
"payload_type": 100
},
"outputs": [
{
"type": "st2110_40",
"id": "anc-out",
"name": "ANC loopback",
"dest_addr": "239.2.0.20:5000",
"dscp": 46,
"payload_type": 100
}
]
}
FieldAllowed values
sample_rate48000, 96000
bit_depth16, 24
channels1, 2, 4, 8, 16
packet_time_us125 (AM), 1000 (PM)
payload_type96127
clock_domain0127
dscp063 (default 46 / EF)

Combining allowed_sources with redundancy is rejected by validation — the merger path doesn’t expose per-packet src and the dual-leg path won’t silently bypass the source filter.

Every audio output (st2110_30, st2110_31, rtp_audio) accepts an optional transcode block for sample-rate / bit-depth / channel-routing conversion via the rubato SRC. IS-08 channel maps hot-reload without a flow restart. Full field reference, presets, and worked examples live in Audio Gateway.

The rtp_audio input/output type is wire-identical to ST 2110-30 (same RFC 3551 RTP + L16/L24 PCM payload) with relaxed constraints — sample rates 32 / 44.1 / 48 / 88.2 / 96 kHz, no PTP requirement, no clock_domain. Use it for WAN contribution, talkback, and ffmpeg/OBS interop.

srt, udp, and rtp_audio outputs accept transport_mode: "audio_302m" to ship 48 kHz LPCM as SMPTE 302M-in-MPEG-TS. Mutually exclusive with packet_filter (SRT), program_number, and SRT redundancy.

Phase A compressed-audio ingress: when a flow input carries AAC in MPEG-TS (RTMP / RTSP / SRT / UDP / RTP), the in-process engine::audio_decode::AacDecoder turns it into PCM so ST 2110-30/-31, rtp_audio, and the SMPTE 302M outputs can consume it without ffmpeg. Default FDK AAC backend supports AAC-LC, HE-AAC v1/v2, and multichannel up to 7.1; symphonia fallback supports AAC-LC mono/stereo only.

Phase B audio_encode block on RTMP / HLS / WebRTC outputs:

{
"type": "rtmp",
"id": "yt-rtmp",
"dest_url": "rtmps://a.rtmps.youtube.com/live2",
"stream_key": "...",
"audio_encode": {
"codec": "aac_lc",
"bitrate_kbps": 96
}
}
FieldAllowed values
audio_encode.codec (RTMP)aac_lc, he_aac_v1, he_aac_v2
audio_encode.codec (HLS)aac_lc, he_aac_v1, he_aac_v2, mp2, ac3
audio_encode.codec (WebRTC)opus
audio_encode.bitrate_kbps16..=512
audio_encode.sample_rate8000, 16000, 22050, 24000, 32000, 44100, 48000
audio_encode.channels1 or 2
audio_encode on WebRTC + video_only=truerejected (audio MID required in SDP)

Requires ffmpeg in PATH at runtime — outputs without audio_encode keep working without ffmpeg installed. RTMP and WebRTC run one persistent ffmpeg per encoded output; HLS forks ffmpeg per segment.

A flow group binds multiple per-essence flows into a single logical unit sharing a PTP clock_domain. The schema lives at the top level of the config:

{
"version": 1,
"flow_groups": [
{
"id": "studio-a-program",
"name": "Studio A program",
"clock_domain": 0,
"flow_ids": ["studio-a-stereo", "anc-flow"]
}
]
}