# hlsdl and the practical limits of downloading HLS playlists from the command line

> hlsdl is a small C program that downloads video on demand from .m3u8 playlists, with AES-128 and SAMPLE-AES decryption for MPEG-2 transport streams. It is well documented, cross platform and honest about the two things it cannot do: remuxing fragmented MP4 discontinuities and separating audio renditions.

**selsta/hlsdl** — C program to download VoD HLS (.m3u8) files

- Repository: https://github.com/selsta/hlsdl
- Stars: 679 · Forks: 160
- Language: C
- License: MIT
- Published: 2026-10-21 · Updated: 2026-10-21 · Language: en
- Canonical page: https://hysenlabs.com/projects/selsta-hlsdl

## A focused downloader for on demand playlists

hlsdl is a C program for downloading VoD HLS .m3u8 files. The scope is stated precisely: it downloads playlists containing MPEG-2 Transport Streams or fragmented MP4, and it supports AES-128 decryption plus SAMPLE-AES decryption for MPEG-2 Transport Streams.

The word VoD matters. Video on demand means a finished recording with a complete playlist, not a live stream with a sliding window. The program does handle live streams, and the options include a live start offset in seconds and a live stream download duration in seconds, but the download model, and the flags for skipping failed segments, are built around a VOD playlist that you can walk from beginning to end.

The dependencies are the other thing worth knowing before you start. The requirements are CMake 3.21 or later, a C11 compiler, libcurl, OpenSSL through libcrypto, and POSIX threads. That is a small, boring, well understood dependency set, and the fact that OpenSSL is required rather than optional tells you that decryption is a first class feature rather than an afterthought.

The repository is laid out like a classic C project: a CMakeLists.txt at the root, a src directory, a Dockerfile, a man page named hlsdl.1, and a CI workflow. The primary language is C, and the licence is MIT.

## Building on Linux and macOS takes three commands

On Linux the dependencies come from the distribution, and the documented packages for Debian and Ubuntu are build-essential, cmake, libcurl4-openssl-dev and libssl-dev. On macOS the documented line is cmake and openssl through Homebrew.

The build itself is a configure and a build step, and the README is explicit about the release configuration:

```sh
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build
```

After that you either run the binary straight from the build directory as build/hlsdl, or install it with cmake --install build. Installing through CMake rather than copying the binary by hand means the man page and any other artefacts land in the right places.

The macOS caveat is the kind of detail that costs an hour if you hit it cold. If OpenSSL is not found on macOS, you add -DOPENSSL_ROOT_DIR="$(brew --prefix openssl)" to the configure command, because Homebrew keeps OpenSSL in its own prefix rather than in the system location that CMake searches by default.

The Dockerfile in the repository does the same thing in a container, using Alpine in two stages. The builder stage installs cmake, ninja, gcc, libc-dev, curl-dev and openssl-dev, copies the sources, configures with Ninja, builds in parallel and installs. The final stage is a much smaller Alpine image with only curl installed, and it runs as a non-root user with uid 1000, with a volume mounted at /var/hlsdl/data.

## Two unsupported cases that decide whether the tool fits

Most tools in this space are vague about their limits. hlsdl names two specific things it cannot do, and both would matter to anyone using it on a real stream.

The first is remuxing fragmented MP4. Output keeps the original container format, and combining fragmented MP4 segments across discontinuities or initialization data changes is not supported. In other words, if the publisher inserts a discontinuity or changes the initialization segment mid stream, the fragments cannot be joined back into one continuous file.

The second is separate audio and SAMPLE-AES for playlists that use EXT-X-MAP. An EXT-X-MAP playlist carries an initialization section, and the documentation states that separate audio and SAMPLE-AES are unsupported in that case.

The practical rule that follows is simple. If your content is a plain MPEG-2 transport stream, the common case for a recorded television broadcast or a VOD asset, hlsdl handles it including decryption. If it is fragmented MP4 with separate audio renditions, you should look elsewhere, and the ToDo list confirms the gap: remux fragmented MP4 discontinuities and separate audio renditions are the first items on it.

