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

Icecast Server

Liquidsoap can be the Icecast server itself. icecast.server accepts source clients such as butt, Mixxx or another liquidsoap, serves their streams to listeners, and speaks the protocols that players, statistics tools and listener authentication backends written for Icecast, RSAS or Icecast-KH expect. You can also feed its mounts from your own liquidsoap sources. To send your stream to an external Icecast server instead, see streaming to Icecast and Shoutcast.

Starting a server​

A server is described by one record, its parameters. Every field is optional and takes its default when missing, so a server needs little more than a source password. Here it also logs its sources coming and going:

icecast-server-basic.liq
server = icecast.server({source_password = "change-me"})

server.on_source_connect(
fun (r) ->
log.important(
"Source connected on mount #{r.mount}"
)
)

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

This listens on port 8000 for source clients and listeners, and serves the Shoutcast source protocol on port 8001. A source client logs in with the user source and the source_password. It can then stream to any mount, for instance /live.mp3, and listeners find it at http://localhost:8000/live.mp3.

Source clients connect with HTTP PUT or POST, Icecast's legacy SOURCE method, a WebSocket, or the Shoutcast protocol, which feeds the mount named by shoutcast_mount. Some paths are never mounts because the server answers them itself: /, /status.json, /status-json.xsl, /metrics, /admin, /admin.cgi, anything under /admin/, and anything ending in /health or /manage.

The main server parameters are:

FieldDefaultMeaning
port8000Port for sources and listeners. The Shoutcast protocol uses port + 1.
bind_addressall interfacesLocal address to bind.
transportplain HTTPThe HTTP transport of the port, for instance TLS (see below).
compatibility"icecast-2.5"Which implementation to mimic (see below).
hostname"localhost"The name the server gives itself in status pages and to backends.
source_password"hackme"Password of source clients when their mount has none of its own.
shoutcast_mount"/stream"Mount fed by a Shoutcast source client.
max_sourcesunlimitedMaximum number of simultaneous source clients.
max_clientsunlimitedMaximum number of simultaneous listeners over all mounts.
source_timeout10.Seconds without data after which a source client is dropped.
client_timeout30.Seconds a listener's connection may stall before it is dropped.
burst_on_connecttrueWhether new listeners get a burst.
burst_size65536Bytes sent at once to a new listener, so that playback starts quickly.
max_history10Titles kept in each mount's history, 1 to 256.
name"icecast.server"Label of the server in logs.

The server warns at startup and at each reload when its source password, or that of a live mount, is the default hackme.

Two settings reach beyond the server. bind_address sets settings.harbor.bind_addrs, which every harbor port uses. Starting a server sets the default frame duration to 0.2 s; a script that sets settings.frame.duration itself keeps its value. Routes registered with harbor.http.register on the server's port before icecast.server is called take precedence over its pages; GET routes registered after it are shadowed.

The server value​

icecast.server returns a value with these methods:

  • on_source_connect, on_source_disconnect, on_listener_connect, on_listener_disconnect and on_metadata register handlers for events (see below);
  • stats() returns a snapshot of the server's statistics;
  • reload(parameters) replaces the parameters of the running server (see below);
  • reopen_logs() reopens the log files after an external tool moved them;
  • attach_source(~format, ?headers, mount, source) and detach_source(mount) feed a mount from a liquidsoap source (see below).

Mounts​

Most stations have more than one mount: the live show, an automated backup, a members-only stream, a low-bitrate version. Each needs its own credentials, limits or fallback. All of this comes from one callback, mount: the server calls it with a mount's path, and it answers the parameters of that mount, or null when the mount has none of its own:

icecast-server-mounts.liq
def mount(path) =
if
path == "/live.mp3"
then
{
source_user = "dj",
source_password = "dj-password",
max_listeners = 100,
fallback = {mount = "/autodj.mp3"}
}
elsif
string.contains(prefix="/test/", path)
then
{hidden = true, max_listener_duration = 600.}
else
null
end
end

icecast.server({source_password = "change-me", mount = mount})

