Skip to content

Examples and Recipes

DatanoiseTV edited this page Jun 18, 2026 · 1 revision

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.

Stations24/7 automated radio · Live DJ with AutoDJ fallback · Multiple stations on one box · Talk / podcast station VideoOBS to HLS with a website embed · Low-latency WebRTC · Adaptive bitrate ladder Delivery & scaleMobile-friendly transcodes · Edge relay / scale-out IntegrationsNow playing everywhere DeploymentDocker Compose · systemd in production · Behind Cloudflare / a reverse proxy · Prometheus + Grafana · Migrating from Icecast


24/7 automated radio

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.

See also: AutoDJ · Webhooks.

Live DJ with AutoDJ fallback

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.

Multiple stations on one box

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).

Talk / podcast station

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.


OBS to HLS with a website embed

Stream video from OBS and embed the player on any web page.

  1. Enable RTMP and (optionally) set a public URL:
    { "base_url": "https://radio.example.com",
      "ingest": { "rtmp_enabled": true, "rtmp_port": "1935" } }
  2. Create the mount /live in Admin → Streams; its source password is the OBS Stream Key.
  3. OBS → Stream (Custom): Server rtmp://radio.example.com/live, Stream Key = that password. Output: x264 + AAC, 1 s keyframe interval.
  4. 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.

Low-latency WebRTC (WHEP)

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 to baseline). Keep RTMP ingest as above.
  • Watch: open https://radio.example.com/player/live?webrtc=1, or POST an SDP offer to https://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.

Adaptive bitrate ladder

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.


Mobile-friendly transcodes

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.

Edge relay / scale-out

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.


Now playing everywhere

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.


Docker Compose

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/.rpm on the host is simpler — see systemd in production. To do it in a container, publish 80:80/443:443, override the command with -port 80 -https-port 443 -config /data/config.json, and set use_https/auto_https/domains in the config.

See also: Installation#docker-ghcr-multi-arch.

systemd in production

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 credentials

Config 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.

Behind Cloudflare / a reverse proxy

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.

Prometheus + Grafana

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.

Migrating from Icecast

TinyIce speaks the Icecast 2 source and listener protocols, so existing encoders and players keep working:

  1. Point your encoder (BUTT, Mixxx, ffmpeg, hardware) at TinyIce: same host:port, same mount, the mount's source password. No encoder change.
  2. Listeners keep their http://host:8000/<mount> URLs and .m3u / .pls playlist links.
  3. /status-json.xsl keeps Icecast-compatible stats tooling working.
  4. 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.

Clone this wiki locally