Skip to main content
Version: 2.5.x (dev)

Icecast Server Emulation

Liquidsoap can act as an icecast-compatible server, accepting source client connections using the icecast.server operator. This allows you to receive streams from software like butt, mixxx, or any other icecast-compatible source client.

Experimental Feature

This functionality is still experimental. While it works for many common use cases, some features may change in future releases and some icecast configuration options are not yet supported.

Basic Usage

The simplest way to start an icecast-compatible server:

icecast-server-basic.liq
server = icecast.server()

# React when a source connects
server.on_connect(
fun (r) ->
log.important(
"Source connected on mount #{r.mount}"
)
)

# React when a source disconnects
server.on_disconnect(
fun (r) ->
log.important(
"Source disconnected from mount #{r.mount}"
)
)

This starts a server on port 8000 with the default password "hackme".

Parameters

  • port: Port to listen on (default: 8000)
  • password: Source password for authentication (default: "hackme")
  • config: Optional path to an icecast XML configuration file
  • dedicated_encoder: Allocate one encoder per listener (see below)
  • serve: Enable or disable the built-in status page (default: true, see below)
  • serve_auth: Optional callback (request) -> bool for access control on status endpoints
  • serve_json: Optional callback (stats) -> string to replace the built-in JSON renderer
  • serve_html: Optional callback (stats) -> string to replace the built-in HTML renderer
  • x_forwarded_for_proxy_ips: List of known reverse-proxy IPs for real-IP extraction (see below)
  • x_forwarded_for: Advanced callback to fully override real-IP extraction logic (see below)
  • format_options: Optional callback (string) -> [(string * string)] that returns muxer options for a given container format name. When null, falls back to settings.icecast.server.default_muxer_options (see below)

Return Value

The icecast.server function returns a record with the following methods:

  • mounts(): Returns a list of currently active mount points
  • get_source(mount): Returns the source for a given mount point
  • get_config(mount): Returns the current {format, streams} record for a mount, or null if the mount is not active
  • stats(): Returns the current mount stats list (same data as the JSON status endpoint)
  • on_connect(handler): Register a handler called when a source connects
  • on_disconnect(handler): Register a handler called when a source disconnects

dedicated_encoder

By default, all listeners on a mount share a single encoder instance. Setting dedicated_encoder=true allocates one independent encoder per listener.

With copy-based encoders (e.g. copy_encoder("matroska") or %ffmpeg with %audio.copy / %video.copy), this amounts to a lightweight remux per listener. The overhead is low — essentially one mux pass per listener — while the benefit is significant: each listener receives a clean, self-contained stream starting from a proper frame boundary.

icecast-server-dedicated-encoder.liq
icecast.server(port=8000, password="hackme", dedicated_encoder=true)

Each listener that connects will get their own stream, properly initialised from a clean frame boundary, regardless of when they join.

Note: With full re-encoding (e.g. %mp3, %aac), dedicated_encoder=true creates a complete encoder per listener, which can be costly under load. Prefer copy-based encoders when using dedicated_encoder.

Reverse Proxy and X-Forwarded-For

When icecast.server runs behind a reverse proxy (e.g. nginx), the listener IP recorded in stats will be the proxy's IP rather than the real client IP. Use x_forwarded_for_proxy_ips to fix this.

x_forwarded_for_proxy_ips takes a list of known proxy IPs. The default implementation (icecast.server.x_forwarded_for) walks the X-Forwarded-For header from right to left, skips any IP in the list, and returns the first non-proxy IP. This is safe against client spoofing: even if a client sends a forged X-Forwarded-For header, the proxy appends the real connecting IP as the rightmost entry, which is what gets used.

# Single nginx proxy at 10.0.0.1
icecast.server(
x_forwarded_for_proxy_ips=["10.0.0.1"],
password="hackme"
)

# CDN + nginx in front
icecast.server(
x_forwarded_for_proxy_ips=["10.0.0.1", "203.0.113.42"],
password="hackme"
)

For advanced use cases — custom header names, CIDR matching, CDN-specific headers like CF-Connecting-IP — pass a full callback via x_forwarded_for. The callback receives a record with ip (connection IP), headers, protocol, uri, and proxy_ips (from x_forwarded_for_proxy_ips, or [] if not set), and must return a string IP:

# Trust Cloudflare's CF-Connecting-IP header
icecast.server(
x_forwarded_for=fun ({ip, headers, ...}) ->
list.assoc(default=ip, "cf-connecting-ip", headers),
password="hackme"
)

Status Page

By default, icecast.server registers two HTTP endpoints on the icecast port:

  • / — an HTML status page showing active mounts, listener counts, and playback controls
  • /status.json — a JSON endpoint with the same data, polled by the HTML page every 5 seconds

Disabling the Status Page

Pass serve=false to disable both endpoints:

icecast.server(serve=false, password="hackme")

Access Control

Use serve_auth to gate both endpoints behind a check. The callback receives the HTTP request and must return true to allow access:

icecast-server-serve-auth.liq
icecast.server(
serve_auth=fun (req) -> req.headers["x-secret"] == "my-secret-token"
)

Any request that fails the check receives a 401 Unauthorized response with a WWW-Authenticate header.

Custom JSON Renderer

Use serve_json to replace the built-in /status.json output. The callback receives the stats list and must return a JSON string. The example below also disables the HTML page by returning a plain not-found response from serve_html:

icecast-server-serve-json.liq
icecast.server(
serve_html=fun (_) -> "<!DOCTYPE html><html><body>Not found.</body></html>",
serve_json=fun (stats) ->
begin
mounts =
list.map(
fun ((mount, s)) ->
{
mount = mount,
name = s.name,
listeners = list.length(s.listeners)
},
stats
)
json.stringify(compact=true, {mounts = mounts})
end
)

The stats list passed to both serve_json and serve_html is a list of (mount, stats) pairs where each stats record contains: name, content_type, mime_type, started, listeners, peak_listeners, and current_metadata.

Live Streaming Muxer Options

Some container formats require specific muxer flags to produce a valid live stream. For example, Matroska and WebM streams need their muxer told that there will be no seekable index at the end.

icecast.server automatically applies these options when remuxing an incoming stream via copy_encoder. The defaults are controlled by settings.icecast.server.default_muxer_options, which maps container format names to lists of FFmpeg muxer options:

FormatDefault optionsEffect
matroskadash=1, live=1Disables index/cues, writes streaming-compatible cluster timestamps
webmdash=1, live=1Same as matroska (WebM is a subset)

To override the defaults globally, set the setting before starting the server:

settings.icecast.server.default_muxer_options :=
[("matroska", [("dash", "1"), ("live", "1")]),
("webm", [("dash", "1"), ("live", "1")])]

To override per-server instance, use the format_options parameter. The callback receives the detected container format name and returns the options list to apply:

# Disable all extra muxer options
icecast.server(format_options=fun (_) -> [], password="hackme")

# Custom options for matroska, defaults for everything else
icecast.server(
format_options=fun (fmt) ->
if fmt == "matroska" then [("dash", "1"), ("live", "1"), ("cluster_size_limit", "1000000")]
else list.assoc(default=[], fmt, settings.icecast.server.default_muxer_options())
end,
password="hackme"
)

When format_options is null (the default), settings.icecast.server.default_muxer_options is used. When provided, it fully replaces the settings lookup — the callback is responsible for returning all options for every format.

Key Differences from Icecast

The liquidsoap icecast server operates fundamentally differently from a traditional icecast server. Understanding these differences is important for getting the most out of this feature.

Direct Encoded Content Manipulation

Unlike icecast, which primarily acts as a relay for encoded streams, liquidsoap can directly manipulate the encoded content. The incoming stream is demuxed, passed as encoded packets through the liquidsoap pipeline, and remuxed for output. This allows advanced format manipulation without ever needing to decode and re-encode, avoiding the CPU and memory consumption typically associated with transcoding.

This makes it possible to:

  • Format-compatible fallbacks: When a source disconnects, the fallback mount seamlessly takes over without format incompatibility, because both streams go through the same processing pipeline.

  • Seamless transitions: Listeners are never disconnected during source switches. The transition happens smoothly within the liquidsoap processing chain.

  • Per-listener clean streams: With dedicated_encoder=true and copy encoders, each listener gets a fresh, properly initialised stream, enabling reliable playback of any streamable container format.

Because of this architecture, the following icecast options are fundamentally incompatible:

  • fallback-override: In icecast, this allows a reconnecting source to "steal back" listeners from a fallback mount. In Liquidsoap, fallback is implemented by switching the underlying source inside the same output via source.dynamic. Listeners remain connected to the original mount throughout — there are no listeners "at the fallback mount" to reclaim, so the concept does not apply.

  • fallback-when-full: In icecast, this redirects to a fallback when max-listeners is reached. Liquidsoap's architecture handles this differently through its own source management.

Using an Icecast Configuration File

You can use a standard icecast XML configuration file:

icecast-server-config.liq
icecast.server(config="/etc/icecast.xml")

This parses the configuration file and extracts supported settings. See the Configuration Reference section below for complete details on what is supported.

Example Configuration File

<icecast>
<limits>
<sources>10</sources>
</limits>

<authentication>
<source-password>mysecretpassword</source-password>
</authentication>

<listen-socket>
<port>8000</port>
</listen-socket>

<http-headers>
<header name="Access-Control-Allow-Origin" value="*" />
</http-headers>

<!-- Default settings for all mounts -->
<mount type="default">
<burst-size>65536</burst-size>
</mount>

<!-- Specific mount configuration -->
<mount type="normal">
<mount-name>/live.mp3</mount-name>
<username>dj</username>
<password>djpassword</password>
<max-listeners>100</max-listeners>
<fallback-mount>/fallback.mp3</fallback-mount>
<on-connect>/path/to/script.sh</on-connect>
<on-disconnect>/path/to/script.sh</on-disconnect>
</mount>
</icecast>