Since it is a function, wildcards, defaults shared by several mounts or a lookup in your own table are just code. A source client may create any mount the other rules allow, as on Icecast: the callback configures mounts, it does not declare them.

FieldMeaning
source_userUser name source clients present, "source" by default.
source_passwordPassword of the mount's source clients, the server's by default.
source_decisionA function (request) -> bool replacing the user and password check of source clients.
on_connectCalled in the background when something starts feeding the mount.
on_disconnectCalled in the background when it stops.
max_listenersMaximum number of simultaneous listeners on the mount.
listener_decisionDecides each listener (see below).
max_listener_durationSeconds after which a listener is disconnected.
fallbackWhat feeds the mount while it has no source: {mount = "/other.mp3"} or {source = s}.
fallback_when_fullRedirect listeners past max_listeners along the fallback mounts, with 307, instead of refusing them.
lingerSeconds the mount keeps its listeners, waiting, when its source drops and nothing else feeds it.
hiddenKeep the mount off the public status pages.
max_historyTitles kept in the mount's history, the server's max_history by default.
health_passwordPassword of the mount's /<mount>/health page.
burst_sizeThe mount's own burst. 0 disables it whatever the server's value.
dedicated_encoderGive each listener its own encoder, whatever the server's value.
http_headersHeaders added to the mount's listener replies (see below).
dump_fileFile the mount's stream is recorded to, a strftime pattern expanded at each recording start.
dump_file_size_limitBytes after which a recording stops.
dump_file_time_limitSeconds after which a recording stops.

Fallbacks​

Liquidsoap does not just relay the bytes it receives: it reads the stream it serves. When a mount loses its source, it switches to its fallback at the next packet, and back to its own source as soon as that one returns. Listeners never leave their mount: their connection, headers and limits stay the same, and the switch is invisible to their player. A fallback mount without a source of its own passes on to its own fallback, up to ten mounts.

A fallback must carry the same format as the mount. A mount with no fallback that loses its source keeps its listeners for linger seconds, so that a DJ whose connection blips does not lose the audience.

Feeding a mount from liquidsoap​

A mount does not need a source client. attach_source feeds it from a liquidsoap source of encoded content, so that your automation is served next to the live mounts:

icecast-server-attach.liq
server = icecast.server({source_password = "change-me"})

radio = mksafe(playlist("/srv/radio/music"))
encoded =
ffmpeg.encode.audio(
%ffmpeg(%audio(codec = "libmp3lame", sample_format = "fltp")),
radio
)

server.attach_source(
format=%ffmpeg(format = "mp3", %audio.copy),
headers=[
(
"ice-name",
"My radio"
)
],
"/radio.mp3",
encoded
)

format is the encoder that writes the content for listeners, here a copy of the MP3 data, and headers are the stream identity headers (ice-name, ice-genre, …) a source client would have sent. attach_source raises error.icecast.server.reserved_path on a path the server answers itself, and error.icecast.server.mount_taken when the mount already has a feeder. The same kind of source can be a mount's fallback, as in the complete example.

Deciding on listeners​

Paid streams, geo-restricted content, a members area or a per-listener ad all need a decision on each listener before the stream starts. A mount's listener_decision is a function from the listener's request to an outcome, a record whose fields are all optional:

FieldMeaning
refuseRefuse or redirect the listener, with icecast.server.refusal or icecast.server.redirect.
time_limitSeconds after which the listener is disconnected.
prerollBytes sent before the stream (see below).
exemptSkip max_listeners, for instance for a trusted relay.
on_releaseCalled once when the listener leaves.
userA new user name for the listener.

The request carries the listener's id, address, headers, user and password (from HTTP Basic credentials), mount, query, the mount's current listeners, and user_listeners(user), the number of listeners that user already has on the mount, as well as protocol, server and compatibility.

The standard library has ready-made decisions, and icecast.server.decision.all composes several in order, the first refusal winning:

icecast-server-decisions.liq
# Closed before 6 pm, local time.
def evening_only(_) =
if
time.local().hour < 18
then
{
refuse =
icecast.server.refusal(
reason="Back at 6 pm",
403
)
}
else
{refuse = null}
end
end

