-
Notifications
You must be signed in to change notification settings - Fork 17
Examples and Recipes
End-to-end setups for real deployments. Each recipe is a tinyice.json fragment
(merge the keys into your single config file) plus the commands to drive and
verify it. For the meaning of every key, see Configuration; for the
mechanics behind each feature, follow the "See also" links.
Apply config changes with ./tinyice reload (or kill -HUP <pid>) — no restart,
no dropped listeners.
Stations — 24/7 automated radio · Live DJ with AutoDJ fallback · Multiple stations on one box · Talk / podcast station Video — OBS to HLS with a website embed · Low-latency WebRTC · Adaptive bitrate ladder Delivery & scale — Mobile-friendly transcodes · Edge relay / scale-out Integrations — Now playing everywhere Deployment — Docker Compose · systemd in production · Behind Cloudflare / a reverse proxy · Prometheus + Grafana · Migrating from Icecast
A station that plays a music library around the clock, shuffles, injects track metadata, lists publicly, and announces each song to a Discord channel.
{
"page_title": "Nightwave Radio",
"page_subtitle": "Synthwave, all night",
"base_url": "https://radio.example.com",
"autodjs": [
{
"name": "Nightwave",
"mount": "/nightwave",
"music_dir": "/music/synthwave",
"format": "mp3",
"bitrate": 128,
"enabled": true,
"loop": true,
"inject_metadata": true,
"visible": true
}
],
"webhooks": [
{
"name": "Discord now-playing",
"url": "https://discord.com/api/webhooks/<id>/<token>",
"method": "POST",
"events": ["now_playing"],
"body_template": "{\"content\": \":notes: **{{.Artist}} – {{.Title}}**{{if .MountURL}} · [Listen]({{.MountURL}}){{end}}\"}",
"enabled": true
}
]
}Listen at https://radio.example.com/nightwave or
/player/nightwave. Set base_url so the webhook can build the Listen link.
A live mount for a human DJ that automatically falls back to a 24/7 AutoDJ when the DJ disconnects — listeners stay connected through the gap.
{
"autodjs": [
{ "name": "Auto", "mount": "/auto", "music_dir": "/music/rotation",
"format": "mp3", "bitrate": 128, "enabled": true, "loop": true,
"inject_metadata": true, "visible": false }
],
"fallback_mounts": { "/live": "/auto" }
}The DJ connects an Icecast/BUTT source to /live (mount password = the DJ's
source password). While /live has no source, listeners are served /auto;
when the DJ goes live, /live takes over. Keep the fallback mount visible: false so only /live is advertised.
See also: Streaming Sources · Configuration → Mounts.
Several independent stations from one process — each its own mount, library, and format.
{
"autodjs": [
{ "name": "Chill", "mount": "/chill", "music_dir": "/music/chill",
"format": "mp3", "bitrate": 128, "enabled": true, "loop": true, "inject_metadata": true, "visible": true },
{ "name": "Jazz", "mount": "/jazz", "music_dir": "/music/jazz",
"format": "opus", "bitrate": 96, "enabled": true, "loop": true, "inject_metadata": true, "visible": true },
{ "name": "Ambient", "mount": "/ambient", "music_dir": "/music/ambient",
"format": "mp3", "bitrate": 96, "enabled": true, "loop": true, "inject_metadata": true, "visible": true }
]
}All three appear on /explore. Raise max_listeners to your bandwidth budget
(Deployment#sizing).
Speech content benefits from Opus tuned for voice and a lower bitrate. Use a
transcoder (or AutoDJ format: opus) with the VoIP application profile:
{
"transcoders": [
{
"name": "talk-voip",
"input_mount": "/talk-src",
"output_mount": "/talk",
"format": "opus",
"bitrate": 48,
"opus_application": "voip",
"channels": 1,
"enabled": true
}
]
}Feed /talk-src from your live source; listeners use /talk.
See also: Transcoding#opus-tuning.
Stream video from OBS and embed the player on any web page.
- Enable RTMP and (optionally) set a public URL:
{ "base_url": "https://radio.example.com", "ingest": { "rtmp_enabled": true, "rtmp_port": "1935" } } - Create the mount
/livein Admin → Streams; its source password is the OBS Stream Key. - OBS → Stream (Custom): Server
rtmp://radio.example.com/live, Stream Key = that password. Output: x264 + AAC, 1 s keyframe interval. - Embed it:
<iframe src="https://radio.example.com/embed/live" width="100%" height="360" frameborder="0" allow="autoplay; fullscreen"></iframe>
Direct HLS for external players: https://radio.example.com/live/playlist.m3u8.
See also: Streaming Sources#video-from-obs-rtmp · Playback and Output.
Sub-second playback for IRL/event streams. The encoder must publish without B-frames.
-
OBS → Output (Advanced) → x264 options: add
bf=0(or set Profile tobaseline). Keep RTMP ingest as above. -
Watch: open
https://radio.example.com/player/live?webrtc=1, or POST an SDP offer tohttps://radio.example.com/live/whep(Content-Type: application/sdp).
HLS remains available on the same mount as the robust fallback.
See also: Playback and Output#webrtc-playback-whep.
Let players pick a rendition. Run one OBS output per rendition to its own mount, then group them into a multivariant playlist.
{
"ingest": { "rtmp_enabled": true, "rtmp_port": "1935" },
"variant_groups": { "/live": ["/live", "/live_720", "/live_480"] }
}In OBS use the "Multiple Outputs" plugin (or three OBS instances) publishing to
/live (source quality), /live_720, and /live_480. Viewers open
/player/live; the player loads /live/master.m3u8 and hls.js does ABR.
BANDWIDTH / RESOLUTION are derived from each member's live ingest metrics.
See also: Playback and Output#obs-simulcast-abr-ladder.
Publish one good source, serve cheaper renditions automatically.
{
"transcoders": [
{ "name": "live-opus-64", "input_mount": "/live", "output_mount": "/live-opus",
"format": "opus", "bitrate": 64, "enabled": true }
],
"auto_transcode_mp3_bitrates": [128, 64]
}The transcoder gives you a 64 kbps Opus mount at /live-opus. The
auto_transcode_mp3_bitrates list spawns ephemeral MP3 mounts
(/live-mp3-128, /live-mp3-64) for any non-MP3 source the moment it connects —
handy when you ingest Ogg/Opus but want MP3 fallbacks without declaring each one.
See also: Transcoding.
Put a TinyIce node close to listeners and have it pull from your origin. Each edge serves its own listeners; the origin sees one connection per edge.
{
"relays": [
{ "url": "https://origin.example.com/nightwave", "mount": "/nightwave",
"password": "", "burst_size": 65536, "enabled": true }
]
}Run this config on each edge box (different region/host). ICY "now playing" metadata is carried through. Relay chaining works; shared cluster state does not — scale each node to its bandwidth ceiling and add edges (Architecture#whats-intentionally-not-here).
See also: Streaming Sources#pulling-a-relay.
Announce track changes to chat platforms and a directory at once: webhooks for
HTTP targets, on_play_command for a local script (e.g. TuneIn AIR).
{
"webhooks": [
{ "name": "Telegram", "url": "https://api.telegram.org/bot<token>/sendMessage",
"method": "POST", "content_type": "application/json", "events": ["now_playing"],
"body_template": "{\"chat_id\":\"<chat>\",\"text\":\"Now playing: {{.Artist}} – {{.Title}}\"}",
"enabled": true }
],
"autodjs": [
{ "name": "Nightwave", "mount": "/nightwave", "music_dir": "/music/synthwave",
"format": "mp3", "bitrate": 128, "enabled": true, "loop": true,
"inject_metadata": true, "visible": true,
"on_play_command": "/opt/tinyice/tunein.sh", "on_play_command_timeout": 10 }
]
}/opt/tinyice/tunein.sh gets the track in environment variables:
#!/bin/bash
curl -s "https://air.radiotime.com/Playing.ashx?partnerId=$P&partnerKey=$K&id=$ID" \
--data-urlencode "title=${TINYICE_TITLE}" \
--data-urlencode "artist=${TINYICE_ARTIST}"Available env vars: TINYICE_ARTIST, TINYICE_TITLE, TINYICE_ALBUM,
TINYICE_FILE, TINYICE_MOUNT. The editor ships presets for Discord, Slack,
Teams, ntfy, Pushover, TuneIn AIR, and more.
See also: Webhooks · AutoDJ#track-hooks.
The published image listens on 8080 inside the container and reads its
config from /data/config.json, so map the port host:8080 and mount a
volume at /data.
services:
tinyice:
image: ghcr.io/datanoisetv/tinyice:latest
restart: unless-stopped
ports:
- "8000:8080" # http / Icecast (reach it on http://host:8000)
- "1935:1935" # RTMP (only if you ingest video)
- "9000:9000/udp" # SRT (only if you ingest SRT)
volumes:
- tinyice-data:/data # config.json, history.db, playlists, ACME certs
volumes:
tinyice-data:docker compose up -d, then read the generated admin password from
docker compose logs tinyice. Edit /data/config.json (in the volume) for the
recipes above and docker compose exec tinyice tinyice reload.
For direct ACME on 80/443, the binary or
.deb/.rpmon the host is simpler — see systemd in production. To do it in a container, publish80:80/443:443, override the command with-port 80 -https-port 443 -config /data/config.json, and setuse_https/auto_https/domainsin the config.
See also: Installation#docker-ghcr-multi-arch.
Install the package, then bring the service up deliberately.
sudo dpkg -i tinyice_*.deb # or: sudo rpm -i tinyice-*.rpm
sudo systemctl unmask tinyice # the unit ships masked on purpose
sudo systemctl enable --now tinyice
sudo journalctl -u tinyice | grep -A4 "FIRST RUN" # generated credentialsConfig lives at /etc/tinyice/tinyice.json, state in /var/lib/tinyice. The
binary is granted cap_net_bind_service, so a direct-ACME config can bind
80/443 with no proxy:
{ "use_https": true, "auto_https": true, "port": "80", "https_port": "443",
"domains": ["radio.example.com"], "acme_email": "admin@example.com" }sudo systemctl reload tinyice # apply config (SIGHUP)See also: Deployment#systemd · Deployment#auto-https-acme.
A proxy is optional — TinyIce does its own TLS. If you put one in front anyway (WAF, shared edge, path routing), declare it as trusted and turn off response buffering.
{ "trusted_proxies": ["127.0.0.1", "10.0.0.0/8", "173.245.48.0/20"] }nginx location for the stream/HLS/SSE paths:
location / {
proxy_pass http://127.0.0.1:8000;
proxy_http_version 1.1;
proxy_buffering off; # streaming + SSE must not be buffered
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_read_timeout 3600s;
}With trusted_proxies set, loopback stops being auto-whitelisted and bans/scan
detection see real client IPs. Get the list wrong and either bans break or
everyone looks trusted.
See also: Security#behind-a-reverse-proxy · Deployment#do-i-need-a-reverse-proxy.
Metrics and pprof are served on the internal port :8081 (firewall it).
# prometheus.yml
scrape_configs:
- job_name: tinyice
static_configs:
- targets: ["10.0.0.10:8081"]Import monitoring/grafana-dashboard.json
into Grafana. In Docker, scrape over the compose network or publish
127.0.0.1:8081:8081 — don't expose :8081 publicly.
See also: Observability.
TinyIce speaks the Icecast 2 source and listener protocols, so existing encoders and players keep working:
- Point your encoder (BUTT, Mixxx, ffmpeg, hardware) at TinyIce: same
host:port, same mount, the mount's source password. No encoder change. - Listeners keep their
http://host:8000/<mount>URLs and.m3u/.plsplaylist links. -
/status-json.xslkeeps Icecast-compatible stats tooling working. - Then adopt the extras incrementally — HLS at
/<mount>/playlist.m3u8, an AutoDJ, transcodes, a built-in player, ACME HTTPS.
See also: Streaming Sources · Playback and Output.
Looking for the reference behind a setting? Configuration. Stuck? Troubleshooting and FAQ.
Repository · Releases · Issues · Security policy · Apache-2.0
Getting started
Streaming
Integrations
Operations
Internals
Help