The second release candidate of liquidsoap 2.5.0 is out! 🎉✨
This is still a release candidate: it is feature-complete and we are looking for as much testing as we can get before the final release. Please try it on a staging environment with your real scripts and report any regression or issue to us. We do not recommend running it in production yet.
If you are upgrading from 2.4.x, make sure to read the migration notes. We start with what changed since rc1, followed by a reminder of the major changes in 2.5.0.
What changed since rc1
Core components rebuilt from their specification
Most of the work since rc1 went into rebuilding some of the most used parts of liquidsoap: the FFmpeg layer, output.harbor, icecast.server, the HLS output, SRT and clocks. For each of them, we first wrote down a precise specification of what the component should do, and then rebuilt it from that specification. Most scripts keep working as they are, and the migration notes list the exceptions.
The result is a lot of long-standing issues going away, HLS and SRT in particular. HLS variants are now all cut on the same grid, with segments that stay within their target duration and playlists that players can rely on. SRT peers no longer stall the clock or each other, inputs recover after reconnecting, and streams are no longer cut short at stop.
These are large rewrites, though, so this is where we need your testing the most! If you use any of these operators, please give this release candidate a try and let us know how it goes.
Stricter, faster typechecking
A source passed to an output must now have exactly the tracks its format encodes. Extra tracks used to be dropped silently, so in some cases an output with an audio and video format ended up encoding an audio-only stream without telling you. This is now a type error. Use source.drop.video and similar operators to drop a track explicitly.
Scripts also typecheck about four times faster and with less memory: checking the standard library without cache went from 10s to under 3s!
The new %typeof(e) type annotation stands for the type of e, which is handy to read back a saved state without having to write out its type:
let json.parse (saved : %typeof(history())) = file.contents("history.json")Major changes in 2.5.0
Liquidsoap now runs on several cores
Liquidsoap 2.5 requires OCaml 5.5 and takes advantage of its core-based concurrency. Scheduled work, that is request resolutions, harbor clients, script callbacks and thread.run handlers, now runs on several cores at once and in parallel with the streaming loop. A busy instance keeps up with much more scheduled work, and heavy work in a callback or a request resolution no longer stalls playback. Clocks run as scheduler tasks too, and FFmpeg encoders and decoders pick their thread count automatically, as the ffmpeg command-line tools do.
Most scripts need no change. The exception is a script where several callbacks share references: two callbacks can now run at the same time, so one may read the references while another is still updating them. The new atomic function groups these updates so that every callback sees them all at once:
def remember(m) =
atomic(
{
current_title := m["title"]
current_artist := m["artist"]
}
)
endIf concurrent execution breaks your script and it cannot be fixed right away, settings.scheduler.legacy := true brings back the previous behavior. If you need it, please let us know what broke: settings.scheduler.legacy will be removed in a later version.
Source composition
switch, fallback, rotate and random have been reworked to "do the right thing" in most default cases. Parameters such as track_sensitive or weights are replaced by methods on each source, and the switching behavior now defaults from what the source is: a file-based source is left at the end of its track, a live source can be cut into at any time.
# Before
radio = fallback(track_sensitive=false, [live, music])
schedule = rotate(weights=[3, 1], [music, jingles])
# After
radio = fallback([live, music])
schedule = rotate([music.{weight = 3}, jingles])Switching also fades by default: a switch cutting into a playing track fades the leaving source out instead of cutting it. cross and crossfade have been simplified along the way, with a single duration parameter replacing start_duration and end_duration. See the source composition page for the details.
An Icecast-compatible server
The new icecast.server makes liquidsoap act as an Icecast server: source clients such as butt or Mixxx connect to it, listeners connect to its mount points, and icecast.server.config.load turns an existing Icecast, RSAS or Icecast-KH XML configuration file into its parameters.
Because liquidsoap understands the content of the stream, Icecast features work much better. For instance, a fallback mount is a liquidsoap switch inside the mount's output, so listeners stay connected to the same mount instead of being moved between mounts and reconnecting.
We intend icecast.server to be an entry point to migrate your Icecast setup to liquidsoap. It is still experimental and some Icecast options are not supported yet, see the documentation for the details.
Subtitles
Subtitles are now a dedicated content type, next to audio and video, with native SRT decoding, FFmpeg subtitle encoding and decoding via %subtitle, passthrough for bitmap subtitles, and operators to read, transform and insert subtitles from a script.
Other notable changes
- The rotation stored in a video file, as phone recordings usually have, is now applied when decoding.
- Video whose size, pixel format or colour properties change in the middle of a stream now plays through
ffmpeg.rawcontent, filters and raw encoders. input.harborwebsocket sources follow the webcast protocol specification.- JACK I/O has been rewritten with native OCaml bindings.
- Daemon mode,
input.udp/output.udpand thefaaddecoder are removed.
Packages
As with rc1, our package repositories can be configured with a single command:
curl -fsSL https://repo.liquidsoap.info/setup.sh | sudo sh -s -- --channel v2.5.0-rc2The full changelog is included below.
As with each release, we want to thank all our users for their patience, early testing and collaboration.
2.5.0-rc2 (2026-11-15)
New:
- Added
icecast.server, an Icecast-compatible streaming server with source authentication, mount points, relay, per-listener encoding, icecast-style logs and limits, and configuration reload onSIGHUPorreload()(#4915, #5414, #5451, #5580). - Added subtitles as a dedicated content type: native SRT decoding, FFmpeg encoding/decoding (
%subtitle,%subtitle.copy),on_subtitle,subtitles.mapandsubtitles.insert(#4860, #4861, #4862). - Rewrote JACK I/O with native OCaml bindings, removing the
bjackdependency (#5028). - Rotated videos, e.g. phone recordings, are decoded upright with their container cropping applied. Disable with
settings.ffmpeg.autorotate := false; see alsotrack.ffmpeg.side_dataandffmpeg.autorotate.on_undecided(#5525). - Video dimensions are detected from the first decoded file. Disable with
settings.video.detect_dimensions := falseor by setting them explicitly (#4881). - Video whose size, pixel format or colour properties change mid-stream now plays through
ffmpeg.rawcontent,ffmpeg.filterand raw encoders. Seesettings.ffmpeg.format_change.*(#5541). - Added a dedicated encoder mode to
output.harbor: each listener gets its own encoder (#5003). - Source callbacks (
on_track,on_metadata, ...) return areleasemethod.source.collect_callback_releasesgathers the callbacks a function registers. See the source callbacks page (#5338). - Added
atomic(f)andr.exchange(v)for script code that must not be observed half-done, now that it runs on several cores at once (#5364). - Added Fedora packages (
x86_64,aarch64) and a dnf repository, configured bycurl -fsSL https://repo.liquidsoap.info/setup.sh | sudo sh(#5473). http.transport.tlsandhttp.transport.sslreload their certificate and key daily (reload_on,reload()), accept getters for them, and no longer requirekeywhen the certificate file contains it (#5450).- Added
on_signalto run a function when the process receives a signal (#5456). - Added the
source(_)type: a source of unknown content, so sources of different content can be held together, e.g. in a list (#5337). - Added
settings.clock.child.max_buffer, bounding what operators such ascrossfadeorstretchbuffer from their child source (#5267). - Added XML
.nfosidecar files as a metadata source (enable_nfo_metadata,file.nfo.metadata) (#4910). - Added
source.content,track.formatandformat.descriptionto inspect content formats (#5096). - Added
getter(t)type annotations (#5078) and%typeof(e), the type ofewithout evaluating it, e.g.let json.parse (saved : %typeof(state())) = ...(#5614). - Added
domaintothread.runto pin a function to one scheduler domain (#5397). - Added an
overrideparameter toinsert_metadata(#5077).
Changed:
- Script callbacks, requests, harbor clients and
thread.runhandlers run concurrently on several cores, and clocks run as scheduler tasks instead of one thread each. Domains are added on demand up tosettings.scheduler.domains(#5539).settings.scheduler.blocking_tasks(default 64) limits concurrent slow tasks and replaces the deprecatedsettings.scheduler.*_queues(#5493).settings.scheduler.legacyandsettings.clock.task := falserestore the previous behavior (#5359, #5397). - The FFmpeg layer was rewritten (#5541). Scripts keep the same syntax and settings. What changes:
- Copied streams start each source on a keyframe of every track, waiting at most
settings.ffmpeg.content.copy.max_keyframe_wait(20 s), and stay in sync across switches and long relays (#2333, #4502, #3329). - Copied files with incompatible stream parameters are skipped when opened.
%audio.rawand%video.rawtake unset parameters from the first frame.- Script and configuration errors fail at evaluation or first start instead of retrying forever.
input.ffmpegtakes atimeout(10 s) (#2358).- Filter graphs share one buffer and track marks across outputs, drop excess beyond
settings.ffmpeg.filter_max_buffer, and are rebuilt when their input comes back (#5302, #5332). - Encoders and decoders pick their thread count automatically (#5388).
threads=1restores single-threaded encoding, andsettings.ffmpeg.scaling_threadssplits scaling. - FFmpeg logs go through liquidsoap's logger.
settings.ffmpeg.log.capture := falserestores standard error (#5469).
- Copied streams start each source on a keyframe of every track, waiting at most
switch,fallback,rotateandrandomreplace their list parameters (transitions,override,track_sensitive,weights, ...) with per-source composition methods. See source composition anddoc/content/migrating.md(#5074).- Switching cuts into a playing track with a fade of up to
settings.source.composition.max_fade(1 s).source.composition.legacy_on_selectrestores the previous behavior (#5077). - A source passed to an output must have exactly the tracks its format encodes, and sources or formats of one list must have the same tracks. Extra tracks used to be dropped silently; use
source.drop.videoand similar (#5627). cross/crossfade:start_duration/end_durationare replaced byduration,liq_cross_*_durationbyliq_cross_duration, and the methods bycross_duration().assume_autocueis removed (#4893). Transitions no longer replay track metadata: a custom transition reads it from its arguments'metadatafield (#5377).- Liquidsoap requires OCaml 5.5 (#5366).
- Removed daemon mode (
-d/--daemon,settings.init.daemon): use a service manager such assystemdorlaunchd(#5392). - Removed
input.udpandoutput.udp(useinput.srt/output.srt), thefaaddecoder (#5444) and the unusedsettings.decoder.ffmpeg.max_interleave_delta. - Renamed the
canvasvideo content type toyuv420p:source(video=canvas)becomessource(video=yuv420p)(#5096). output.harbor:on_connect/on_disconnectreceive a listener record,listenerslists them,acceptfilters them, andipno longer includes the port (#5414). Mounts match literally,override_mountis needed to take over a served mount, and unsynced clocks needallow_unsynced_clock. Removeddumpfileand the deprecatedon_connect/on_disconnectarguments (#5580).harbor.http.registerreturns a value with aremovemethod, replacingharbor.remove. Registering the same path and method twice raises an error (#5580).input.harborwebsockets follow the webcast specification and RFC 6455: errors close with defined codes, pings are answered, and non-string metadata is ignored (#5625).- YAML goes through
miniyaml, now required along withxml-light. Integers parse asint. Block scalars, anchors, aliases, tags and multi-document streams are rejected.yaml.stringifylostscalar_styleandlayout_style(#5233). - Deprecated
fallback.skip: usetrack_sensitive = getter(false)on the fallback source. output.filefails onon_closeerrors unlessreopen_on_errorhandles them (#4879).addkeeps the metadata of all its sources (#4892).prometheus.latencymetrics end in_ratioinstead of_seconds(#5470).- Record fields are evaluated when the record is built, even if they are never used (#5310).
- Bindings without
letaccept destructuring, field paths and type annotations, e.g.(x, y) = (1, 2)(#5310). - String interpolation accepts any expression, e.g.
"#{m["key"]}"(#5310). - Integers are implicitly cast to floats (#2887).
Fixed:
%mp3no longer segfaults on musl systems such as Alpine (#5322).- The FFmpeg decoder no longer leaves up to 150 ms of silence after a seek, which made CUE playlists click (#5324).
output.harbor: listeners joining an Ogg stream late can decode it (#5567, #5547), Ogg streams carry no ICY metadata (#5567), and ICY titles change at their stream position (#5580).- Active inputs replaced through
source.dynamicare stopped and released (#5391). output.file.hlshandles transient filesystem errors withreopen_on_error(#5263), rejects unwritable directories at startup (#5264), and puts ID3 metadata in the first mpegts segment (#5117).- Fixed typechecking running out of memory on some scripts mixing doubly nullable values (#5601).
time.predicateparses predicates combined withand,or,notand parentheses (#5534).- A
crosstransition that drops the incoming source no longer loses that track's announcement (#5383). - FFmpeg filter graphs no longer drop the tail their filters hold at end of stream (#5332).
%ffmpegcopied video carries its frame rate, and inlineffmpeg.encode.*operators report unknown options and keep them past the first track (#5302).delayno longer gets stuck after its first track (#5282).- Fixed a full connection-timeout stall on Windows when a host's first address refuses the connection (#5533).
- Fixed a race when registering harbor endpoints and mounts (#5293).
time.zone.settakes effect (#5368).
Optimized:
- Scripts typecheck about four times faster with less memory (#5600).