github savonet/liquidsoap v2.5.0-rc2
Liquidsoap 2.5.0-rc2

latest release: rolling-release-v2.5.x
pre-release55 minutes ago

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"]
    }
  )
end

If 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.raw content, filters and raw encoders.
  • input.harbor websocket sources follow the webcast protocol specification.
  • JACK I/O has been rewritten with native OCaml bindings.
  • Daemon mode, input.udp/output.udp and the faad decoder 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-rc2

The 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 on SIGHUP or reload() (#4915, #5414, #5451, #5580).
  • Added subtitles as a dedicated content type: native SRT decoding, FFmpeg encoding/decoding (%subtitle, %subtitle.copy), on_subtitle, subtitles.map and subtitles.insert (#4860, #4861, #4862).
  • Rewrote JACK I/O with native OCaml bindings, removing the bjack dependency (#5028).
  • Rotated videos, e.g. phone recordings, are decoded upright with their container cropping applied. Disable with settings.ffmpeg.autorotate := false; see also track.ffmpeg.side_data and ffmpeg.autorotate.on_undecided (#5525).
  • Video dimensions are detected from the first decoded file. Disable with settings.video.detect_dimensions := false or by setting them explicitly (#4881).
  • Video whose size, pixel format or colour properties change mid-stream now plays through ffmpeg.raw content, ffmpeg.filter and raw encoders. See settings.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 a release method. source.collect_callback_releases gathers the callbacks a function registers. See the source callbacks page (#5338).
  • Added atomic(f) and r.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 by curl -fsSL https://repo.liquidsoap.info/setup.sh | sudo sh (#5473).
  • http.transport.tls and http.transport.ssl reload their certificate and key daily (reload_on, reload()), accept getters for them, and no longer require key when the certificate file contains it (#5450).
  • Added on_signal to 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 as crossfade or stretch buffer from their child source (#5267).
  • Added XML .nfo sidecar files as a metadata source (enable_nfo_metadata, file.nfo.metadata) (#4910).
  • Added source.content, track.format and format.description to inspect content formats (#5096).
  • Added getter(t) type annotations (#5078) and %typeof(e), the type of e without evaluating it, e.g. let json.parse (saved : %typeof(state())) = ... (#5614).
  • Added domain to thread.run to pin a function to one scheduler domain (#5397).
  • Added an override parameter to insert_metadata (#5077).

Changed:

  • Script callbacks, requests, harbor clients and thread.run handlers run concurrently on several cores, and clocks run as scheduler tasks instead of one thread each. Domains are added on demand up to settings.scheduler.domains (#5539). settings.scheduler.blocking_tasks (default 64) limits concurrent slow tasks and replaces the deprecated settings.scheduler.*_queues (#5493). settings.scheduler.legacy and settings.clock.task := false restore 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.raw and %video.raw take unset parameters from the first frame.
    • Script and configuration errors fail at evaluation or first start instead of retrying forever.
    • input.ffmpeg takes a timeout (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=1 restores single-threaded encoding, and settings.ffmpeg.scaling_threads splits scaling.
    • FFmpeg logs go through liquidsoap's logger. settings.ffmpeg.log.capture := false restores standard error (#5469).
  • switch, fallback, rotate and random replace their list parameters (transitions, override, track_sensitive, weights, ...) with per-source composition methods. See source composition and doc/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_select restores 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.video and similar (#5627).
  • cross/crossfade: start_duration/end_duration are replaced by duration, liq_cross_*_duration by liq_cross_duration, and the methods by cross_duration(). assume_autocue is removed (#4893). Transitions no longer replay track metadata: a custom transition reads it from its arguments' metadata field (#5377).
  • Liquidsoap requires OCaml 5.5 (#5366).
  • Removed daemon mode (-d/--daemon, settings.init.daemon): use a service manager such as systemd or launchd (#5392).
  • Removed input.udp and output.udp (use input.srt/output.srt), the faad decoder (#5444) and the unused settings.decoder.ffmpeg.max_interleave_delta.
  • Renamed the canvas video content type to yuv420p: source(video=canvas) becomes source(video=yuv420p) (#5096).
  • output.harbor: on_connect/on_disconnect receive a listener record, listeners lists them, accept filters them, and ip no longer includes the port (#5414). Mounts match literally, override_mount is needed to take over a served mount, and unsynced clocks need allow_unsynced_clock. Removed dumpfile and the deprecated on_connect/on_disconnect arguments (#5580).
  • harbor.http.register returns a value with a remove method, replacing harbor.remove. Registering the same path and method twice raises an error (#5580).
  • input.harbor websockets 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 with xml-light. Integers parse as int. Block scalars, anchors, aliases, tags and multi-document streams are rejected. yaml.stringify lost scalar_style and layout_style (#5233).
  • Deprecated fallback.skip: use track_sensitive = getter(false) on the fallback source.
  • output.file fails on on_close errors unless reopen_on_error handles them (#4879).
  • add keeps the metadata of all its sources (#4892).
  • prometheus.latency metrics end in _ratio instead of _seconds (#5470).
  • Record fields are evaluated when the record is built, even if they are never used (#5310).
  • Bindings without let accept 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:

  • %mp3 no 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.dynamic are stopped and released (#5391).
  • output.file.hls handles transient filesystem errors with reopen_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.predicate parses predicates combined with and, or, not and parentheses (#5534).
  • A cross transition 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).
  • %ffmpeg copied video carries its frame rate, and inline ffmpeg.encode.* operators report unknown options and keep them past the first track (#5302).
  • delay no 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.set takes effect (#5368).

Optimized:

  • Scripts typecheck about four times faster with less memory (#5600).

Don't miss a new liquidsoap release

NewReleases is sending notifications on new releases.