Liquidsoap 2.4.0 — The people’s release! 🧑🏭
⚠️ Note: This blog post was written with the help of a machine.. Because why not! But this sentence and everything else was reviewed by a human. Of course!…
Liquidsoap 2.4.0 is almost here! This one is all about making your life easier — smoothing out rough edges, clearing up long-standing points of confusion, and giving you more tools to write clean, maintainable scripts.
You’ll see some changes that require a small update to your scripts — but each of them is here to solve problems that have tripped people up for years. Think of it as a little spring cleaning for your streaming setup. 🧹
Let’s walk through the most important changes.
🪝 Callbacks: Clearer, Safer, and All in One Place
Callbacks are now standardized and moved to a dedicated section in the documentation. No more hunting around wondering where each one lives or guessing how it’s supposed to work.
The big change: most callbacks are now registered as methods on your source or output, instead of as constructor arguments. This makes your code more modular — you can build your source, then wire up callbacks later when you have the data you need.
s = playlist("music.m3u")
s.on_metadata(synchronous=false, fun (m) -> log("New track: #{m["title"]}"))
Notice the synchronous
parameter? It’s required now.
synchronous=false
→ runs asynchronously in a separate task (safe if your callback might take time)synchronous=true
→ runs inside the main streaming loop (fast callbacks only!)
In the past, many users accidentally slowed down their whole stream because a callback took too long. Now you’re forced to think about it — and do the right thing. ✅
⚠️ Warnings When Overwriting Top-Level Variables
Ever accidentally do this?
request = ...
# Later...
request.create(...) # 💥 Cryptic type error!
It was far too easy to overwrite important built-in modules (like request
) and end up with confusing type errors. Now Liquidsoap will warn you before you shoot yourself in the foot. A small safeguard, big peace of mind.
🚫 No More null()
Headaches
Previously, null
was a function — you had to call null()
to get a null value, or null(value)
to wrap something. This confused a lot of people (and the typechecker wasn’t helping).
Now, null
can be used directly:
my_var = null
Function form still works if you need it:
my_var = null("some value") # Explicit nullable
Cleaner syntax, fewer “Wait, why isn’t this working?” moments.
✨ Destructuring & Enhanced Labelled Arguments
Function arguments just got way more flexible. You can now destructure right in the parameter list:
def print_data({ title, artist }) =
log("Now playing: #{artist} — #{title}")
end
And with enhanced labelled arguments, you can keep APIs clear without shadowing important names:
def handle_track(~request:r) =
log("Request URI: #{request.uri(r)}")
end
No more awkward renaming or risking collisions with top-level modules.
⏰ Cron Support for Scheduling Tasks
You asked for it: Liquidsoap now understands cron syntax. Schedule actions at precise times — just like your system cron job.
cron.add("0 * * * *", fun () ->
q.push(request.create("hourly_jingle.mp3"))
)
Want something to happen exactly on the hour? Easy. Need a special track every day at 5 PM? Done. This is going to make time-based automation much more familiar and powerful.
🔐 TLS Client Certificate Validation
Thanks to a contribution from @DelilahHoare, Liquidsoap can now validate SSL certificates provided by clients using our TLS backend. This feature adds an extra layer of security for scenarios where you want to ensure that only trusted clients can connect. We’re especially glad to see contributions to the OCaml core — and we’d love to welcome more contributors there!
🛠 Other Notable Changes
- LUFS-based loudness correction per track 🎚 — now unified with ReplayGain via
normalize_track_gain
. liquidsoap.script.path
variable — find out where your running script lives.- Better memory usage — initial compaction now on by default.
- Improved source & clock naming for clearer logs.
- Removed old, unmaintained ImageLib support.
- New external decoder API for safer, easier file-to-file decoding.
- Deprecated
insert_metadata
operator in favor of a defaultinsert_metadata
method.
🐛 Important Fixes
A few of the most impactful bug fixes:
- Autocue’s
start_next
now behaves as expected (but may slightly change existing output). - Fixed crashes with SRT on Windows.
- Memory leak in FFmpeg inline encoder is gone.
- More reliable playlist reloads (no race conditions).
- No more mysterious errors for scripts in paths with non-ASCII characters.
📄 Migration Cheatsheet
If you’re upgrading an existing script, here are the key before/after changes you might need to make.
1. Callbacks: moved to methods, synchronous
required
Before:
output.icecast(%vorbis, host="...", port=8000, password="...",
mount="stream.ogg", on_connect=fun () -> log("Connected!"))
After:
o = output.icecast(%vorbis, host="...", port=8000, password="...",
mount="stream.ogg")
o.on_connect(synchronous=false, fun () -> log("Connected!"))
2. null
can be used directly
Before:
x = null()
After:
x = null
3. Destructuring & labelled arguments
Before:
def handler(m) =
let { title, artist } = m
log("#{artist} — #{title}")
end
After:
def handler({ title, artist }) =
log("#{artist} — #{title}")
end
Renaming labelled arguments:
def handle_track(~request:r) =
log(request.uri(r))
end
4. Top-level overwrite warnings
No script changes needed — but if you see:
Warning 6: Top-level variable request is overridden!
…consider renaming your variable.
5. insert_metadata
now a method
Before:
s = insert_metadata(s)
s.insert_metadata([("title", "My Song")])
After:
s.insert_metadata([("title", "My Song")])
Wrapping Up
Liquidsoap 2.4.0 may not be the flashiest release, but it’s one of the most user-focused in recent memory. By clearing up long-standing points of confusion, standardizing APIs, and adding features like cron support, we’ve made it easier than ever to write scripts that are both powerful and maintainable.
As always, check the migration notes before upgrading — especially for callback changes — and enjoy a smoother scripting experience. 🚀