Content origination subsystem. Generates live HLS video with signed TLTV
protocol endpoints — a fully self-contained origin server in a single binary.
Currently the only subcommand is test; future subcommands (image, loop)
will add other source types.
Generates a live SMPTE EG 1-1990 color bar test pattern with TLTV branding, channel name, wall clock (or uptime counter), and a continuous 1 kHz audio tone. Pure Go H.264 Baseline encoder (I_16x16 + I_4x4 prediction, CAVLC), AAC-LC audio (48 kHz mono), MPEG-TS muxer, and HLS segmenter — no ffmpeg required.
# Minimal — ephemeral key, 640x360, port 8000
tltv server test
# Named channel with persistent key
tltv server test -k channel.key --name "TLTV Test"
# 1080p at 30fps with custom QP
tltv server test -k channel.key --name "My Channel" --width 1920 --height 1080 --qp 22
# With TLS (auto Let's Encrypt), viewer, and cache
tltv server test -k channel.key --name "Test" --hostname test.example.com \
--tls --viewer --cache
# Docker
docker run -v keys:/data -p 8000:8000 \
-e KEY=channel.key -e NAME="Docker Test" -e HOSTNAME=test.example.com \
tltv server test| Flag | Env | Default | Description |
|---|---|---|---|
-k, --key |
KEY |
(ephemeral) | Ed25519 seed file (hex). Auto-generated if omitted. |
--channels |
CHANNELS |
1 | Number of channels to generate. Each gets its own key, test pattern label, and protocol endpoints. Channel 1 uses --key; channels 2+ auto-generate to ch2.key, ch3.key, etc. |
| Flag | Env | Default | Description |
|---|---|---|---|
--description |
DESCRIPTION |
— | Channel description (included in signed metadata) |
--tags |
TAGS |
— | Comma-separated tags, max 5 (e.g. test,experimental) |
--language |
LANGUAGE |
— | ISO 639-1 language code (e.g. en, ja) |
--icon |
ICON |
(TLTV logo SVG) | Path to icon file (PNG, JPEG, or SVG). Default: built-in TLTV logo. |
| Flag | Env | Default | Description |
|---|---|---|---|
--access |
ACCESS |
public |
Access mode: public or token |
--token |
TOKEN |
— | Access token for private channels (implies --access token) |
--on-demand |
ON_DEMAND=1 |
off | Mark channel as on-demand |
| Flag | Env | Default | Description |
|---|---|---|---|
-n, --name |
NAME |
TLTV |
Channel name shown on test screen |
--uptime |
UPTIME=1 |
off | Show elapsed time instead of wall clock |
--timezone |
TIMEZONE |
UTC | IANA timezone for clock (e.g. America/New_York) |
--font-scale |
FONT_SCALE |
0 (auto) | Text scale factor. Auto: height/120 for SD, height/135 for HD+ |
| Flag | Env | Default | Description |
|---|---|---|---|
--variants |
VARIANTS |
— | Comma-separated renditions (e.g. 1080p,720p,360p). When set, stream.m3u8 returns a master playlist. Presets: 240p–4320p. |
--width |
WIDTH |
640 | Video width (16–7680). Non-16-aligned values rounded up internally. |
--height |
HEIGHT |
360 | Video height (16–4320). Non-16-aligned values rounded up internally. |
--fps |
FPS |
30 | Frames per second |
--qp |
QP |
26 | Quantization parameter (0–51). Lower = better quality, more bits. |
| Flag | Env | Default | Description |
|---|---|---|---|
--segment-duration |
SEGMENT_DURATION |
2 | Segment duration in seconds |
--segment-count |
SEGMENT_COUNT |
5 | Sliding window size (number of segments in playlist) |
| Flag | Env | Default | Description |
|---|---|---|---|
-l, --listen |
LISTEN |
:8000 |
Listen address. Changes to :443 when --tls is enabled. |
-H, --hostname |
HOSTNAME |
(auto) | Public host:port for the origins field in metadata. Omit to create a private origin that relays cannot discover. |
| Flag | Env | Default | Description |
|---|---|---|---|
--tls |
TLS=1 |
off | Enable TLS. Uses ACME (Let's Encrypt) if no cert/key provided. |
--tls-cert |
TLS_CERT |
— | PEM certificate file (manual TLS) |
--tls-key |
TLS_KEY |
— | PEM private key file (manual TLS) |
--tls-staging |
TLS_STAGING=1 |
off | Use Let's Encrypt staging directory |
--acme-email |
ACME_EMAIL |
— | Email for ACME account (optional) |
ACME certs are stored in ./certs/ (override with TLS_DIR). Renewal runs
hourly in the background; certs renew when <30 days remain.
| Flag | Env | Default | Description |
|---|---|---|---|
--cache |
CACHE=1 |
off | Enable in-memory response cache with singleflight dedup |
--cache-max-entries |
CACHE_MAX_ENTRIES |
100 | Max cached items (LRU eviction) |
--cache-stats |
CACHE_STATS |
0 | Log cache stats every N seconds (0 = off) |
| Flag | Env | Default | Description |
|---|---|---|---|
-P, --peers |
PEERS |
— | Comma-separated tltv:// URIs to advertise in peer exchange |
-g, --gossip |
GOSSIP=1 |
off | Include validated gossip-discovered channels in peers response |
| Flag | Env | Default | Description |
|---|---|---|---|
--viewer |
VIEWER=1 |
off | Serve production web player at / (private channels require /?token=...) |
--debug-viewer |
DEBUG_VIEWER=1 |
off | Serve diagnostic debug viewer at / (mutually exclusive with --viewer) |
--viewer-title |
VIEWER_TITLE |
(empty) | Nav bar label text |
--no-viewer-footer |
VIEWER_FOOTER=0 |
on | Hide footer links |
See docs/viewer.md for details on viewer modes and display config.
| Flag | Env | Default | Description |
|---|---|---|---|
--log-level |
LOG_LEVEL |
info |
debug, info, or error |
--log-format |
LOG_FORMAT |
human |
human (timestamped) or json (one object per line) |
--log-file |
LOG_FILE |
stderr | Log to file instead of stderr |
| Flag | Env | Default | Description |
|---|---|---|---|
--config |
CONFIG |
— | JSON config file (fields map 1:1 to flags, underscores) |
--dump-config |
— | — | Print resolved config as JSON and exit |
Config fields map 1:1 to CLI flags (underscores instead of dashes). CLI flags override config values; config overrides env vars.
{
"key": "channel.key",
"name": "My Test Channel",
"width": 1920,
"height": 1080,
"fps": 30,
"qp": 22,
"listen": ":8000",
"hostname": "test.example.com:443",
"segment_duration": 2,
"segment_count": 5,
"tls": true,
"viewer": true,
"cache": true,
"log_level": "info",
"guide": {
"entries": [
{
"title": "Test Signal",
"start": "2026-01-01T00:00:00Z",
"end": "2026-12-31T23:59:59Z"
}
]
}
}The guide field accepts three forms:
- Inline entries:
{"entries": [...]}— define a custom guide schedule - File reference:
"guide.json"— path to a guide JSON file - Omitted/null: uses the default ephemeral guide (midnight-to-midnight UTC, auto-renewed)
Frame caching. The display changes once per second (clock tick). At 30 fps the encoder caches the NAL data and only re-renders when the time string changes — a 30x reduction in encoding work.
Config hot-reload. The server watches the config file by mtime (no inotify,
30-second check interval). name and guide are reloadable without restart;
encoder settings (width, height, fps, qp), key, and listen require
a restart.
Ephemeral guide. When no guide is provided, the server generates a midnight-to-midnight UTC guide entry automatically. It re-signs every 5 minutes so the guide rolls over at midnight.
Protocol endpoints. The server exposes the full TLTV v1 protocol surface:
/.well-known/tltv, metadata, guide, stream (HLS), and peers. Documents are
re-signed every 5 minutes.
No shortcut paths. The server does NOT serve /stream.m3u8 or /seg0.ts
convenience paths. All stream access goes through protocol paths
(/tltv/v1/channels/{id}/stream.m3u8).
Docker timezone. The binary embeds time/tzdata so --timezone works in
scratch containers without filesystem timezone data.
HOSTNAME in Docker. Docker sets HOSTNAME to the container ID by default.
Always set it explicitly (-e HOSTNAME=public.example.com) or the metadata
origins field will contain the container ID.
Private origins. Omit --hostname to run a private origin. Without a
hostname, the signed metadata contains no origins field, so relays and peers
cannot discover or advertise the server's address. Viewers must be given the
address directly.
Private viewer auth. When --viewer is enabled on a private channel, the
embedded viewer root (/) and /api/info require the same ?token=... used by
the protocol endpoints. The viewer JSON never echoes the raw secret back to the
browser.
Private HLS manifests. For access=token, generated server playlists
(stream.m3u8, master playlists, variant playlists, audio playlists, subtitle
playlists) append ?token=... to every child URI so playback continues after
the first manifest fetch.
Resolution rounding. Non-16-aligned dimensions are rounded up internally (H.264 macroblock alignment). The SPS includes frame cropping to report the requested resolution to decoders.
Audio. 1 kHz AAC-LC tone (48 kHz, mono). Pre-encoded 48-frame ADTS loop (~1 second, 8.5 KB) cycled for each segment. Audio PTS is continuous across segment boundaries — no gaps.