For fragmented MP4 the program does have a useful property: each fragmented MP4 file includes its initialization data, so a single file is self contained even though the sequence cannot be merged.

## The option set covers quality selection, headers and DRM bypass

The full flag list is published in the documentation, which is unusual and helpful. The options that shape a normal download are these:

```
-b ... Automatically choose the best quality.

-W ... Choose largest width lower or equal than this.

-H ... Choose largest height lower or equal than this.

-A ... Select audio language.
```

Quality is selected either automatically or by bounding width and height, and the audio language can be chosen explicitly, which is what you want on a stream that carries several audio tracks.

The output controls are worth separating from the request controls. The -o flag chooses the name of the output file, with a dash as an alias for stdout, so the program can pipe into another tool. The -m flag saves each segment to a numbered file, and the documentation notes it cannot be used together with -o -. The -f flag forces overwriting of the output file, and -q prints less to the console while -v prints more information.

On the request side there is -u for a custom HTTP User-Agent header, -h for a custom HTTP header, -p for a proxy URI, and -C for a file holding cookie data in the old Netscape or Mozilla cookie data format. There is also -c, which treats HTTP code 206 as 200 even if the request was made without a range header, and -s and -i for live start offset and live download duration in seconds, plus -e for a refresh delay.

Two flags deserve attention because they are about circumvention. The -F flag forces the program to ignore detection of DRM, and the -K flag forces an AES key value given as a hex string. The -k and -n flags allow parts of an AES key URI to be replaced, old and new. These exist for legitimate cases where the key URI does not match where the key is actually served, and they are also what a DRM-protected stream would need in order to be downloaded at all.

## Running it in a container, and the output layout that follows

The usage form is ./hlsdl [options] url, and the Docker image mirrors it:

```sh
docker run -v ./data:/var/hlsdl/data --rm -it hlsdl:latest hlsdl [options] url
```

The volume mount is the point of the arrangement. The image declares /var/hlsdl/data as a volume and sets it as the working directory, and the final image copies the installed binary into /usr/local/bin. Mounting your own directory there is what gets the file out of the container, since the container filesystem is discarded when it exits.

Because the container runs as uid 1000 rather than root, the image creates the data directory and chowns it to 1000:1000. That is the detail that trips people up: if your host directory is owned by a different user, the write fails with a permission error that looks like a program bug and is not one. The Dockerfile also uses BuildKit cache mounts on /var/cache/apk for the package installs, so repeated builds reuse the downloaded packages.

The build image itself is available with a single command:

```sh
docker build -t hlsdl:latest .
```

For debugging a download, two flags are worth remembering. The -d flag prints the OpenSSL decryption command, and the -t flag prints the links to the .ts files. The -t flag in particular tells you what the program thinks the segments are before it starts fetching them, which is the fastest way to find out whether a playlist is being parsed the way you expect.

## Windows support is real but needs a specific environment

The project documents three build environments rather than one, and the differences matter if you are on Windows.

The MSYS2 route uses the UCRT64 shell, and the documented package install is:

```sh
pacman -S --needed mingw-w64-ucrt-x86_64-gcc mingw-w64-ucrt-x86_64-cmake \
    mingw-w64-ucrt-x86_64-ninja mingw-w64-ucrt-x86_64-curl mingw-w64-ucrt-x86_64-openssl
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release
cmake --build build
```

The result is build/hlsdl.exe, and the documentation is specific that you run it from the same shell, which matters because the DLLs it needs come from that environment.

The MSVC route uses vcpkg instead. You set VCPKG_ROOT to your vcpkg installation and run in PowerShell:

```powershell
& "$env:VCPKG_ROOT/vcpkg.exe" install curl openssl pthreads --triplet x64-windows
cmake -S . -B build -A x64 `
    "-DCMAKE_TOOLCHAIN_FILE=$env:VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake"