def parameters() =
members =
icecast.server.decision.all(
[
icecast.server.decision.access_lists(deny="/etc/radio/banned.txt"),
icecast.server.decision.htpasswd("/etc/radio/members.htpasswd"),
evening_only
]
)
{
source_password = "change-me",
mount =
fun (path) ->
path == "/members.mp3" ? {listener_decision = members} : null
}
end

server = icecast.server(parameters())
  • icecast.server.decision.access_lists(~deny, ~allow, ~agents) refuses addresses from a deny file, addresses missing from an allow file, and User-Agent values from an agents file. Lines are addresses or wildcard patterns.
  • icecast.server.decision.htpasswd(file) checks HTTP Basic credentials against a file of user:md5 lines. Without valid credentials the listener is refused with 401, which makes a browser ask for them.
  • icecast.server.decision.url_auth(~cache, options) asks a web service of yours, with Icecast's protocol (see below).

The files are read again when they change.

URL authentication​

Icecast leaves the decision to a web service, and icecast.server speaks the same protocol, extended the way RSAS and Icecast-KH extend it, so that an existing backend keeps working:

icecast-server-url-auth.liq
def parameters() =
cache = icecast.server.preroll_cache(directory="/srv/radio/prerolls")
auth =
icecast.server.decision.url_auth(
cache=cache,
{
listener_add = "http://localhost:8080/listener/add",
listener_remove = "http://localhost:8080/listener/remove"
}
)
{source_password = "change-me", mount = fun (_) -> {listener_decision = auth}}
end

server = icecast.server(parameters())

For each listener, the server POSTs Icecast's form fields to listener_add: action, server, port, client, mount, user, pass, ip, agent, then the fields RSAS and Icecast-KH add. The reply admits the listener with icecast-auth-user: 1 (or x-icecast-auth-result: ok), and can set a time limit, redirect the listener, refuse it with its own status and message, or choose a preroll. A request that fails refuses the listener with 401, unless presume_innocent is set. When an admitted listener leaves, the server POSTs to listener_remove with the session's duration, which is what billing and concurrent-session tracking need.

The options keep Icecast's names: listener_add, listener_remove, username, password, auth_header, timelimit_header, header_auth, header_timelimit, header_message, header_alter_action, header_alter_argument, action_add, action_remove, headers, header_prefix, timeout, presume_innocent and on_error_wait. client_add and client_remove are accepted as aliases of listener_add and listener_remove.

Listener limits​

A listener past the mount's max_listeners or the server's max_clients is refused with the status the chosen implementation sends: 503 Service Unavailable under "icecast-2.5" and "rsas", 403 Forbidden under "icecast-2.4" and "kh". Players retry on 503 and not on 403, so some tools need the older one:

icecast-server-status-codes.liq
icecast.server({source_password = "change-me", compatibility = "icecast-2.4"})

Prerolls​

A preroll is a short file a listener hears before joining the stream: a station jingle, a sponsor message, or an ad an ad server chose for that listener. A decision gives it as bytes, and icecast.server.preroll_cache loads them from a local file or a URL:

icecast-server-preroll.liq
def parameters() =
cache = icecast.server.preroll_cache(directory="/srv/radio/jingles")
{
source_password = "change-me",
mount =
fun (_) ->
{listener_decision = fun (_) -> {preroll = cache("welcome.mp3")}}
}
end

server = icecast.server(parameters())

The file is sent as it is, before the stream, so it must use the stream's codec and parameters: for an MP3 stream, an MP3 file with the same sample rate and channels. Files are kept in memory and shared by every listener, up to settings.icecast.server.preroll_cache_size bytes in all, the least recently used dropped first. A local file is read again when it changes, and a URL is checked again after two hours using its ETag. A file that cannot be loaded is logged, and the listener gets the stream without a preroll.

Events​

Listener statistics, royalty reports, dashboards and notifications are all built on knowing who connects and when. The server reports each event to the handlers you register:

