🎚️ Multitrack Support in Liquidsoap
Starting in version 2.2.0, Liquidsoap gained support for multitrack operations — a powerful addition that lets you work with individual tracks inside your media files. Whether it’s audio, video, or metadata, you can now demux, remux, encode, decode, apply filters, and more — all at the track level!
This unlocks advanced media workflows, like keeping multiple language tracks, adding a default video stream to audio-only files, or even mixing and matching content across sources.
Let’s walk through what this means, step by step 👇
🧠 What is a Track?
A media file can contain multiple “tracks.” Think of a movie file with:
- 🎧 English and French audio tracks
- 🎥 One video track
- 📝 Metadata like title and artist
In Liquidsoap, these tracks are made accessible through operators that let you manipulate them individually. This opens up many possibilities — but also introduces a few new concepts and conventions to learn.
🔧 Requirements: When You Need FFmpeg
Some multitrack features rely on FFmpeg, while others don’t:
Feature | Requires FFmpeg? |
---|---|
Track-level encode/decode | ✅ Yes |
Encode or decode multiple audio or video tracks | ✅ Yes |
Demuxing/remuxing tracks | ❌ No |
Source track manipulation | ❌ No (unless decoding/encoding) |
To fully unlock multitrack functionality, make sure your Liquidsoap is compiled with FFmpeg support.
🎬 Using Multitrack Media
Let’s say you have a media file with multiple tracks, like:
movie.mkv
├── audio (English)
├── audio_2 (French)
└── video
You can create a source with this file like so:
s = single("/path/to/movie.mkv")
By default, only the first audio and video track will be used:
output.file(%ffmpeg(%audio.copy, %video.copy), "/path/to/copy.mkv", s)
🪵 Logs will confirm the detected tracks:
[output_file:3] Content type is {audio=ffmpeg.copy,video=ffmpeg.copy}.
[decoder.ffmpeg:3] Requested content-type for "/path/to/movie.mkv": {audio=ffmpeg.copy,video=ffmpeg.copy}
[decoder.ffmpeg:3] FFmpeg recognizes "/path/to/movie.mkv" as video: {codec: h264, 1920x1038, yuv420p}, audio: {codec: aac, 48000Hz, 6 channel(s)}, audio_2: {codec: aac, 48000Hz, 6 channel(s)}
[decoder.ffmpeg:3] Decoded content-type for "/path/to/movie.mkv": {audio=ffmpeg.copy(codec="aac",channel_layout="5.1",sample_format=fltp,sample_rate=48000),video=ffmpeg.copy(codec="h264",width=1920,height=1038,aspect_ratio=1/1,pixel_format=yuv420p)}
Here, audio_2
is present but unused. Let’s fix that
👇
🎛️ Custom Track Handling
What if you want to keep both audio tracks, re-encoding the second one to stereo?
output.file(
%ffmpeg(
%audio.copy,
%audio_2(channels=2, codec="aac"),
%video.copy
),"/path/to/copy.mkv",
s )
🪵 Logs now show audio_2
being processed:
[output_file:3] Content type is {audio=ffmpeg.copy,audio_2=pcm(stereo),video=ffmpeg.copy}.
[decoder.ffmpeg:3] Requested content-type for "/path/to/movie.mkv": {audio=ffmpeg.copy,audio_2=pcm(stereo),video=ffmpeg.copy}
[decoder.ffmpeg:3] FFmpeg recognizes "/path/to/movie.mkv" as video: {codec: h264, 1920x1038, yuv420p}, audio: {codec: aac, 48000Hz, 6 channel(s)}, audio_2: {codec: aac, 48000Hz, 6 channel(s)}
[decoder.ffmpeg:3] Decoded content-type for "/path/to/movie.mkv": {audio=ffmpeg.copy(codec="aac",channel_layout="5.1",sample_format=fltp,sample_rate=48000),audio_2=pcm(5.1),video=ffmpeg.copy(codec="h264",width=1920,height=1038,aspect_ratio=1/1,pixel_format=yuv420p)}
You now have a file with two audio tracks (one copied, one re-encoded) and one video track 🎉
⚠️ Playlist Caveats
If your source is a playlist:
s = playlist("/path/to/playlist")
…and you request multiple tracks:
output.file(
fallible=true,
%ffmpeg(%audio.copy, %audio_2(...), %video.copy),
"/path/to/copy.mkv",
s )
Then only files with all requested tracks (audio + audio_2 + video) will be accepted. Others will be skipped.
🧭 Track Naming Conventions
When decoding, track names follow this pattern:
audio
,audio_2
,audio_3
, …video
,video_2
,video_3
, …
These names are fixed by the decoder — you can’t rename them directly when reading media.
For example, this will fail:
output.file(%ffmpeg(%audio_fr.copy, %audio_en(...), "/path/to/file.mkv", playlist("...")))
Why? Because the decoder doesn’t know what audio_fr
or
audio_en
means. Track names must match what the decoder
emits: audio
, audio_2
, etc.
Once you’re remuxing tracks, however, you can assign any name you want!
🔄 Demuxing and Remuxing Tracks
To extract and rebuild track sets:
s = playlist(...)
let {audio, video, metadata, track_marks} = source.tracks(s)
You can then remix these into a new source:
s = source({
audio = audio,
video = video,
metadata = metadata,
track_marks = track_marks
})
Tracks can also be added or replaced:
s = source(source.tracks(s).{video = source.tracks(image).video})
One limitation is that it is not currently possible to add default tracks.
The following won’t work:
video = source.tracks(s).video ?? source.tracks(image).video
🧹 Cleaning Up Tracks
Want to remove track_marks
?
let {track_marks=_, ...tracks} = source.tracks(s)
s = source(tracks)
This is equivalent to the older drop_tracks
operator.
🔌 Track-Level Operators
Many operators now work directly on tracks. Examples:
mono = track.audio.mean(audio_track)
encoded = track.ffmpeg.encode.audio(%ffmpeg(%audio(codec="aac")), audio_track)
🚨 But beware: some operators (like inline encoders) put the track on a new clock. You’ll need to re-derive metadata and track marks from the new track to avoid clock conflicts:
let encoded_audio = track.ffmpeg.encode.audio(..., audio)
s = source({
audio = encoded_audio,
metadata = track.metadata(encoded_audio),
track_marks = track.track_marks(encoded_audio)
})
🏷️ How Encoders Detect Track Types
Liquidsoap uses naming conventions and hints to determine what kind of data each track holds:
Priority order:
%audio.copy
or%video.copy
→ auto-detect- Named content type (
audio_content
orvideo_content
) - Track name contains “audio” or “video”
- Codec implies type
✅ Example: Explicit typing
output.file(
%ffmpeg(
%en(audio_content, codec=audio_codec),
%fr(codec="aac"),
%director_cut(video_content, codec=video_codec)
),"/path/to/copy.mkv",
s )
✅ Or by naming convention
%audio_en(codec=...)
%director_cut_video(codec=...)
Internally, Liquidsoap maps this to content-type info. Once handed off to FFmpeg, track names are lost — FFmpeg just sees numbered tracks in order.
🚀 Summary
Multitrack support opens up powerful new workflows:
- 🔄 Mix and match audio/video/metadata across sources
- 🧱 Build custom media containers
- 🎯 Target different formats per track
- 🧪 Combine synchronous and asynchronous operations (with care!)
We’ve only scratched the surface — go ahead and explore the code, experiment, and let your creativity flow! 💡
And if there’s an operator you wish worked at the track level, don’t hesitate to open a feature request!