A C library that converts HLS streams (.m3u8) to MP4 files without FFmpeg or any GPL/LGPL code. Includes a Flutter FFI plugin for iOS, Android, and macOS.
- Parses both master and media M3U8 playlists
- Automatically selects highest-bandwidth variant from master playlists
- Supports AES-128-CBC encrypted segments (with key caching)
- Demuxes MPEG-TS to extract H.264 video and AAC audio
- Muxes into a standard MP4 container
- Progress and logging callbacks
- Concurrent segment prefetching for faster downloads
- Per-segment retry with configurable timeout
- No global state; fully reentrant
- C99, no compiler extensions
- MPEG-TS segments only — The library supports HLS streams with MPEG-TS (
.ts) segments. Streams using fMP4 (fragmented MP4 /.m4s) segments are not currently supported. You can identify fMP4 playlists by the#EXT-X-MAPtag in the media playlist. - VOD only — Live streams (playlists without
#EXT-X-ENDLIST) are not supported. The library downloads all segments listed in the playlist; live playlists with a sliding window will fail or produce incomplete output. - H.264 + AAC only — The demuxer extracts H.264 video and AAC audio. Other codecs (H.265/HEVC, AC-3, EAC-3) are not demuxed.
All dependencies are permissively licensed:
| Library | License | Purpose |
|---|---|---|
| libcurl | MIT | HTTP fetching |
| mbedTLS | Apache 2.0 | AES-128-CBC decryption |
| minimp4 | Public Domain | MP4 muxing (vendored) |
sudo apt update
sudo apt install libcurl4-openssl-dev libmbedtls-dev cmake build-essentialbrew install curl mbedtls@3 cmakeNote: mbedTLS 4.x removed the legacy AES API. Use
mbedtls@3(3.6.x) which providesmbedtls/aes.h. Pass-DCMAKE_PREFIX_PATH="/opt/homebrew/opt/curl;/opt/homebrew/opt/mbedtls@3"to CMake if the keg-only packages aren't found automatically.
Using the build script:
./build.sh build # Build all targets
./build.sh test # Build and run unit tests
./build.sh run # Build and run the video benchmark
./build.sh clean # Remove build directoryOr manually with CMake:
mkdir build && cd build
cmake .. -DCMAKE_PREFIX_PATH="/opt/homebrew/opt/curl;/opt/homebrew/opt/mbedtls@3"
cmake --build .
ctest --output-on-failure # run testsTo disable tests: cmake -DBUILD_TESTS=OFF ..
The library exposes two functions:
#include <hls2mp4.h>
int hls2mp4_convert(const hls2mp4_options *opts);
const char *hls2mp4_error_string(int code);typedef struct {
const char *input_url; // URL to .m3u8 playlist
const char *output_path; // Local path for output .mp4
hls2mp4_progress_cb on_progress; // Optional, may be NULL
hls2mp4_log_cb on_log; // Optional, may be NULL
void *userdata; // Passed to callbacks
int timeout_ms; // HTTP timeout, 0 = default (10s)
int max_retries; // Per-segment retries, 0 = default (3)
} hls2mp4_options;| Code | Constant | Meaning |
|---|---|---|
| 0 | HLS2MP4_OK |
Success |
| -1 | HLS2MP4_ERR_NETWORK |
HTTP request failed |
| -2 | HLS2MP4_ERR_PARSE |
Playlist parse error |
| -3 | HLS2MP4_ERR_DEMUX |
TS demux error |
| -4 | HLS2MP4_ERR_DECRYPT |
AES decryption failed |
| -5 | HLS2MP4_ERR_MUXER |
MP4 muxer error |
| -6 | HLS2MP4_ERR_IO |
File I/O error |
#include <hls2mp4.h>
#include <stdio.h>
static void on_progress(int done, int total, void *ud) {
(void)ud;
printf("\r[%d/%d] segments", done, total);
fflush(stdout);
}
int main(void) {
hls2mp4_options opts = {
.input_url = "https://example.com/stream/playlist.m3u8",
.output_path = "output.mp4",
.on_progress = on_progress,
.timeout_ms = 15000,
.max_retries = 3,
};
int ret = hls2mp4_convert(&opts);
if (ret != HLS2MP4_OK) {
fprintf(stderr, "Error: %s\n", hls2mp4_error_string(ret));
return 1;
}
printf("\nDone!\n");
return 0;
}With CMake (after installing or using add_subdirectory):
find_package(hls2mp4 REQUIRED)
target_link_libraries(myapp PRIVATE hls2mp4::hls2mp4)Or link directly:
gcc -o myapp myapp.c -lhls2mp4 -lcurl -lmbedtls -lmbedcryptoThe hls2mp4_flutter/ directory contains a Flutter FFI plugin that wraps the C library for mobile and desktop apps.
| Platform | Status |
|---|---|
| iOS | Supported |
| Android | Supported |
| macOS | Supported |
import 'package:hls2mp4_flutter/hls2mp4_flutter.dart';
final result = await Hls2Mp4Converter.convert(
inputUrl: 'https://example.com/stream/playlist.m3u8',
outputPath: '/path/to/output.mp4',
timeoutMs: 30000,
maxRetries: 3,
onProgress: (progress) {
print('${(progress.fraction * 100).toStringAsFixed(0)}%');
},
);
if (result.success) {
print('Conversion complete!');
} else {
print('Error: ${result.errorMessage}');
}The conversion runs on a background isolate and never blocks the UI thread. Progress callbacks are delivered in real time via SendPort.
cd hls2mp4_flutter
# Generate FFI bindings (only needed after changing hls2mp4.h)
dart run ffigen --config ffigen.yaml
# Build Android prebuilt dependencies (first time only)
cd android && ./build_android_deps.sh && cd ..
# Run the example app
cd example
flutter run -d macos # or -d ios, -d androidAndroid requires cross-compiled curl and mbedTLS static libraries. Run the build script once before your first Android build:
cd hls2mp4_flutter/android
./build_android_deps.shThis downloads curl 8.12.1 and mbedTLS 3.6.3, cross-compiles them for arm64-v8a, armeabi-v7a, and x86_64, and places the results in android/prebuilt/.
The example app at hls2mp4_flutter/example/ demonstrates converting 11 public HLS streams to MP4 with a progress UI and file opening via the open_file package.
The benchmark and example app use these public HLS streams:
| Stream | Source | Protocol |
|---|---|---|
| Tears of Steel | Unified Streaming | HTTPS |
| fMP4 BIPBOP | Apple | HTTPS |
| Tears of Steel (MP4) | Unified Streaming | HTTPS |
| Live Test 1 | Akamai | HTTPS |
| Live Test 2 | Akamai | HTTPS |
| Dolby Stereo | CloudFront | HTTP |
| Dolby Multichannel | CloudFront | HTTP |
| Dolby Multilanguage | CloudFront | HTTP |
| Azure Promo 1 | Azure Media Services | HTTP |
| Azure Promo 2 | Azure Media Services | HTTP |
| Azure 4K | Azure Media Services | HTTP |
include/hls2mp4.h Public API header
src/
hls2mp4.c Main conversion logic + segment prefetching
http.c HTTP client (libcurl wrapper)
m3u8.c M3U8 playlist parser
ts_demux.c MPEG-TS demultiplexer
aes.c AES-128-CBC decryption (mbedTLS wrapper)
muxer.c MP4 muxer (minimp4 wrapper)
third_party/
minimp4.h Vendored MP4 muxing library
tests/
run_videos.c Benchmark with 11 HLS streams
compare_ffmpeg.sh Compares output against ffmpeg reference
fix_loop.sh Iterative fix loop using Claude Code
test_m3u8.c M3U8 parser unit tests
test_ts_demux.c TS demuxer unit tests
test_aes.c AES decryption unit tests
hls2mp4_flutter/ Flutter FFI plugin (iOS, Android, macOS)
lib/ Dart API wrapper
src/ Symlinks to ../src, ../include, ../third_party
android/ Android build config + prebuilt dependency script
example/ Flutter example app
MIT