Protocols in Liquidsoap

When Liquidsoap plays a track, it doesn’t just magically appear — it has to be resolved from somewhere. That “somewhere” could be a local file, a remote URL, a database entry, or even something generated on the fly.

That’s where protocols come in. Protocols are little rules that tell Liquidsoap:

“If you see a request in the form protocol:arguments, here’s how to turn it into a real file or stream you can play.” 🎯

For example:

http://www.example.com/song.mp3
say:Hello world!
s3://my-bucket/path/to/file.mp3

In each case, the prefix before the : is the protocol, and the part after it is the argument passed to your resolver code.

Built-in and custom protocols

Liquidsoap already ships with many ready-made protocols, written in the Liquidsoap scripting language. You can explore them in the protocol reference. But the real power comes when you define your own.

The anatomy of a protocol

Every protocol is defined by a handler function. The handler:

  1. Accepts the protocol arguments and some extra helper parameters.
  2. Returns a single resolved URI
  3. Can call other protocols in sequence, building a chain of transformations.

The function always gets two special variables:

  • ~rlog → A logging function. Use it to write debug or info messages that stay attached to the request.
  • ~maxtime → A UNIX timestamp after which your resolver should give up.

The process.uri helper 🛠️

Before diving into the examples, it’s important to understand process.uri, a convenient helper for creating URIs of the form:

process:<binary> <arguments>

When Liquidsoap encounters such a URI, it will automatically execute the given command and cancel it if it exceeds ~maxtime.

If you provide a uri argument, Liquidsoap will first resolve that URI to a local file. Your command can then use two placeholders:

  • $(input) → replaced with the local file resolved from the uri argument (only if uri is provided).
  • $(output) → replaced with the path to a temporary file whose extension is taken from the extname argument.

⚠️ The output file is created empty before the command runs, to prevent race conditions on file ownership. This means your process must be able to overwrite it.

By using process.uri, you can safely wrap external commands in a way that’s time-bound, predictable, and integrates smoothly into Liquidsoap’s request resolution chain.

Example 1 — Fetching from S3 ☁️

Let’s say your files live on Amazon S3, and you want Liquidsoap to fetch them on demand:

def s3_protocol(~rlog, ~maxtime, arg) =
  extname = file.extension(leading_dot=false, dir_sep="/", arg)
  process.uri(extname=extname,
              "aws s3 cp s3:#{arg} $(output)")
end

protocol.add("s3", s3_protocol,
             doc="Fetch files from S3 using the AWS CLI",
             syntax="s3://bucket/path/to/file")

Now a request like:

s3://my-bucket/song.mp3

will be downloaded locally and returned as the playable URI.

Example 2 — Database lookup 📀

Protocols can also be dynamic. For instance, you might store file paths in a database keyed by track IDs:

def db_lookup_protocol(~rlog, ~maxtime, arg) =
  string.trim(process.read("psql -t -c 'SELECT path FROM tracks WHERE id=#{int_of_string(arg)};'"))
end

protocol.add("db_lookup", db_lookup_protocol,
             doc="Fetch file path from database by track ID")

Now you can request:

db_lookup:42

and Liquidsoap will resolve it via your database.

Example 3 — Adding preprocessing

Want to normalize audio before playing? Or apply a voice-over?

def normalize_protocol(~rlog, ~maxtime, arg) =
  process.uri(extname="wav",
              uri=arg,
              "normalize-audio $(input) $(output)")
end

protocol.add("normalize", normalize_protocol,
             doc="Normalize audio levels before playback")

You can chain protocols too:

normalize:cue_cut:s3://my-bucket/file.mp3

Liquidsoap will fetch from S3 → cut the segment → normalize it → play.

Chaining magic

The real beauty of protocols is chaining. Each protocol resolves to a single URI, which can then be handed off to the next one in the chain. This means you can build complex request pipelines:

voiceover:normalize:db_lookup:1234

💡 Here, db_lookup fetches a path, normalize evens out the audio, and voiceover mixes in an announcement.

Tips for writing robust protocols

  • Always respect ~maxtime to avoid long-hanging processes.

  • Use ~rlog generously for debugging:

    rlog("Downloading from S3: #{arg}")
  • Keep your commands secure — if you interpolate arg into a shell command, validate or escape it.

  • Test each piece of the chain independently before combining them.

By mastering protocols, you’re not just telling Liquidsoap where to find your content — you’re designing the path it takes to get there. That’s a superpower in streaming workflows, letting you pull from anywhere, process in any way, and still keep things flowing smoothly. 🚀