Configuration file
Unda reads one YAML file: /etc/unda/unda.yaml with the installer, /etc/unda/unda.yaml inside the Docker container. Restart Unda after changing it (sudo systemctl restart unda; live encoders reconnect by themselves).
Secrets can stay out of the file: UNDA_API_TOKEN, UNDA_SRT_PASSPHRASE and UNDA_STREAM_KEYS (or the same names ending in _FILE, pointing at a file) in /etc/unda/unda.env override it. Restream, webhook and pull input URLs take url_env or url_file instead of url.
api.token (UNDA_API_TOKEN) is deprecated: for scripts and Prometheus, create an API key under Server → API keys in the dashboard instead. While the token is set, Unda logs a warning at startup and shows one to admins on the Overview.
This is the complete example file, with every setting and its default:
server:
rtmp_addr: ":1935"
http_addr: ":8080"
# HTTPS. Easiest: automatic certificates from Let's Encrypt. Point the
# domain's DNS at this server, open ports 443 and 80, then:
# http_addr: ":443"
# http_redirect_addr: ":80" # plain HTTP: redirects to HTTPS, answers Let's Encrypt
# public_https_port: 443 # only behind a port map (Docker 443 -> 8443):
# # the port that redirect sends browsers to
# tls:
# letsencrypt:
# domains: [tv.example.com]
# email: ops@example.com # optional: expiry notices from Let's Encrypt
# # directory_url: staging # try a setup first without hitting rate limits
# Certificates are kept in <paths.data_dir>/acme and renewed on their own;
# the Server page shows their expiry, and an alert fires if renewal fails.
#
# Or your own certificate files (certbot or any other tool keeps them fresh;
# they are re-read when they change, and a corrupt renewal keeps serving the
# last good certificate):
# tls:
# cert_file: /etc/letsencrypt/live/tv.example.com/fullchain.pem
# key_file: /etc/letsencrypt/live/tv.example.com/privkey.pem
#
# rtmps_addr: ":1936" # RTMP over TLS, with the same certificate
# Reverse proxies in front of the server (IPs or CIDRs) whose X-Forwarded-For
# and X-Forwarded-Proto are believed: the client address for sign-in throttling
# and the audit log, and whether the browser used HTTPS. Empty = trust nobody.
# trusted_proxies: ["127.0.0.1/32"]
# The names viewers use, when HTTPS is handled by a proxy in front (Caddy,
# nginx) so this server has no certificate to tell them from. Needed only for
# a license issued for a domain.
# public_domains: [tv.example.com]
# People sign in to the dashboard with accounts (invite-only; the first start
# prints a one-time setup link for the owner). Passwords, API keys, the
# api.token and session cookies are only accepted over HTTPS, or over plain HTTP
# from this machine — see server.tls, trusted_proxies and
# auth.allow_insecure_login.
#
# Secrets can come from the environment instead of this file (env wins):
# UNDA_API_TOKEN, UNDA_SRT_PASSPHRASE, UNDA_STREAM_KEYS (comma-separated)
# each also as UNDA_<NAME>_FILE=/path (Docker/Kubernetes secrets pattern).
# If this file holds secrets, keep it chmod 600 — the server warns otherwise.
log:
level: info
# Legacy stream keys: each publishes under a stream of the SAME name, so the key
# is visible in every watch link. Optional — prefer channels (dashboard →
# Channels), whose secret key is separate from the public stream name.
streams:
keys:
- mykey
paths:
recordings_dir: ./data/recordings # default <data_dir>/recordings
hls_dir: ./data/hls
# media_dir: ./data/media # files that playout channels play (default <data_dir>/media)
# rewind_dir: ./data/hls/.rewind # streams' rewind windows and every stream's few HLS segments
# # (default <hls_dir>/.rewind): a window takes bitrate x minutes,
# # e.g. 1.1 GB for 30 min at 5 Mb/s
# The server's own state: the account database (unda.db), the license file
# and the first-run setup token. Keep it private to the server's user and back
# it up (`unda admin backup <file>` is safe while the server runs).
data_dir: ./data
rtmp:
publish_policy: reject # or "replace"
idle_timeout_seconds: 8
hls:
segment_duration_seconds: 2 # target segment duration; default 2
segment_count: 6 # sliding-window segment count; default 6, min 3
# Each HLS viewer gets a session id (?sid=) carried in its playlist and segment
# URLs, so one browser tab counts as one viewer. Behind a CDN that caches on the
# full URL, either make it ignore the query string or set this to true (viewers
# are then counted by address + User-Agent).
disable_session_ids: false
# Each stream is packaged for HLS from the moment it goes live, so a viewer
# opening a channel nobody is watching starts at once instead of waiting for a
# segment to be made. true packages a stream only once someone asks for it.
on_demand: false
# Segments are written to paths.rewind_dir (a few MB per stream, written at
# the stream's bitrate): about 0.3 MB of memory per stream instead of about
# 14 at 3 Mb/s. true keeps them in memory instead (no disk writes).
segments_in_memory: false
api:
# DEPRECATED: an owner-level bearer token for scripts and Prometheus, from
# before accounts existed. It has no two-factor sign-in, never expires and
# is "config token" in the audit log; while it is set the server logs a
# warning and the dashboard shows one to admins. Use API keys instead
# (dashboard → API keys): each has a name and a role and can be revoked.
# curl -H "Authorization: Bearer <token>" http://localhost:8080/api/v1/streams
# If you still use it, generate a strong one (openssl rand -hex 32), ideally via
# UNDA_API_TOKEN / UNDA_API_TOKEN_FILE instead of this file. The server
# refuses to start with the placeholder value below.
# token: "changeme-generate-a-strong-random-token"
auth:
# Accept passwords, API keys, the api.token and session cookies over plain
# HTTP from other machines. Off by default: any of them sent unencrypted can
# be read on the network. Only for a closed studio LAN; prefer HTTPS.
allow_insecure_login: false
license:
# The signed license file. Default: <paths.data_dir>/license.json, which is
# where the dashboard (License page) and "unda license install" write it.
# No license = evaluation mode for 30 days (2 live streams, 2 accounts besides
# the owner); after that new streams are refused until a license is installed.
# file: /var/lib/unda/state/license.json
srt:
# Optional. SRT ingest is disabled unless addr is set — a third way to
# get media in, alongside RTMP and (soon) whatever else, not a
# requirement. Publish with e.g.:
# ffmpeg ... -f mpegts "srt://SERVER:6000?streamid=publish:mykey"
# Viewers pull with: srt://SERVER:6000?streamid=read:mykey (no stream key needed;
# the passphrase below applies to viewers too). ":6000" listens on IPv4 and IPv6.
# addr: ":6000"
# latency_ms: 500 # SRT transmission latency; default 500
# passphrase: "" # if set (10-79 chars), publishers and viewers must encrypt with it
webrtc:
# WebRTC (license feature "webrtc"): about a second of delay.
# Publish (WHIP, OBS 30+: Service "WHIP"): https://SERVER/whip/<stream>, Bearer Token = the stream key
# Play (WHEP): https://SERVER/webrtc/<stream>, or the watch page's "Low latency" switch
# Keys and playback tokens are refused over plain HTTP (except from this machine).
# All media uses one UDP port: open it in the firewall (and publish it in Docker).
# disabled: false
# udp_port: 8189
# public_ips: [203.0.113.7] # behind 1:1 NAT (Docker's bridge, a cloud VM with a private address):
# # the public address(es) to announce; forward udp_port to this machine
# stun_servers: ["stun:stun.l.google.com:19302"] # or let the server find its public address itself
# ice_servers: # given to players and encoders whose network blocks UDP (a TURN relay)
# - urls: ["turn:turn.example.com:3478?transport=udp", "turns:turn.example.com:443?transport=tcp"]
# # username: unda # a fixed account, with credential / credential_env / credential_file
# turn_secret_env: UNDA_TURN_SECRET # or turn_secret / turn_secret_file: coturn's static-auth-secret;
# # each client gets its own credentials, valid turn_ttl_minutes (default 1440)
# # Limit coturn to relaying to this server (allowed-peer-ip), see the WebRTC guide
# max_publishers: 64 # WHIP sessions at once
# max_viewers: 500 # WHEP sessions at once
# max_audio_streams: 16 # streams whose sound is converted for WHEP viewers at once (one FFmpeg each)
record:
# Recording is started/stopped per stream via the API:
# curl -X POST -H "Authorization: Bearer <token>" \
# -d '{"action":"start"}' http://localhost:8080/api/v1/streams/mykey/record
# Files land in paths.recordings_dir/<stream>/<YYYY-MM-DD_HH-MM-SS>.ts
# (MPEG-TS: crash-safe by construction, playable while still being written).
split_duration_minutes: 60 # new file every ~N minutes, at the next keyframe; default 60
retention_days: 0 # delete recordings older than N days; 0 (default) = never delete
restream:
# Push live streams to external destinations (RTMP, RTMPS, SRT, UDP or RTP). Targets
# here start at boot; more can be added/removed live via
# POST /api/v1/streams/<stream>/push {"name":"yt","url":"rtmp://..."}
# GET /api/v1/streams/<stream>/push (state, redacted URL, last error)
# DELETE /api/v1/streams/<stream>/push/<id>
# A failing destination retries with backoff (5s, 10s, 30s, 60s cap) and
# never affects the others. The URL contains the destination's stream key:
# it is never logged or returned by the API (only a redacted form is), but
# it IS stored in plaintext in this file unless you use url_env / url_file
# (exactly one of url / url_env / url_file per target) — keep unda.yaml private.
# targets:
# - stream: mykey
# name: youtube
# url: rtmp://a.rtmp.youtube.com/live2/xxxx-xxxx-xxxx-xxxx
# - stream: mykey
# name: relay
# url: srt://relay.example.com:9000?streamid=publish:mykey&latency=800
# - stream: mykey # MPEG-TS over UDP: set-top boxes, hardware decoders, IPTV
# name: headend
# url: udp://192.168.1.60:6000 # unicast
# - stream: mykey
# name: lan-multicast
# url: udp://239.1.1.10:6000?ttl=4&iface=eth0 # multicast; pkt_size=1316 is the default
# - stream: mykey
# name: rtp-headend
# url: rtp://192.168.1.60:5004?pkt_size=1316 # MPEG-TS over RTP; operator only, no encryption
# - stream: mykey
# name: rist-partner
# url: rist://partner.example.com:5000?buffer=1000 # RIST Simple Profile: lost packets resent; even port
# - stream: mykey
# name: rist-encrypted
# url_env: UNDA_PUSH_RIST_URL # rist://host:port?secret=...&aes-type=256: Main Profile, encrypted, any port
# - stream: mykey
# name: twitch
# url_env: UNDA_PUSH_TWITCH_URL # or: url_file: /run/secrets/twitch_url
# - stream: mykey # broadcast TV: re-encoded for a DVB multiplexer,
# name: dtt-mux # IP modulator or satellite uplink (udp://, rtp://, srt://, rist:// or http-pull://)
# url: udp://239.1.1.20:5000?ttl=4
# broadcast: # 16:9, CBR MPEG-TS with DVB service information
# format: 576p25 # 576p25 (SD, default), 720p50 or 1080i25 (HD, interlaced)
# video_codec: h264 # h264 (default), hevc (576p25/720p50 only) or mpeg2 (576p25 only);
# # H.264-only DVB-T2/S2 networks can't show hevc or mpeg2
# mux_kbps: 1300 # constant total rate, null packets included: what the station takes.
# # Default 1300 for SD h264; otherwise the minimum safe value for the video
# # video_kbps: 960 # default: fitted to mux_kbps (960 in 1300 with AAC 128); else h264 8000 HD,
# # hevc 1200 / 4000, mpeg2 4000
# audio_codec: aac # aac (default; mp2 with mpeg2 video), mp2 or ac3 (Dolby Digital)
# audio_kbps: 128 # default 128 (aac) / 192 (mp2, ac3)
# # aac_packing: latm # adts (default, most receivers) or latm (DVB's LATM/LOAS, stream type 0x11)
# service_name: "Unda TV" # SDT channel name (default: the stream name)
# service_provider: "Unda" # SDT provider
# service_id: 1 # also: transport_stream_id, original_network_id,
# # pmt_pid: 4096 # pmt_pid, video_pid (256), audio_pid (257)
# # ad_cues: true # SCTE 35 ad cues for an ad inserter (start breaks on the stream page);
# # cue_pid: 258 by default
# - stream: mykey # broadcast TV the station's receiver pulls over HTTP from this server:
# name: cable-headend # https://<server>/tv/<token>.ts (the Restream tab shows the link)
# url: http-pull://?token=Ab3dEf6hIj9kLm2nOp5qRs&allow=198.51.100.0/24
# # token: 22 letters, digits, - or _ (without it the link changes at
# # every start); allow: who may pull; plain=1: plain HTTP too
# broadcast:
# service_name: "Unda TV"
limits:
# Concurrent connections per client IP, counted separately on the RTMP, HTTP
# and SRT listeners and for WebRTC sessions. Set it on any server reachable from the internet: left
# at 0 (unlimited, the built-in default) one client can hold as many
# connections as it likes. Raise it where many viewers share one address (a
# venue, an office); browsers over HTTPS use one connection each. Behind a
# reverse proxy (Caddy, nginx) every HTTP connection comes from the proxy,
# so either leave it at 0 there or let the proxy do its own per-IP limiting.
max_conns_per_ip: 100
# max_egress_mbps: 80 # once reached, NEW viewers are refused (503 / RTMP reject);
# # existing viewers are never throttled or dropped
# UDP / MPEG-TS (M13): plain UDP in and out, for LANs, IPTV headends, set-top boxes and
# hardware encoders. Use SRT over the internet: UDP has no retransmission, login or encryption.
# udp:
# timeout: 5s # a silent input goes offline after this, and resumes by itself
# jitter: 200ms # input delay line that smooths arrival timing
# recv_buffer_mb: 8 # socket receive buffer per input; Linux caps it at net.core.rmem_max (see README)
# loss_warn_percent: 0.1 # recent (60 s) packet loss that turns the dashboard yellow ...
# loss_critical_percent: 1 # ... and red
# inputs:
# - name: camera-1 # the stream key it publishes as
# listen: 0.0.0.0:5001
# allow: [192.168.1.20] # sender IPs or CIDRs; empty = anyone who can reach the port
# timeout: 5s
# - name: rtp-camera
# listen: 0.0.0.0:5004
# rtp: true # RFC 2250 MPEG-TS over RTP (payload type 33)
# allow: [192.168.1.20]
# # path2: "239.2.1.1:5004" # RTP only: the same stream a second way (SMPTE 2022-7)
# # interface2: eth2 # the interface to join the path2 group on
# - name: iptv-feed
# multicast: 239.1.1.1:5000 # IPv4 group to join
# interface: eth0
# outputs: # any stream out as MPEG-TS over UDP (these become restream targets)
# - stream: channel-1
# to: 192.168.1.60:6000
# - stream: channel-1
# multicast: 239.1.1.10:6000
# ttl: 4
# interface: eth0
# Pull inputs (license feature "pull"): streams Unda fetches from elsewhere and
# publishes under the input's name, one FFmpeg each (FFmpeg required). HLS
# (.m3u8; the highest-bitrate variant is taken), MPEG-TS or FLV over HTTP(S),
# RTMP(S) and RTSP(S). H.264 video is copied, audio that isn't AAC converted;
# other video only with transcode_video: true (about a core per HD channel). An
# address can hold a password or token: give it with url_env or url_file to
# keep it out of this file. They can also be made in the dashboard (Server ->
# Pull inputs).
# pull_inputs:
# - name: news
# url: https://example.com/live/news/index.m3u8
# - name: lobby-camera
# url_env: LOBBY_CAMERA_URL # e.g. rtsp://user:password@192.168.1.64/stream1
# rtsp_transport: tcp # or udp
# - name: partner-feed
# url_file: /etc/unda/partner-feed.url # e.g. http://iptv.example.com:8080/live/user/pass/123.ts
# # user_agent: "VLC/3.0.20" # some IPTV providers only serve certain players
# # timeout: 10s # how long the stream is held while the source reconnects
# # transcode_video: true # MPEG-2/HEVC sources
# # paused: true # listed, not pulled
# Multi-channel inputs (MPTS in): split a bundle of TV channels (a satellite
# receiver's or operator's multi-program transport stream) into separate streams.
# Programs in H.264 + AAC are split in-process; other audio is converted with
# FFmpeg; other video only with transcode_video: true (costly).
# mpts_inputs:
# - name: sat-bundle
# source: udp://239.1.1.1:5000 # or udp://:5000 (unicast), rtp://...
# # source: srt://ird.example.com:9000?streamid=sat&passphrase=... # connect to an SRT sender
# # source: srt://:9000?passphrase=... # or wait for one to connect (one at a time)
# # source: rist://@:5000?buffer=1000 # RIST Simple Profile receiver (even port; RTCP on the next)
# # source: rist://@:5001?secret=... # RIST Main Profile, encrypted with the sender's passphrase (any port)
# interface: eth0 # multicast: interface to join on
# # allow: [192.168.1.20] # optional: only these senders
# # discover_only: true # list the programs (API/dashboard), publish nothing
# programs:
# - program: 101 # program_number in the feed
# stream: sports-hd # the Unda stream it becomes
# # audio_language: eng # or audio_pid: 0x102; default the first audio
# # transcode_video: true # needed for MPEG-2/HEVC video (about a core per HD channel)
# - program: 102
# stream: news-24
# Multi-channel transport streams (MPTS): several streams as the programs (TV
# channels) of one constant-bitrate MPEG-TS, for an IPTV network, a DVB
# multiplexer or modulator, or a partner. Needs FFmpeg: each program is copied
# (bitrate = its share, above its source's peak) or re-encoded to SD with encode.
# mpts_outputs:
# - name: iptv-bundle-1
# destination: udp://239.2.2.1:6000 # or rtp://..., srt://host:port?streamid=..., rist://host:5000
# interface: eth0 # multicast: outgoing interface
# ttl: 4
# provider: "Unda Community TV" # SDT provider name
# network_id: 1
# transport_stream_id: 1
# total_bitrate: 25Mbps # constant, null packets included; omit = the programs' sum + headroom;
# # vbr = variable: no padding, only what the channels carry (IP networks)
# pat_pmt_interval: 100ms # sdt_interval defaults to 500ms (at most 2s)
# offline: black # while a stream is offline its channel shows: black (default), slate, none
# # slate_image: /etc/unda/slate.png # the picture for offline: slate
# # epg: false # playout channels carry now/next (DVB EIT) unless this is false
# programs:
# - stream: channel-1
# program: 1
# name: "Channel 1" # SDT service name
# pmt_pid: 0x100
# pids: { video: 0x101, audio: [0x102] }
# bitrate: 6Mbps # copied as it is: this program's constant share
# # ad_cues: true # SCTE 35 ad cues (cue_pid: automatic unless set)
# - stream: sports-hd
# program: 2
# name: "Sports HD"
# pids: auto # assigned without clashes
# encode: { format: 1080i25, video_codec: h264, audio_codec: ac3 } # re-encoded (see broadcast outputs)
# Adaptive-bitrate transcoding. Turn it on and choose the encoder (CPU or a GPU)
# in the dashboard: Server → Transcoding. Needs FFmpeg. The settings below are
# optional and advanced; enabled/hardware/max_concurrent here only seed a server
# that has never saved them from the dashboard.
# transcode:
# streams: ["vip"] # optional: keys transcoded automatically at publish ("*" = every stream)
# # ffmpeg_path: ffmpeg segment_seconds: 2
# # fps: 30 MAXIMUM output frame rate; each stream keeps its own (25, 30); 50 and 60 are halved
# # max_source_mbps: 60 refuse to transcode a source above this video bitrate (it is still served passthrough)
# # Rungs are ceilings: each stream gets rungs no bigger than itself, in its own aspect ratio,
# # at no more bitrate than it justifies (see the README).
# # renditions default to 2160p/1080p/720p/480p/360p/240p/144p; override with a list of
# # {name, width, height, video_kbps, audio_kbps} (even dimensions, name a-z0-9_-)
# Dashboard multiview stills (one small JPEG per stream, decoded from the latest
# keyframe by FFmpeg). On whenever FFmpeg is found; bounded to a couple of decodes at a time.
# thumbnails:
# disabled: false # true = no stills (tiles show names and stats only)
# ffmpeg_path: ffmpeg # default: transcode.ffmpeg_path, else ffmpeg from PATH
# max_concurrent: 2 # simultaneous decodes (1-16)
monitoring:
# /metrics (Prometheus) and /debug/pprof/* are served behind the API token —
# metrics carry live stream keys, which are publish credentials.
# disable_pprof: true # remove /debug/pprof/* entirely
# Every live channel's sound is measured (Multiview meters, the "no sound"
# alarm) when FFmpeg is found: about 0.5% of a CPU core and 15 MB a channel.
# silence_alarm_seconds: 10 # silent this long = "no sound" (3-3600)
# disable_audio_levels: true # no meters, no alarm
# Every live channel's picture is checked from its keyframes every few
# seconds (the "picture frozen" and "picture black" alarms) when FFmpeg is
# found: one small decoder a channel.
# frozen_alarm_seconds: 20 # not moving this long = "picture frozen" (5-3600)
# black_alarm_seconds: 10 # black this long = "picture black" (3-3600)
# disable_picture_check: true # no picture alarms
# Webhook alerts (M10). Nothing is sent unless at least one webhook is set.
# alerts:
# min_input_kbps: 1500 # alert if an input stays below this; 0 (default) = no bitrate alerts
# bitrate_grace_seconds: 20 # how long it must stay low; also the warm-up after a stream starts
# cooldown_seconds: 60 # min gap between identical alerts for one stream
# events: [stream_down, stream_up, bitrate_low, bitrate_recovered] # default: all four
# webhooks:
# - name: ops
# url_env: UNDA_ALERT_OPS_URL # exactly one of url / url_env / url_file;
# # Slack/Discord URLs are secrets — keep them out of this file
# Twice a day, read the vendor's latest release description and tell owners
# and admins on the dashboard when a newer version exists. Nothing is
# downloaded or installed; the request carries only this server's version.
# updates:
# disabled: true
# Viewer sessions (the dashboard's Sessions page): each player watching a
# stream over HLS, DASH, RTMP or SRT, with its address, player, start, length
# and traffic. Kept in the database; only operators and above can see them.
# sessions:
# retention_days: 30 # 0 = 30, at most 365
# disabled: true # keep no history on disk (viewers watching now are still shown)
# Daily database copies: once a day, while it runs, the server copies its
# database (accounts, channels and their keys, playlists, inputs, settings)
# to <data_dir>/backups/daily-<date>.db. They stay on this disk: copy that
# folder and license.json to another machine (see the install guide).
# backups:
# keep_days: 7 # 0 = 7, at most 365
# disabled: true # take no daily copies (another backup covers the database)
# 24/7 channels (playout). Each channel plays the files of a schedule, fills
# the gaps, and lets a live stream take over while it is live. It is a stream
# like any other: watch /watch/<name>, restream it, record it. Every item is
# re-encoded to one picture size, so each channel runs an encoder all the
# time: about half to two thirds of a CPU core at 720p30 with libx264.
# Needs FFmpeg (and ffprobe next to it) and the "playout" license feature.
# playout:
# asrun_retention_days: 90 # the as-run log; -1 keeps it forever
# media_max_gb: 0 # cap on the media folder for uploads (0: none; 5 GB of disk is always left free)
# channels:
# - name: tv1 # the stream's name
# schedule: /etc/unda/schedules/tv1.yaml
# live: studio # optional: this stream takes over while live
# # width: 1280, height: 720, fps: 30 (25/30/50/60), video_kbps: 3000, audio_kbps: 128
# filler: image # black (default), image or video (looped)
# filler_file: /etc/unda/slate.png # image: a picture file; video: a file in media_dir