icecast-server-listener-callbacks.liq
server = icecast.server({source_password = "change-me"})

server.on_listener_connect(
fun (listener) ->
log.important(
"Listener #{listener.id} (#{listener.ip}) tuned in to #{listener.mount}"
)
)

server.on_listener_disconnect(
fun (listener) ->
log.important(
"Listener #{listener.id} left #{listener.mount} after #{
int(listener.duration)
}s and #{listener.bytes_sent} bytes"
)
)

server.on_metadata(
fun (update) ->
log.important(
"Now playing on #{update.mount} for #{update.listeners} listener(s): #{
update.metadata["title"]
}"
)
)

Listener handlers receive a record with id, mount, ip (hashed, see privacy), user, user_agent, referer, uri, protocol, connected_at, duration and bytes_sent: zero on connect, the session's totals on disconnect. Source handlers receive mount, ip (the source client's address, not hashed), agent, server ({hostname, port, instance}) and compatibility, and on connect the liquidsoap source feeding the mount, its format, the headers the source client sent and its streams, the descriptions of its streams. on_metadata handlers receive mount, metadata and the mount's listeners.

Every on_* method takes synchronous, false by default. An asynchronous handler runs in its own thread and never delays the stream, but no order is guaranteed between such handlers, so counters they share must use atomic. A synchronous handler runs in order with the event and must be short. Every admitted listener produces exactly one connect and one disconnect event.

Status pages and statistics​

The server answers a few pages on its own port:

  • /: an HTML page with each mount's name, format, now playing, listener count and a player;
  • /status.json: the same data as JSON;
  • /status-json.xsl: Icecast's JSON, which players and website widgets read;
  • /health and /<mount>/health: RSAS's health document, protected by health_password when set;
  • /metrics: a Prometheus export, when metrics is true.

They show counts only, never listener addresses, and leave out hidden mounts. status_page = false turns off the first three, /health and /<mount>/health; under "rsas" the health pages stay, as RSAS serves them on their own. status_access gates the first three and /metrics behind a check of your own:

icecast-server-serve-auth.liq
icecast.server(
{
source_password = "change-me",
status_access =
fun (request) -> request.headers["x-secret"] == "my-secret-token"
}
)

status_json and status_html replace the rendering of /status.json and /. Each receives the stats() snapshot, hidden mounts included, so a public page filters them on hidden. Its mounts hold, per mount, mount, name, listeners, peak_listeners, connections, listening_time, bytes_sent, metadata, history and the stream's identity:

icecast-server-serve-json.liq
# The snapshot includes hidden mounts: a public page leaves them out.
def public_mounts(stats) =
list.filter(fun (m) -> not m.hidden, stats.mounts)
end

icecast.server(
{
source_password = "change-me",
status_html =
fun (stats) ->
"<!DOCTYPE html><html><body>#{list.length(public_mounts(stats))} streams \
on air.</body></html>",
status_json =
fun (stats) ->
begin
mounts =
list.map(
fun (m) ->
{
mount = m.mount,
name = m.name,
listeners = list.length(m.listeners)
},
public_mounts(stats)
)
json.stringify(compact=true, {mounts = mounts})
end
}
)

With an admin_password (user admin_user, "admin" by default), the server also serves the admin pages: /admin/listeners is a page to watch each mount and its listeners, update a mount's metadata or disconnect its source, /admin/listeners.json lists each mount's listeners, and /admin/stats, /admin/listmounts and /admin/listclients answer Icecast's XML for administration tools. Without an admin_password, the admin pages refuse everyone with 401.

The admin commands metadata, killsource and fallbacks work as on Icecast. They authenticate with the mount's source credentials or the admin's; /admin.cgi also takes them as user and pass query arguments.

Log files​

Log analyzers and statistics tools read the files Icecast writes. icecast.server writes the same access, playlist and error logs:

icecast-server-access-log.liq
server =
icecast.server(
{
source_password = "change-me",
access_log = "/var/log/liquidsoap/access.log",
playlist_log = "/var/log/liquidsoap/playlist.log",
error_log = "/var/log/liquidsoap/error.log",
admin_password = "admin-secret"
}
)

