This is the first release candidate for the 2.5.x release cycle!
TODO: more details!
2.5.0 (unreleased)
New:
- Added
getter(t)type annotation syntax for getter types (#5078). - Added
atomic(f), which runsfwithout letting any other atomic section run at the same time. Script code now runs on several cores at once, so a group of changes that must not be observed half-done — writing several references together, or checking a flag and setting it — needs to say so. - Added
r.exchange(v), which sets a reference and returns the value it replaced in one
indivisible step. Now that script code runs on several cores at once, a flag guarding
work that must happen only once cannot be read and set separately: two threads both see
it unset and both do the work. - Added
source.contentoperator returning an associative list of frame field names to their content format,track.formatreturning the content format of a single track, andformat.descriptionreturning a typed record description of a content format. - Renamed the internal video content type from
canvastoyuv420p, which better reflects the
actual content; the content itself is still organized as a canvas ofyuv420players. Type
annotations such assource(video=canvas)must be updated tosource(video=yuv420p). - Added subtitles support as a dedicated content type. Includes native SRT decoding,
FFmpeg subtitle encoding/decoding (%subtitle), passthrough for bitmap subtitles
(%subtitle.copy), callbacks (on_subtitle), transformations (subtitles.map),
and dynamic insertion (subtitles.insert) (#4860, #4861, #4862). - Video dimensions (
video.frame.width/height) are now automatically detected
from the first decoded video file. This can be disabled by settingsettings.video.detect_dimensions
tofalseor by explicitly setting the video dimensions. - Added support for XML
.nfosidecar files as a metadata source (enable_nfo_metadata,
file.nfo.metadata) (#4910). - Added Icecast-compatible streaming server (
icecast.server) with support for source
authentication, mount points, relay, and per-listener encoding (#4915). It writes icecast's
access, playlist and error logs, reports listener sessions through callbacks, serves an admin listener
page, and hashes listener IPs by default (#5408). Its configuration file is reloaded on
SIGHUPorreload()without disconnecting sources or listeners (#5418). - Added
on_signal, which runs a function when the process receives a signal such as
SIGHUPorSIGUSR2, and returns areleasemethod (#5418). - Added dedicated encoder mode to
output.harbor: mount points can now be served with
a single shared encoder instead of per-listener encoding (#5003). - Rewrote JACK I/O using native OCaml bindings, removing the dependency on the
bjack
C library (#5028). - Added
settings.clock.child.max_buffer, the amount of data an operator such as
crossfadeorstretchmay buffer from its child source. Operators sharing a child
source must consume it at converging rates; past this value, an error names the operator
that fell behind instead of letting the buffer grow without bound (#5267). - Source callbacks (
on_track,on_metadata,on_frame,on_connect, ...) now return a
releasemethod taking the callback back, so a function registering on a source it is given
can release what it registered instead of keeping it alive for as long as that source lives.
Existing scripts are unaffected, the returned value being still ignorable; see the new
source callbacks page. - Added
source.collect_callback_releases, which runs a function and gathers the callbacks
it registered on a given list of sources into a singlerelease. This is whatswitchand
crossuse, available to scripts calling their own functions more than once. - Clocks created by script code running after startup are now started by the code that
created them, through an effect handler scoped to that code, instead of a global registry
polled after every script application. - Added the
source(_)type: a source whose content is unknown. Any source can be used where
one is expected, so sources of different content can be held together, for instance in a list,
without unifying their content types, while nothing can be assumed about what asource(_)
streams. - Added
domaintothread.run, pinning the function and every rerun to the
scheduler worker on that domain. An object is only collected by a GC on the
domain that allocated it, which makes this the way to ask for one. http.transport.tlsandhttp.transport.sslread their certificate and key again once a
day, so a renewed certificate applies without a restart or dropping connections. The
reload_onargument changes when,reload()applies it at once, and the certificate and key
arguments now accept getters (#5418).- Added Fedora packages for the current Fedora release, on
x86_64andaarch64, and a
matching dnf repository:curl -fsSL https://repo.liquidsoap.info/setup.sh | sudo sh
now configures it on Fedora as it does apt and apk elsewhere.
Changed:
-
Liquidsoap now requires OCaml 5.5 to build.
-
Removed the deprecated
input.udpandoutput.udpoperators. Useinput.srt/output.srt
for low-latency network streaming. -
ffmpeglog messages now go through liquidsoap's logger by default, with the same timestamps, log file and levels as the rest of the logs. Setsettings.ffmpeg.log.capture := falseto print them to standard error as before. -
output.harbor'son_connectandon_disconnectcallbacks now receive a listener record with
its session duration and bytes sent, and a newlistenersmethod lists connected listeners. The
ipfield no longer includes the client port (#5408). -
Scheduled work — requests, harbor clients, script callbacks,
thread.runhandlers — is now fully concurrent,
taking advantage of OCaml 5's core-based concurrency, so a busy instance keeps up with much more of it at once.
Heavy work in a callback or a request resolution no longer stalls playback either. -
Added
settings.scheduler.legacy, a fail-safe for a script that concurrent execution breaks: tasks run on
threads, one at a time, as they did before. It will be removed in a later version once the concurrent
scheduler has settled. -
Removed optional
faaddecoder, superseded byffmpeg(#5444) -
The
prometheus.latencymetrics end in_ratioinstead of_seconds: their values are ratios of the frame duration. -
Deprecated
settings.scheduler.generic_queues,settings.scheduler.fast_queuesand
settings.scheduler.non_blocking_queues: the scheduler sizes itself from the number of cores and there is
nothing left to tune. They now only configure the legacy scheduler, and setting them without it logs a
warning.settings.scheduler.blocking_tasksreplaces them, limiting how many slow tasks — request
resolutions,thread.runhandlers, last.fm submissions — may run at once. It defaults to one per
domain and never fewer than 8, so a machine with few cores keeps room to run several at once. Raising it pays off
when those tasks truly wait. A task that uses a core instead of waiting on one, such as probing a
file for its decoder, only takes cores the streaming threads need. -
Clocks run as scheduler tasks rather than each on a thread of its own. A clock that is ahead
of real time parks and is resumed by the scheduler's timer, so a machine running many outputs
has as many busy threads as cores instead of one per clock competing for them. A clock
catching up yields its domain to the others after each burst of frames. Ticks run directly on
a scheduler domain, one at a time, ahead of request resolution.
A clock driving JACK input or output keeps a thread of its own, since it rests by waiting on the
JACK server.settings.clock.task := falserestores a thread per clock everywhere. -
When the scheduler is busy, quick work is served before slow work: the server, then request resolutions, then
long tasks such as last.fm submissions. The order used to be arbitrary and often favoured the slow ones. -
Bindings written without
letaccept the same targets aslet— destructuring patterns, field paths and type
annotations — so(x, y) = (1, 2),r.field = 1and(n : int) = 2are all valid, and an invalid left-hand side
reports what is allowed instead of a bare syntax error. A leading binding inside the{ … }function shorthand
still requireslet, since{ x = 1 }is a record literal. -
String interpolation accepts any expression:
"#{m["key"]}","#{ {a = 1}.a }"and nested interpolated strings
now work. Error positions inside#{ … }point at the offending code rather than at the start of the string. -
http.transport.sslandhttp.transport.tls:keyparameter is now optional in server mode when the certificate file also contains the private key. -
switch,fallback,rotate,random: replaced the parallel list parameters
(transitions,transition_length,override,track_sensitive,
replay_metadata,single,weights) with per-source composition methods
(composition_type,track_sensitive,replay_metadata,single,weight,
on_select,on_leave) registered on every source, switching behavior now
defaulting automatically based on whether a source is file-based or live. See the
new source composition page, and
doc/content/migrating.mdfor the parameter-by-parameter mapping (#5074). -
Switching now fades by default: a switch cutting into a playing track fades the
leaving source out over up tosettings.source.composition.max_fadeseconds (default
1.) when it carries only PCM audio, handoffs happening at a track boundary being
unchanged since nothing was interrupted. Usesource.composition.legacy_on_selectto
restore the previous behavior. -
Deprecated
fallback.skip. A source withtrack_sensitiveset tofalsecan be cut
into mid-track and thefilecomposition profile skips a source it was left in the
middle of, sofallback([main, fallback_source.{track_sensitive = getter(false)}])
now does natively what the operator polled for on every frame. -
Simplified the
cross/crossfadeimplementation:start_durationandend_durationare
replaced by a singledurationparameter, the metadata overridesliq_cross_start_duration
andliq_cross_end_durationbyliq_cross_duration, and thestart_duration()and
end_duration()methods bycross_duration(). Autocue-specific code and the
assume_autocuesetting are removed (#4893). -
Add metadata from all sources/tracks in
addoperators (#4892). -
Make sure script fails on
on_closeerrors inoutput.fileto prevent
fatal errors from being ignored. Usereopen_on_errorto ignore errors
from the callback. -
Allow implicit casting of an integer as a float (#2887).
-
Removed
settings.decoder.ffmpeg.max_interleave_delta. The setting was never read by
the decoder; stream interleaving is bounded by
settings.decoder.ffmpeg.max_interleave_durationalone. -
Overhauled the FFmpeg filter graph implementation, fixing frame loss, dropped
track marks and a hang. A graph is now a single source whose outputs share one
buffer and one set of track marks, so a track boundary cuts all of them,id
on any output names the graph, and outputs consumed at diverging rates raise
pastsettings.ffmpeg.filter_max_buffer. -
xml-lightandminiyamlare now required dependencies ofliquidsoap-lang, making XML and
YAML support always available. YAML integers parse asintinstead offloat(#5233). -
YAML parsing and rendering now go through
miniyaml, which covers the subset of YAML usually
hand-written in configuration files. Block scalars (|and>), anchors, aliases, tags and
multi-document streams are now rejected with an explicit error instead of being parsed, and
yaml.stringifylost itsscalar_styleandlayout_stylearguments: it always renders in
block style with plain scalars, quoting only where needed. -
FFmpeg encoders and decoders now pick their thread count automatically, as the
ffmpeg
command-line tools do, instead of running on a single thread. Transcoding no longer
bottlenecks on one core: an%ffmpegoutput usinglibx265and a 4K input is about 2.5
times faster. Passthreads=1to an%ffmpegencoder to get the previous behavior back
(#5014). -
Video scaling can be split across cores through
settings.ffmpeg.scaling_threads, as
ffmpegdoes through its filter graphs. It defaults to1, scaling on the calling thread:
splitting a frame costs a fan-out and a join every frame, which a stream held to real time
pays continuously and, on our measurements, does not earn back.0uses one thread per
core (#5014). -
Removed daemon mode: the
-d/--daemoncommand-line option, thesettings.init.daemon
settings and the pidfile they wrote. Detaching from the terminal meant forking, which is
unsafe now that liquidsoap runs on several cores. Use a service manager such assystemd
orlaunchdto run liquidsoap in the background; both handle pidfiles, log redirection and
privilege dropping.
Fixed:
-
Active inputs such as
input.ffmpegreplaced throughsource.dynamicare now stopped and
released. They used to keep their connection, decoder and threads alive for the lifetime of
the script (#5389). -
A transition that drops the incoming source no longer drops that track's announcement with
it.crosshad already consumed the metadata into the buffer it hands the transition, so
the track played on, audible and unannounced, once the transition was over. It is now
announced on the tail, where that audio starts, and only when the transition did not
announce it itself (#5360). -
crossno longer replays either track's metadata into the transition. The outgoing one was
announced a second time by a transition keeping that source, and with anadd-shaped
transition both replays landed at the same frame position, so only one survived and the
incoming track's own announcement was dropped. The incoming one is already inside the
buffered data it was replayed into, so it only ever fired when there was nothing to replay,
announcing an empty metadata. The transition still receives both as themetadatafield of
its arguments, so a transition that read a track's fade overrides off the stream must now
read them from that argument, ascross.simpleandcross.plotdo (#5360). -
metadata.deduplicateno longer compares across a track mark, which dropped the metadata of
a track repeating the one before it, e.g. a single file on a loop (#5360). -
time.zone.setnow takes effect. Setting the time zone was silently ignored once anything had already
read the local time, which in practice meant always. -
Rendering a type no longer goes through
Format.str_formatter, which is per-domain and shared with whatever else
formats a value there: a type printed from a scheduler task came out empty. -
Log entries written during shutdown are no longer dropped: the logging thread returned without a final flush.
-
Error messages naming a type no longer come out with the type missing, which could happen when the error was
raised from a callback or athread.runhandler. -
The last lines of the log are no longer lost on shutdown.
-
Callbacks a script registers on the sources
switchandcrosshand toon_select,
on_leaveand transition functions are now released when the selection or the crossing
ends, instead of accumulating on those sources for as long as they live. -
An FFmpeg filter graph is no longer finished off by an input that runs dry. A source
going unavailable for a passing reason, abufferfilling up or a queue waiting on a
request, still tears the graph down, since avfilter cannot reopen a graph that has seen
end of file, but the graph is now rebuilt when the input comes back, each generation
announced as a new stream so its timestamps line up with the last (#3944). -
An FFmpeg filter graph dropped the tail its filters were holding at end of
stream: the sink never asked its duration converter for what it had left. A
5.00s source throughloudnormproduced 2.08s, and now produces 4.98s. -
Fixed the ffmpeg decoder leaving up to ~150ms of silence after a seek: the
packets between the seek point and the target were dropped without being
decoded, so codecs carrying state across packets, mp3 and its bit reservoir
in particular, started cold. This is what made a CUE playlist click between
tracks (#3971). -
Anything logged while evaluating the script, deprecation warnings in
particular, was silently dropped when liquidsoap exited before reaching the
streaming loop, e.g. onNo output defined, nothing to do.(#3375). -
Liquidsoap no longer exits when started as
root: it now logs a warning
instead. Container detection through/proc/1/cgroupdoes not work under
cgroup v2, which made the check fire on plaindocker run(#3406). -
Fixed
output.fileand the other piped outputs ignoring the delay returned by
reopen_on_errorwhen the failure happened while opening the file: the output
staysIdlein that case and was retried on every streaming cycle (#2437). -
output.filereportsUnixerrors, e.g.Permission deniedwhen creating the
destination directory, assystemerrors, like it already did forSys_error
(#2437). -
Fixed
%mp3segfaulting on musl-based systems such as Alpine: the lame
bindings held their 144KB output buffer on the stack, more than a musl thread
stack can take (#3936). -
rotateandrandomname the switch they build internally after themselves,
e.g.schedule_rotate, instead of leaving it with an anonymous id (#3851). -
Fixed
clock.createdocumenting and reporting sync modes,"CPU"and
"unsynced", that it does not accept. The accepted values are"auto",
"cpu","none"and"passive". -
Fixed
file.watchlosing track of a file that gets replaced rather than written in
place: an inotify watch follows the inode, so a writer doing the usual
write-to-temporary-then-rename silenced the watcher for good, and the containing
directory is watched instead.playlistwithreload_mode="watch"also waits for a
burst of events to settle before reloading, so a writer that truncates before writing
no longer triggers two racing reloads and plays tracks out of order (#3343). -
Fixed HLS segment boundaries drifting away from
segment_duration: a segment
closing on a stale split position re-anchored the next boundary on it instead
of the segment grid. The drift rate depends on the encoder's frame size, so
variants using different codecs, e.g. HE-AAC and AAC-LC, diverged from each
other without bound (#5319). -
The names liquidsoap's syntactic sugar expands to are no longer writable in a script: they now start with an
underscore followed by a digit, which the lexer does not accept as an identifier, so user code cannot shadow what
let eval,let json.parse,let xml.parse,let yaml.parse,let sqlite.row/sqlite.queryor%argsofexpand
to._nullis unchanged, being a real callable builtin. -
Record fields are evaluated when the record is built, whether or not they are ever invoked. Previously a field of a
record that was invoked directly, as in{a = 1, b = f()}.a, was dropped along with its effects when running a
script normally, but not under--interactiveor in the standard library, and not if the record was bound to a name
first. -
Inline
ffmpeg.encode.*operators report unrecognized codec options, as the
container encoder does. -
Fixed
%ffmpegcopy encoder initializing the video stream twice and setting the
average frame rate on the one it discarded, so copied video carried no frame rate. -
Fixed inline
ffmpeg.encode.*operators losing their codec options after the
first track: the encoder consumed them from the format's own table, which was
then empty when the encoder was rebuilt at the next track boundary. -
Fixed
%ffmpegvideo streams being printed as%%video(...). -
Fixed FFmpeg stream descriptions listing video and subtitle streams in reverse
order. -
Fixed
delaygetting stuck after its first track and dropping the following
track's metadata (#5282). -
Fixed
sequencedropping the first chunk of a source, along with its
metadata, when the source it follows leaves no room in the current frame.
This showed up as the track after across/crossfadetransition built with
sequencekeeping the previous track's metadata until the next track
boundary (#5296). -
Fixed
output.file.hlsterminating the process on transient filesystem
errors. The output now supports areopen_on_errorcallback (#5259). -
Fixed
output.file.hlsaccepting unwritable output, temporary and
persist_atdirectories during initialization (#5261). -
Fixed ID3 timed metadata missing from first mpegts HLS segment: metadata is
now registered before the segment is opened and before data is encoded (#5084). -
Make active
stereotoolreally be active.. (#4882) -
Fixed
fMP4HLS support for audio+video streams (#4841) -
Fixed crossfade clocks inconsistencies leading to weird caching audio glitches (#4851)
-
Fixed first frame of new track getting lost when crossfading with
fade.induration is set to0.(#4887) -
Fixed race condition when registering harbor endpoint and mounts (#5292)
-
Fixed crash with concurrent
stop/startoperations (#4849) -
Fixed metadata leak from underlying ffmpeg
oggdemuxer inffmpegstream
decoder (#4848) -
Make sure
output.filedoes not create files without data (#4899) -
Fixed deadlock when a GC finalizer logged from inside another log call: the log
queue no longer allocates while holding its mutex. -
Added an
overrideparameter toinsert_metadata: whenfalse, metadata the source
provides itself takes precedence over the inserted one. The composition profiles use it
so that replaying metadata on selection never overwrites a fresh track's own metadata.