Configuration Reference

This section provides a comprehensive reference for icecast XML configuration options, indicating which are supported, which are not yet implemented, and which will likely never be supported due to architectural differences.

listen-socket

OptionStatusNotes
portSupportedServer listening port
bind-addressSupportedSets settings.harbor.bind_addrs
tls / sslSupportedIcecast 2.4: 0/1. Icecast 2.5: disabled/auto_no_plain. Other 2.5 values log a warning and disable TLS.
shoutcast-mountNot implementedShoutcast compatibility mount

Note: Only the first listen-socket entry is used. Multiple listen sockets are not supported; a warning is logged if more than one is found.

authentication

OptionStatusNotes
source-passwordSupportedGlobal password for source clients

limits

OptionStatusNotes
sourcesSupportedMaximum number of simultaneous source connections
clientsParsed (TODO)Maximum number of listeners (not yet enforced)
source-timeoutSupportedTimeout in seconds for source connections (float)
client-timeoutSupportedTimeout in seconds for client connections (float)
burst-sizeSupportedDefault burst size for new listeners (bytes)
burst-on-connectSupportedSet to 0 to disable burst entirely
queue-sizeNot usedLiquidsoap manages queues differently
header-timeoutNot usedLiquidsoap handles headers differently

paths

OptionStatusNotes
basedirNot implemented
logdirSupportedEnables log file and sets path to #{logdir}/<script>.log
pidfileSupportedEnables pidfile and sets path to the value
tls-certificate / ssl-certificateSupportedPath to TLS certificate file (required when TLS is enabled). May include the private key.
tls-keySupportedPath to separate TLS private key file (icecast 2.5 only)
webrootNot implementedNo built-in web interface
adminrootNot implementedNo built-in admin interface
allow-ipNot implementedUse a reverse proxy (nginx) or firewall instead
deny-ipNot implementedUse a reverse proxy (nginx) or firewall instead
ssl-allowed-ciphersNot implementedTLS cipher configuration not exposed
aliasNot implementedURL aliasing not supported

http-headers

Global HTTP headers are fully supported. Use the standard icecast format:

<http-headers>
<header name="Access-Control-Allow-Origin" value="*" />
<header name="X-Custom-Header" value="value" />
</http-headers>

mount

Mount configurations support both type="default" (settings applied to all mounts) and type="normal" (specific mount configurations).

Supported Mount Options

OptionDescription
mount-nameMount point path (e.g., /live.mp3)
usernameSource client username (default: "source")
passwordMount-specific password (overrides global)
dump-filePath to dump the raw stream to a file
burst-sizeInitial burst size for new listeners (bytes)
fallback-mountMount to fall back to when this source disconnects
on-connectShell command to execute when source connects
on-disconnectShell command to execute when source disconnects
http-headersCustom HTTP headers for this mount's responses

Unsupported Mount Options

These options are not yet implemented:

OptionReason
max-listenersNot yet implemented
hiddenNot yet implemented
publicDirectory listing registration not implemented
introIntro file playback not implemented
max-listener-durationListener duration limits not implemented
authenticationURL-based authentication not implemented

Incompatible Mount Options

These options are fundamentally incompatible with liquidsoap's architecture.

OptionReason
fallback-overrideLiquidsoap manages sources through its own pipeline; "stealing back" listeners doesn't apply
fallback-when-fullLiquidsoap handles listener limits through its own source management

Configuration Sections Not Supported

The following icecast configuration sections are not supported and will be ignored:

  • fileserve - Static file serving (use liquidsoap's harbor HTTP handlers)
  • relay - Stream relaying (use liquidsoap's input.http instead)
  • directory - Directory listings (YP)
  • logging - Use liquidsoap's logging settings
  • security - Use liquidsoap's security settings

Complete Example

Here's a complete example showing a radio station setup with multiple mounts and fallback handling:

icecast-server-complete.liq
# Start the icecast-compatible server
server =
icecast.server(
port=8000,
password="source_password",
config="/etc/liquidsoap/icecast.xml"
)

# Create a fallback source (e.g., playlist)
fallback_source = playlist("/path/to/music")

# When a DJ connects, log it and optionally do something
server.on_connect(
fun (r) ->
begin
log.important(
"DJ connected on #{r.mount}"
)
# You could trigger notifications, update a database, etc.
end
)

server.on_disconnect(
fun (r) ->
log.important(
"DJ disconnected from #{r.mount}"
)
)

# Output the stream
# The server handles incoming connections automatically
# and creates output.harbor instances for each mount

Minimal Configuration File

<icecast>
<authentication>
<source-password>changeme</source-password>
</authentication>

<listen-socket>
<port>8000</port>
</listen-socket>

<mount type="default">
<burst-size>65536</burst-size>
<fallback-mount>/fallback</fallback-mount>
</mount>

<mount type="normal">
<mount-name>/live</mount-name>
</mount>
</icecast>

See Also