# logrotate moves the files, then sends SIGHUP.
ignore(on_signal("hup", fun () -> server.reopen_logs()))
  • The access log gets one line per listener session, in Icecast's combined log format with the session length appended.
  • The playlist log gets one line per metadata change: time, mount, listener count and artist - title.
  • The error log gets the server's own events, in Icecast's format, levels and categories, up to log_level (on liquidsoap's scale: 4, info, by default). Every event also goes to liquidsoap's own log.

"-" writes to standard error. Files rotate past log_size bytes, to <file>.old, or to a timestamped name with log_archive. After logrotate moves a file, reopen_logs() opens it again at its path. The server takes no signal itself: the example binds SIGHUP with on_signal. A setup that also reloads on SIGHUP reopens the logs in the same handler, as the reload example does, since a reload keeps files whose path did not change open.

Listener privacy​

A listener's address is personal data. The server applies address_hash to every listener address before anything sees it: event handlers, stats(), the admin pages and the logs. Source client addresses are the operator's own side and are not hashed. The default is a 16-character MD5 digest, the same on every run, so that you can still count unique listeners. It has no key, so anyone can hash a guessed address and compare. A secret key protects them better:

icecast-server-ip-hash.liq
# The key must stay secret: whoever holds it can check guessed addresses.
icecast.server(
{
source_password = "change-me",
address_hash = fun (ip) -> string.digest("my-secret-key" ^ ip)
}
)

If you need the plain addresses and are allowed to keep them, opt in explicitly:

icecast-server-plain-ips.liq
icecast.server({source_password = "change-me", address_hash = fun (ip) -> ip})

Listener decisions and URL authentication backends receive the plain address, as on Icecast.

Behind a reverse proxy​

Behind a proxy such as nginx, every listener connects from the proxy's address. List your proxies in proxy_ips, as addresses or CIDR ranges, and the server takes the listener's address from X-Forwarded-For, walking it from the right and skipping trusted proxies. An empty list trusts the rightmost entry. For other headers, forwarded_address is a function of the request that answers the address:

icecast-server-forwarded.liq
# Cloudflare sends the client's address in its own header.
icecast.server(
{
source_password = "change-me",
forwarded_address =
fun (request) -> list.assoc.nullable("cf-connecting-ip", request.headers)
}
)

HTTP headers​

http_headers, on the server and per mount, adds headers to listener replies. An entry is {name, value}, or {name, compute}, where compute is a function of the request and the reply's status, for headers that depend on them. An entry with a status applies only to replies with that status. A reply's list is, in order, the listener decision's headers, the stream identity, the server's entries, then the mount's, and the first entry of a name wins: a mount cannot override a header the server sets. icecast.server.headers.cors(~origins, ~expose) builds the CORS headers web players need:

icecast-server-headers.liq
icecast.server(
{
source_password = "change-me",
http_headers =
icecast.server.headers.cors(origins=["https://radio.example"]),
mount =
fun (_) -> {http_headers = [{name = "X-Robots-Tag", value = "noindex"}]}
}
)

TLS​

TLS is a property of the port's transport. Pass one built with http.transport.tls or http.transport.ssl:

icecast-server-tls.liq
icecast.server(
{
source_password = "change-me",
port = 8443,
transport =
http.transport.tls(
certificate="/etc/radio/cert.pem",
key="/etc/radio/key.pem"
)
}
)

The transport reads its certificate again at each reload and once a day on its own, so that a renewed certificate is used without a restart. See Renewing certificates.

Encoding and muxing​

By default, the listeners of a mount share one encoder. dedicated_encoder = true gives each listener its own. With copy encoders, such as %ffmpeg(format = "matroska", %audio.copy), this is a cheap remux, and each listener gets a stream that starts with its own container header, so that listeners can join any streamable container at any time. With full re-encoding, it costs one encoder per listener. A mount's own dedicated_encoder wins over the server's:

icecast-server-dedicated-encoder.liq
# Each listener of /live.mkv gets its own encoder; other mounts share one.
icecast.server(
{
source_password = "change-me",
mount =
fun (path) -> path == "/live.mkv" ? {dedicated_encoder = true} : null
}
)

Some containers need options to be written live: Matroska and WebM get live=1 by default. muxer_options is a function from a container name to the options to use, and settings.icecast.server.default_muxer_options holds the defaults.

Compatibility​

Icecast 2.4, Icecast 2.5, RSAS and Icecast-KH differ in small, visible ways: the status of a refused listener, the details of URL authentication, the status pages, the CORS headers. compatibility chooses which one the server mimics: "icecast-2.5" (default), "icecast-2.4", "rsas" or "kh". Wherever they differ, the chosen one wins. A behaviour only another implementation has, and that conflicts with nothing, is kept too, so that a backend written for any of them keeps working.

compatibility is read once, at startup. A reload with another value keeps the running one and logs it.

Some behaviours are the same under every value, because the server reads the streams it serves: listeners stay on their mount through fallbacks and come back with their source, a mount keeps its listeners for linger seconds through a source drop, one transport replaces Icecast's TLS modes, and proxy_ips takes CIDR ranges.

Reloading​

A running server usually has sources and listeners connected, and restarting it to change a password or a fallback would drop all of them. reload(parameters) replaces the whole record in place. The new record is checked first: when it is invalid, reload raises error.icecast.server.invalid_parameters and nothing changes. Otherwise it applies in one step, and no source or listener is disconnected.

Build the record in one function, called for the first start and for every reload, so that each reload computes it whole, overrides included:

icecast-server-reload.liq
config = "/etc/icecast2/icecast.xml"

# Every reload computes the whole record again, overrides included.
def parameters() =
icecast.server.config.load(compatibility="icecast-2.4", config).{port = 8100}
end

server = icecast.server(parameters())

def reload() =
try
server.reload(parameters())
catch err do
log.important(
"Configuration not reloaded: #{err.message}"
)
end
end

# One signal for a configuration reload and logrotate's reopen.
ignore(
on_signal(
"hup",
fun () ->
begin
reload()
server.reopen_logs()
end
)
)
ignore(file.watch(config, reload))

Parameters apply at different times:

  • name, port, bind_address, transport and compatibility are read at startup only. A reload that changes the name, port, address, transport or compatibility logs that it was not applied.
  • client_timeout, burst_on_connect, burst_size, http_headers, dedicated_encoder and muxer_options apply when a mount output is next created. A running mount keeps the values it started with.
  • Everything else applies at its next use: the next listener, the next source connection, the next log line, the next event. A live mount follows a changed fallback at once, and one left with nothing to send is shut down.

Create liquidsoap sources once, outside the function: a mount follows its fallback source by identity, and a source rebuilt at each reload would be a new one. Handlers registered with the on_* methods are kept by a reload. event_bindings, which are part of the record, are replaced.

Using an Icecast configuration file​

icecast.server.config.load(path) turns an Icecast, RSAS or Icecast-KH XML configuration file into parameters, so that an existing setup moves over as it is:

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

The file is one way to build the record: you can change the result like any other record, for instance icecast.server.config.load(path).{port = 8100}, and reload it as above. The compatibility argument ("icecast-2.5" by default) is written into the record and also decides how mounts are matched and whether the cors header type exists. No element of the file sets it, because most files are valid for several implementations.

A mount's settings come from the record's mount function, which the file fills in. To change one mount, wrap that function. It returns null for a path the file has no <mount> for, so start from m ?? {} to cover that case too:

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

# /live.webm gets a dedicated encoder per listener, on top of what the file
# says about it; every other mount keeps the file's settings.
def mount(path) =
m = config.mount(path)
if path == "/live.webm" then (m ?? {}).{dedicated_encoder = true} else m end
end

icecast.server(config.{mount = mount})

The translation is best effort. It maps the common settings and ignores, with a warning naming it, every element it does not reproduce: the other settings keep working. A file that would be served less safely is rejected instead, with error.icecast.server.invalid_parameters:

  • malformed XML, a root other than <icecast>, or a value of the wrong kind, such as a non-numeric <port>;
  • a mount <authentication> other than url or htpasswd, or one using <role>: ignoring it would open the mount to everyone;
  • a <tls> mode that serves TLS and plain HTTP on one port (auto, auto_no_plain, rfc2817), or a certificate with no <tls> mode, which Icecast takes to mean the same, and a <tls> mode that enables TLS with no certificate. Set <tls> to rfc2818 for TLS or disabled for plain HTTP.

What is translated​

ElementBecomes
hostname, location, admin, server-idhostname, location, admin_contact, server_id
listen-socket: port, bind-address, tls or ssl, shoutcast-mount, trusted-proxyport, bind_address, transport, shoutcast_mount, proxy_ips. Only the first listen-socket is used.
authentication: source-password, admin-user, admin-password, health-passwordthe parameters of the same names
limits: sources, clients, client-timeout, source-timeout, burst-size, burst-on-connect, max-listeners (KH)max_sources, max_clients, the timeouts, the burst, and every mount's default max_listeners
paths: logdir, webroot, certificate and key, x-forwarded-for, allow-ip, deny-ip, deny-agentslog locations, the preroll directory, the TLS transport, proxy_ips, access lists on every mount
logging: accesslog, errorlog, playlistlog, loglevel, logsize, logarchivethe log parameters; files go under logdir, and logsize is in KiB
http-headers, and RSAS's custom-headershttp_headers, on the server or the mount
event-bindingsevent_bindings, running the exec, url and log actions
mountthe mount callback (see below)

In a <mount>, mount-name, username, password, max-listeners, burst-size, fallback-mount, fallback-when-full, linger-for (KH), hidden, on-connect, on-disconnect, http-headers, max-listener-duration, max-history, intro, preroll (RSAS), dump-file and its limits, health-password and authentication are translated.

A path takes the type="normal" mount of that name, completed field by field with the first type="default" mount whose mount-name pattern matches, or that has none. Under "kh", every mount-name is a pattern: an exact name wins, then the lowest priority, then file order. <authentication type="url"> becomes URL authentication with the same options, including mount_add, mount_remove and stream_auth. type="htpasswd" becomes the htpasswd decision, and allow_duplicate_users set to 0 or connections-per-user limits each user's listeners. Access lists come first in every mount's decision, then authentication, then the intro and the preroll.

fallback-override is accepted and changes nothing: listeners always come back to the mount's own source. Directory listings (YP), relays and master/slave setups, fileserve, alias and security are not available and warn.

A complete example​

This server keeps an automated backup behind its live mount, protects a members mount with an htpasswd file, writes an access log, and reloads on SIGHUP:

icecast-server-complete.liq
# Created once, outside `parameters`: after a reload, the mount keeps
# following the same source.
backup =
ffmpeg.encode.audio(
%ffmpeg(%audio(codec = "libmp3lame", sample_format = "fltp")),
mksafe(playlist("/srv/radio/backup"))
)

def parameters() =
members = icecast.server.decision.htpasswd("/etc/radio/members.htpasswd")
{
hostname = "radio.example.org",
source_password = "change-me",
max_clients = 500,
access_log = "/var/log/radio/access.log",
mount =
fun (path) ->
if
path == "/live.mp3"
then
{max_listeners = 200, fallback = {source = backup}}
elsif
path == "/members.mp3"
then
{listener_decision = members, fallback = {mount = "/live.mp3"}}
else
null
end
}
end

server = icecast.server(parameters())

server.on_source_connect(
fun (r) ->
log.important(
"DJ connected on #{r.mount} from #{r.agent}"
)
)

ignore(
on_signal(
"hup",
fun () ->
begin
server.reload(parameters())
server.reopen_logs()
end
)
)

See also​

  • Harbor: the input that receives streams, which icecast.server builds on.
  • Harbor HTTP: serving your own HTTP endpoints, on the same port when registered before icecast.server.
  • HLS output: another way to deliver streams to listeners.