Serve live HLS (and optional MPEG-DASH) from a raw PCM s16le audio stream
README.md

hlsd #

Release FlakeHub

A small, self-contained daemon that takes a raw PCM s16le audio stream and serves it live as HLS — and optionally MPEG-DASH — over HTTP.

Audio is read from stdin, a FIFO, or a Unix socket, encoded, packaged into fragmented-MP4 (CMAF) segments, and published behind an actix-web server with a rolling live window.

The default build is pure Rust with no system dependencies (FLAC via flacenc). AAC, MP3, and Opus are available as opt-in Cargo features that compile vendored C sources — they need a C compiler at build time but no pre-installed system library. Works on Linux and macOS.

Table of Contents #

Features #

  • Input from stdin, FIFO, or Unix domain socket
  • macOS: ships a virtual audio output device ("hlsd Audio") — select it in the Sound menu and stream whatever the system plays (--input virtual)
  • HLS (fMP4, #EXT-X-VERSION:7) and optional MPEG-DASH from the same segments
  • Codecs: FLAC (default, pure Rust), AAC-LC, MP3, Opus (feature-gated)
  • Configurable via TOML and/or CLI flags (CLI overrides the file)
  • Live sliding-window playlist with automatic old-segment eviction
  • Correct MIME types, permissive CORS, atomic manifest writes

Install #

Prebuilt binaries are published for Linux and macOS (x86_64 and arm64) plus FreeBSD/NetBSD (amd64) on every tagged release. All prebuilt binaries ship with the extra codecs enabled (NetBSD: AAC + MP3 only).

Debian / Ubuntu (apt) #

Packages are hosted on Gemfury:

echo "deb [trusted=yes] https://apt.fury.io/tsiry/ /" \
  | sudo tee /etc/apt/sources.list.d/hlsd.list
sudo apt-get update
sudo apt-get install hlsd

Fedora / RHEL (dnf) #

sudo tee /etc/yum.repos.d/hlsd.repo >/dev/null <<'EOF'
[fury]
name=Gemfury Private Repo
baseurl=https://yum.fury.io/tsiry/
enabled=1
gpgcheck=0
EOF
sudo dnf install hlsd

Nix #

# From the flake (installs into your profile)
nix profile install github:tsirysndr/hlsd

# Or run without installing
nix run github:tsirysndr/hlsd -- --help

Homebrew #

brew install tsirysndr/tap/hlsd

npm #

The @tsiry/hlsd package downloads the prebuilt binary for your platform from GitHub Releases (sha256 verified) on install:

npm install -g @tsiry/hlsd

# or run without installing
npx @tsiry/hlsd --help

Download from GitHub Releases #

Grab a tarball for your platform from the releases page:

# Example: Linux x86_64 — pick the asset matching your OS/arch
curl -fsSL -o hlsd.tar.gz \
  https://github.com/tsirysndr/hlsd/releases/latest/download/hlsd-<version>-linux-amd64.tar.gz
tar -xzf hlsd.tar.gz
sudo install -m 0755 hlsd /usr/local/bin/hlsd

Build from source #

# Default: pure Rust, FLAC only, no system deps
cargo build --release

# With extra codecs (compiles vendored C — needs a C compiler, no system libs)
cargo build --release --features aac
cargo build --release --features mp3
cargo build --release --features opus
cargo build --release --features all-codecs

The binary is target/release/hlsd.

Quick start #

Pipe any PCM s16le / 44.1 kHz / stereo source into hlsd over stdin:

# Example producer (any tool that emits raw PCM works):
some-audio-source | ./target/release/hlsd --out-dir ./stream --dash

Then open:

  • HLS media playlist: http://127.0.0.1:8080/stream.m3u8
  • HLS multivariant: http://127.0.0.1:8080/master.m3u8
  • MPEG-DASH manifest: http://127.0.0.1:8080/stream.mpd (with --dash)
  • Landing page: http://127.0.0.1:8080/

Feeding hlsd with ffmpeg #

hlsd doesn't use ffmpeg internally, but ffmpeg is a handy way to produce the raw PCM. Emit s16le, 44100 Hz, 2 channels to stdout (pipe:1) and pipe it into hlsd's stdin — the two ffmpeg output flags must match hlsd's --sample-rate / --channels:

# Stream a file (looped, in real time) as live HLS + DASH
ffmpeg -re -stream_loop -1 -i input.mp3 \
    -f s16le -ar 44100 -ac 2 pipe:1 \
  | ./target/release/hlsd --out-dir ./stream --dash
# Capture a live source (e.g. an internet radio) and republish it
ffmpeg -re -i https://example.com/stream.aac \
    -f s16le -ar 44100 -ac 2 pipe:1 \
  | ./target/release/hlsd --out-dir ./stream
# Generate a 440 Hz test tone — useful for a quick smoke check
ffmpeg -f lavfi -i "sine=frequency=440:sample_rate=44100" \
    -ac 2 -f s16le pipe:1 \
  | ./target/release/hlsd --out-dir ./stream

For Opus, produce 48 kHz PCM and tell hlsd to match:

ffmpeg -re -i input.wav -f s16le -ar 48000 -ac 2 pipe:1 \
  | ./target/release/hlsd --codec opus --sample-rate 48000 --out-dir ./stream

-re makes ffmpeg read/emit at real-time (wall-clock) speed, which is what you want for live output. Drop it to transcode as fast as possible.

Playing the stream #

ffplay is a player, so it consumes hlsd's HTTP output — it does not feed hlsd's stdin (use ffmpeg for that).

HLS plays in ffplay out of the box:

ffplay -autoexit http://127.0.0.1:8080/stream.m3u8
mpv http://127.0.0.1:8080/stream.m3u8

DASH needs a DASH-capable player. Note that ffmpeg/ffplay can only read .mpd when built --enable-libxml2; many builds (e.g. Homebrew's) omit it and report Invalid data found when processing input. Check with ffprobe -demuxers | grep dash — if there's no dash demuxer, use a browser player (dash.js / Shaka Player), VLC, or a libxml2-enabled ffmpeg:

ffplay http://127.0.0.1:8080/stream.mpd   # only with a libxml2-enabled ffmpeg

Safari plays HLS natively, and the built-in landing page at http://127.0.0.1:8080/ links to the manifests.

Input from a FIFO #

mkfifo /tmp/pcm.fifo
./target/release/hlsd --input fifo --input-path /tmp/pcm.fifo &
some-audio-source > /tmp/pcm.fifo

Input from a Unix socket #

hlsd binds the socket and waits for a producer to connect:

./target/release/hlsd --input unix --input-path /tmp/pcm.sock &
some-audio-source | nc -U /tmp/pcm.sock

macOS: stream system audio (virtual device) #

hlsd can register a virtual audio output device with macOS. Once installed, "hlsd Audio" shows up in the sound output menu (System Settings → Sound, and the menu-bar volume control) like any other speaker. Everything the system plays to it is looped back into hlsd and served as HLS/DASH — no ffmpeg, no third-party loopback driver.

Under the hood this is a CoreAudio AudioServerPlugIn (the same mechanism BlackHole uses), written in pure Rust (driver/src/lib.rs, a dependency-free cdylib), compiled at build time and embedded in the hlsd binary. Installing copies the bundle to /Library/Audio/Plug-Ins/HAL/hlsd.driver, ad-hoc signs it, and restarts coreaudiod — that's why it needs sudo:

sudo hlsd driver install     # register the device (one-time)
hlsd driver status           # check bundle + device registration
sudo hlsd driver uninstall   # remove it again

Then:

  1. Pick hlsd Audio as the output device in System Settings → Sound (or option-click the menu-bar volume icon).
  2. Start hlsd capturing from it:
hlsd --input virtual --sample-rate 48000
  1. Play any audio on the Mac and open http://127.0.0.1:8080/stream.m3u8.

Notes:

  • The device is stereo; --input virtual supports 44.1, 48, 88.2 and 96 kHz (hlsd switches the device to the requested rate automatically).
  • The system volume/mute keys work and are applied to the captured signal.
  • While "hlsd Audio" is the selected output you won't hear the audio on your speakers. To listen locally and stream, create a Multi-Output Device in Audio MIDI Setup that combines your speakers with "hlsd Audio", and select that as the output.
  • The embedded driver is universal (arm64 + x86_64) when both Rust targets are installed at build time (rustup target add x86_64-apple-darwin aarch64-apple-darwin); otherwise it targets the host architecture, which is fine when hlsd and coreaudiod run the same architecture.
  • If the device doesn't appear after install, check log show --last 2m --predicate 'process == "coreaudiod"'.

Run at login with launchd #

To keep the stream up whenever you're logged in, use the example LaunchAgent in dist/launchd/com.github.tsirysndr.hlsd.plist:

# 1. Point ProgramArguments at your hlsd binary (`which hlsd`) and tweak flags.
cp dist/launchd/com.github.tsirysndr.hlsd.plist ~/Library/LaunchAgents/

# 2. Load it (starts immediately and at every login):
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.github.tsirysndr.hlsd.plist

# Restart / stop:
launchctl kickstart -k gui/$(id -u)/com.github.tsirysndr.hlsd
launchctl bootout gui/$(id -u)/com.github.tsirysndr.hlsd

The agent runs hlsd --input virtual with KeepAlive, so hlsd is restarted automatically if it exits (for example when coreaudiod is restarted). It must be a per-user LaunchAgent, not a system LaunchDaemon: capturing from the device requires the logged-in user's CoreAudio session.

Configuration #

Load a TOML file with --config, override individual values with flags:

./target/release/hlsd --config hlsd.toml --port 9000 --codec flac

Full example config (all options) #

Every key below is optional and shows its default value.

[input]
# Where the raw PCM s16le stream comes from:
# "stdin" | "fifo" | "unix" | "virtual" (macOS virtual device, see above).
source = "stdin"
# Filesystem path for the "fifo" or "unix" source (ignored for stdin).
# path = "/tmp/hlsd.sock"
sample_rate = 44100
channels = 2

[encoder]
# Codec: "flac" (pure Rust, always available) | "aac" | "mp3" | "opus".
# aac/mp3/opus require building with the matching Cargo feature.
codec = "flac"
# Target bitrate for lossy codecs (accepts 96000, "128k", "1M"). Ignored by FLAC.
bitrate = "128k"
# FLAC compression level 0..=8 (higher = smaller & slower). FLAC only.
flac_compression = 5

[output]
# Directory where manifests and segments are written and served from.
dir = "./stream"
# Target segment length in seconds.
segment_duration = 4
# Number of segments kept in the live window (older ones are deleted).
playlist_size = 6

[hls]
# Emit an HLS playlist (stream.m3u8 + master.m3u8).
enabled = true

[dash]
# Emit an MPEG-DASH manifest (stream.mpd).
enabled = false

[server]
host = "127.0.0.1"
port = 8080

CLI flags #

Flag Config key Description
-c, --config <FILE> — TOML config file
--input <stdin|fifo|unix|virtual> input.source Input source (virtual is macOS-only)
--input-path <PATH> input.path FIFO / socket path
--sample-rate <HZ> input.sample_rate PCM sample rate
--channels <N> input.channels PCM channels
--codec <flac|aac|mp3|opus> encoder.codec Codec
--bitrate <RATE> encoder.bitrate Lossy bitrate (128k, 96000, 1M)
--out-dir <DIR> output.dir Output/serve directory
--segment-duration <SECS> output.segment_duration Segment length
--playlist-size <N> output.playlist_size Live window size
--dash / --no-dash dash.enabled Toggle DASH output
--no-hls hls.enabled Disable HLS output
--host <HOST> server.host Bind host
-p, --port <PORT> server.port Bind port

Codec support & compatibility #

All codecs are packaged in fragmented MP4 (CMAF), shared by HLS and DASH.

Codec Build System deps Notes
FLAC default none (pure Rust) Lossless; plays in Safari and MSE browsers
AAC-LC --features aac none (vendored C) Best broad player support
MP3 --features mp3 none (vendored C) mp4a.6B; MSE support varies
Opus --features opus none (vendored C) 48 kHz input only; MSE support varies

For Opus, feed 48 kHz PCM (--sample-rate 48000); other rates are rejected.

How it works #

PCM (stdin/fifo/unix) ──▶ encoder ──▶ CMAF fMP4 muxer ──▶ segment files + manifests
                                                                      │
                                                          actix-web HTTP server
  1. Input reads blocking s16le bytes and frames them into i16 samples.
  2. Encoder turns samples into codec frames (each independently muxable).
  3. Muxer writes one immutable init.mp4 and a moof+mdat media segment per fragment.
  4. Segmenter maintains the sliding window, evicting and deleting old segments, and rewrites stream.m3u8 / master.m3u8 / stream.mpd atomically.
  5. Server serves everything with correct content types and CORS.

Development #

cargo test --release                    # unit + end-to-end smoke test (FLAC, no ffmpeg)
cargo build --release --features all-codecs

On macOS, the HAL driver has its own smoke test that loads the plug-in the way coreaudiod would and exercises its interface (property tree, loopback IO, controls) without touching the system audio stack:

cargo build --release -p hlsd-driver
cc -Wall -o /tmp/hlsd_driver_test driver/smoke_test.c \
   -framework CoreAudio -framework CoreFoundation
/tmp/hlsd_driver_test target/release/libhlsd_driver.dylib

The smoke test synthesizes PCM, runs the binary over stdin, and asserts a valid playlist and fMP4 segments are produced.

License #

MIT — see LICENSE.