cmake --build build --config Release
```

That produces build/Release/hlsdl.exe, and the pthreads package in the vcpkg install is there because the requirements list POSIX threads, which MSVC does not provide natively.

The Linux and macOS instructions remain the shortest path, and the Docker route sidesteps the toolchain question altogether for anyone who just wants the binary.

## The state of the project and what it will not do for you

The repository is not archived and the last push was on 2026-10-02, so the work is current, although there are no GitHub releases and no tagged versions, which means there is no version marker to pin a build to. The CI workflow builds the project on each push, and a man page ships in the repository as hlsdl.1, so the tool is intended to be a proper command in a shell rather than a script you invoke with a path.

Two gaps are worth being honest about. The first is the live stream story. There are flags for a start offset, a duration and a refresh delay, and the ToDo list mentions multithreading as an idea rather than a shipped feature, so a long live capture is a sequential download with retries rather than a parallel one. The second is the fragmented MP4 gap described earlier, which is a genuine limitation rather than a missing nicety.

A concrete alternative for the transport stream case is FFmpeg, whose HLS demuxer covers the same playlists and can remux and separate audio as part of the same pass, at the cost of a much larger dependency and a far more configurable command line. The difference in approach is that hlsdl is a small single purpose program that does one thing and stays out of the way, while FFmpeg is a general media pipeline where HLS is one input format among many.

The trade is straightforward. If you want a transport stream file from a VOD playlist, hlsdl gives you that in a few hundred lines of C with a documented flag list and a container that runs as a non-root user. If you need to join fragmented MP4 across discontinuities, pull separate audio renditions, or transcode on the way through, hlsdl is the wrong tool and its own ToDo list says so.

## Conclusion

hlsdl fits someone who has a legal video on demand playlist and wants it as a file, and who is comfortable compiling a C program with CMake, libcurl and OpenSSL. It does not fit a workflow that needs fragmented MP4 discontinuity remuxing or separate audio renditions, since the project's own ToDo list names both as unimplemented. Before you rely on it, check whether your playlist uses EXT-X-MAP, because separate audio and SAMPLE-AES are documented as unsupported for those playlists, and run it once on a short VOD with -v to see what it reports.

## FAQ

### How do I build hlsdl on Linux?

Install build-essential, cmake, libcurl4-openssl-dev and libssl-dev on Debian or Ubuntu, then run cmake -S . -B build -DCMAKE_BUILD_TYPE=Release and cmake --build build. Run build/hlsdl or install with cmake --install build.

### Does hlsdl support downloading separate audio and video tracks?

Not for playlists using EXT-X-MAP. The documentation states that separate audio and SAMPLE-AES are unsupported for those playlists, and remuxing fragmented MP4 discontinuities and separate audio renditions appear on the project's ToDo list.

### What encryption does hlsdl support?

It supports AES-128 decryption and SAMPLE-AES decryption for MPEG-2 Transport Streams. The -K flag forces an AES key value as a hex string, and the -k and -n flags allow parts of an AES key URI to be replaced.

### How do I run hlsdl in Docker?

Build the image with docker build -t hlsdl:latest ., then run docker run -v ./data:/var/hlsdl/data --rm -it hlsdl:latest hlsdl [options] url. The image runs as user 1000, so the mounted directory has to be writable by that user.

### How do I choose the best quality when downloading with hlsdl?

The -b option automatically chooses the best quality, and the -W and -H options choose the largest width or height lower than or equal to the value you give. The -A option selects the audio language.

## Sources

- [Issues](https://github.com/selsta/hlsdl/issues)
- [License: MIT](https://github.com/selsta/hlsdl/blob/master/LICENSE)
- [README](https://github.com/selsta/hlsdl/blob/master/README.md)
- [selsta/hlsdl on GitHub](https://github.com/selsta/hlsdl)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/selsta-hlsdl
