⚠️ BETA RELEASE — v0.9.0This project is currently in Beta. The public API, command-line flags, and output folder layout may still change between 0.x releases. Please report bugs and feedback at:
A general-purpose HLS media converter built on top of FFmpeg, by @devdasher.
Turns any MKV / MP4 / MOV / TS / WEBM / AVI file into an HLS stream folder
with a ready-to-serve master.m3u8 playlist — including adaptive bitrate
video renditions, per-track audio profiles, and WebVTT subtitles — all from
a single command.
Comes with a Python library API so you can embed the converter in your own scripts and applications.
🔗 Related project — Desktop GUI
Prefer a graphical interface? A companion desktop app is available:
HLS Converter GUI — a PySide6 frontend with the same conversion engine, plus a built-in HLS preview player, drag & drop, per-track audio overrides, and one-click track merging.
- Download prebuilt executables for Windows, Linux, macOS from the Releases page: https://github.com/devdasher/hls-converter-gui/releases
- Or install from PyPI:
pip install hls-converter-gui
The GUI automatically installs the CLI package as a dependency, so you get both in one command.
- hls-converter-cli
pip install hls-converter-cli
This installs the hls-converter command on your PATH.
git clone https://github.com/devdasher/hls-converter-cli.git
cd hls-converter-cli
pip install -e .
hls-converter --version
hls-converter --about
- Python 3.10 or newer
- FFmpeg and ffprobe must be installed and available either on your
PATH, or passed explicitly with--ffmpegand--ffprobe.
How to install FFmpeg:
| OS | Command / source |
|---|---|
| Windows | Download from https://www.gyan.dev/ffmpeg/builds/ (essentials build is fine) and add the bin folder to your PATH |
| macOS | brew install ffmpeg |
| Linux | sudo apt install ffmpeg (Debian/Ubuntu) or sudo dnf install ffmpeg (Fedora) |
To confirm FFmpeg is visible:
ffmpeg -version
ffprobe -version
Convert a single file using the default settings (video is stream-copied, every audio track is stream-copied, all text subtitles are extracted):
hls-converter "movie.mkv"
Output goes into a movie-stream/ folder next to the input file, containing
a master.m3u8 you can serve directly from any HTTP server.
Choose a custom output directory:
hls-converter "movie.mkv" --output "D:\streams\movie"
Re-encode the video to multiple resolutions with a compatible audio set:
hls-converter "movie.mkv" --video-mode encode --video-profiles 720,1080
Add a subtitle file to an existing stream:
hls-converter "persian.srt" --add-to-stream "D:\streams\movie" --language fa
Preview everything without actually running FFmpeg:
hls-converter "movie.mkv" --dry-run
Run hls-converter --help for the full flag list. The most important ones
are summarised below.
| Flag | Description |
|---|---|
--output PATH |
Output stream folder. Default: <input_stem>-stream next to the input file. |
--clean |
Delete the entire output folder before starting. Useful when re-running with different settings. |
--skip-master |
Do not regenerate master.m3u8. Useful when merging multiple conversions into one folder. |
--dry-run |
Print every FFmpeg command without executing it. |
--ffmpeg PATH |
Path to the ffmpeg binary (default: search PATH). |
--ffprobe PATH |
Path to the ffprobe binary (default: search PATH). |
--default-audio-track N |
Force audio track N to be marked DEFAULT=YES in the master playlist. |
--default-subtitle-track N |
Force subtitle track N to be marked DEFAULT=YES. |
--version |
Show version and exit. |
--about |
Show author, license, and project links. |
| Flag | Description |
|---|---|
--add-to-stream PATH |
Add the input file (audio or subtitle) to this existing stream folder. |
--track N |
Assign the new track to number N. Default: next available. |
--language LANG |
ISO 639-1 language tag for the new track (e.g. fa, en, de). |
--title TEXT |
Human-readable title for the new track. |
--audio-add-mode MODE |
auto (default), copy, or encode. |
--audio-level LEVEL |
data-saver, small, balanced, high, source. |
--audio-codec CODEC |
Target codec when re-encoding: aac, libopus, libmp3lame, ac3, eac3. |
--audio-profile PROFILE |
AAC encoder profile (e.g. aac_low, aac_he). |
--subtitle-offset-ms N or TRACK=N,... |
Shift subtitle timings in milliseconds. |
--subtitle-time-scale NUM/DEN |
Scale subtitle timestamps (e.g. 25/23.976). |
| Flag | Description |
|---|---|
--video-mode MODE |
copy (default), encode, or none. |
--video-label LABEL |
Folder name for copy mode. Default: source height (e.g. 1080p). |
--video-profiles LIST |
Comma-separated target heights for encode mode, e.g. 480,720,1080. |
--video-level LEVEL |
Auto-bitrate preset: data-saver, small, balanced (default), high, source. |
--video-rate-mode MODE |
auto (default), crf, abr, or 2pass. |
--video-bitrate LIST |
Per-quality bitrate overrides in kbps, e.g. 480=850,720=1300,1080=2800. |
--video-crf LIST |
Per-quality CRF overrides, e.g. 480=24,720=20,1080=18. |
--video-codec CODEC |
FFmpeg encoder. Default: libx264. Try libx265 or h264_nvenc. |
--video-preset PRESET |
x264/x265 preset. Default: medium. Try fast or slow. |
Supported heights: 144, 240, 360, 480, 540, 720, 1080, 1440, 2160.
Bitrate level multipliers (applied to the built-in default bitrates):
| Level | Multiplier |
|---|---|
data-saver |
0.55× |
small |
0.75× |
balanced |
1.00× |
high |
1.50× |
source |
Uses ~90% of the source bitrate, capped per resolution |
Rate modes:
auto— Uses CRF if a--video-crfoverride is present, otherwise ABR.crf— Constant Rate Factor. Quality-driven, ignores bitrate.abr— Average bitrate. Uses--video-bitrateoverrides when present.2pass— Two-pass ABR. Best quality per byte but runs FFmpeg twice.
By default, every audio track is stream-copied into HLS, preserving the original codec whenever it is HLS-compatible (AAC, MP3, AC-3, E-AC-3).
| Flag | Description |
|---|---|
--audio MODE |
encode, none, or TRACK:encode. Repeatable. |
--audio-profiles LIST |
Encoded profiles. Format: BITRATE or BITRATE:CHANNELS, e.g. 128:2,224:6. |
--audio-bitrate LIST |
Per-track bitrate override, e.g. 2=128,4=224. |
--audio-channels LIST |
Per-track channel count override, e.g. 2=2,4=6. |
--audio-codec CODEC |
Target codec: aac (default), libopus, libmp3lame, ac3, eac3. |
--audio-profile PROFILE |
AAC encoder profile. Default: aac_low. |
--audio-level LEVEL |
data-saver, small, balanced (default), high, source. |
Examples:
Copy all audio (default):
hls-converter "movie.mkv"
Encode every audio track to the default profile set:
hls-converter "movie.mkv" --audio encode
Encode only tracks 1 and 3, with custom profiles:
hls-converter "movie.mkv" --audio 1:encode,3:encode --audio-profiles 128:2,224:6
Per-track bitrate and channel overrides:
hls-converter "movie.mkv" --audio encode --audio-bitrate 1=128,2=224 --audio-channels 2=6
Skip all audio:
hls-converter "movie.mkv" --audio none
Text-based subtitles are extracted to WebVTT with an injected
X-TIMESTAMP-MAP header so they stay in sync with HLS video segments.
| Flag | Description |
|---|---|
--subtitles MODE |
auto (default) or none. |
--subtitle-offset-ms N |
Global shift in milliseconds (positive = delay). |
--subtitle-offset-ms TRACK=N,... |
Per-track offset, e.g. 1=500,2=-200. |
--subtitle-time-scale NUM/DEN |
Scale timestamps for drift correction, e.g. 25/23.976. |
Supported input codecs: subrip, srt, ass, ssa, webvtt,
mov_text. Bitmap subtitles (PGS, VobSub) are skipped with a warning.
When to use --subtitle-time-scale: when the subtitles were timed for a
different frame rate than the video. The most common case is subtitles timed
for 25 fps PAL but the video is 23.976 fps — use 25/23.976. The inverse
case (23.976 → 25) uses 23.976/25.
You can attach an external audio file or subtitle file to an already-built
stream folder and regenerate master.m3u8:
hls-converter "commentary.m4a" --add-to-stream "D:\streams\movie" --language en --title "Director's Commentary"
hls-converter "persian.srt" --add-to-stream "D:\streams\movie" --language fa
The new track receives the next available number by default. Assign a
specific number with --track N.
Conflict protection: if a track already exists at that number and was
generated from a different source file, the tool refuses to overwrite it.
Use a different --track value or --clean on the original stream.
Audio add modes:
| Mode | Behavior |
|---|---|
auto (default) |
Copy if the source codec is HLS-compatible, otherwise encode. |
copy |
Always copy. Fails if the codec is incompatible. |
encode |
Always re-encode to --audio-codec. |
Supported external audio inputs: .m4a, .aac, .mp3, .ac3,
.eac3, .opus, .flac, .wav, .ogg, .mka.
Supported external subtitle inputs: .srt, .ass, .ssa, .vtt,
.sub, .sbv.
| Flag | Description |
|---|---|
--segment-time SEC |
HLS segment length. Default: 6 seconds. |
--keyframe-interval SEC |
Force a keyframe every N seconds. Default: 6. |
--threads N |
Encoder thread count. Default: 12. |
Recommended combinations:
- Fast start / low-latency:
--segment-time 4 --keyframe-interval 4 - General purpose:
--segment-time 6 --keyframe-interval 6(default) - Slow network / better cache:
--segment-time 10 --keyframe-interval 10
The keyframe interval should be equal to or less than the segment time, otherwise segments will not start on clean keyframes and players may stutter.
The CLI is also a reusable Python library. Every public class and function is exported from the top-level module.
from pathlib import Path
from hls_converter_cli import (
HLSConverter, AppConfig, VideoRequest, VideoProfile,
AudioConfig, SubtitleConfig,
CancellationToken, ProgressEvent,
)
# Build an AppConfig however you like — you can reuse build_config(args)
# from CLI parsing, or construct one manually.
config = AppConfig(
ffmpeg="ffmpeg",
ffprobe="ffprobe",
output=None,
clean=False,
skip_master=False,
video=VideoRequest(
mode="encode",
profiles=[
VideoProfile(quality="720p", height=720),
VideoProfile(quality="1080p", height=1080),
],
),
video_level="balanced",
video_rate_mode="auto",
video_codec="libx264",
video_preset="medium",
threads=8,
audio=AudioConfig(),
audio_codec="aac",
audio_profile="aac_low",
subtitles=SubtitleConfig(mode="auto"),
keyframe_interval=6.0,
segment_time=6,
default_audio_track=None,
default_subtitle_track=None,
)
converter = HLSConverter(config)
token = CancellationToken()
result = converter.convert(
input_file=Path("movie.mkv"),
output_dir=Path("movie-stream"),
on_log=lambda level, msg: print(f"[{level}] {msg}"),
on_progress=lambda ev: print(f"{ev.stage} {ev.label} {ev.percent:.1f}%"),
cancel_token=token,
)
if result.success:
print("Master playlist:", result.master_playlist)
else:
print("Failed:", result.error)Cancelling from another thread:
token.cancel() # terminates any running FFmpeg processAdding an external track programmatically:
from hls_converter_cli import AddToStreamOptions
result = converter.add_to_stream(
input_file=Path("persian.srt"),
stream_dir=Path("movie-stream"),
options=AddToStreamOptions(language="fa", track=3),
)- Classes:
HLSConverter,CancellationToken,ProgressEvent,ConversionResult - Config models:
AppConfig,VideoRequest,VideoProfile,AudioConfig,AudioRequest,AudioProfile,SubtitleConfig,AddToStreamOptions - Media info:
MediaInfo,VideoStream,AudioStream,SubtitleStream - Parsing helpers:
build_parser,build_config - Errors:
ConverterError - Metadata:
__version__,__status__,__author__,__license__,APP_NAME,AUTHOR,GITHUB_URL,PYPI_URL
For an input file movie.mkv you will get:
movie-stream/
├── master.m3u8 ← entry point for players
├── .converter.json ← source fingerprint and stream info
├── video/
│ ├── 480p/
│ │ ├── playlist.m3u8
│ │ ├── 00000.ts
│ │ ├── 00001.ts
│ │ └── ...
│ ├── 720p/
│ │ └── ...
│ └── 1080p/
│ └── ...
├── audio/
│ ├── 1/
│ │ ├── 128k-2ch/
│ │ │ ├── playlist.m3u8
│ │ │ └── *.ts
│ │ └── 224k-6ch/
│ │ └── ...
│ └── 2/
│ └── ...
└── subtitles/
├── 1/
│ ├── playlist.m3u8
│ └── sub.vtt
└── 2/
└── ...
Serve the whole folder from any HTTP server and point your player at
master.m3u8:
python -m http.server 8000
# then open http://localhost:8000/movie-stream/master.m3u8
The master playlist already contains EXT-X-MEDIA entries for all audio and
subtitle tracks, EXT-X-STREAM-INF entries with BANDWIDTH, RESOLUTION,
CODECS, AUDIO, and SUBTITLES attributes, and a proper HLS version tag.
- Probing —
ffprobereads every stream in the input file: codec, resolution, frame rate, channels, language, and per-stream average bitrate (measured by summing packet sizes). - Video — In copy mode, the selected video stream is remuxed into HLS without re-encoding. In encode mode, it is scaled to each requested height using Lanczos filtering, forced to even dimensions, and encoded with the chosen codec and rate control.
- Audio — In copy mode, each audio track is remuxed. In encode mode, each track is converted to every requested profile (bitrate × channels), with automatic channel downmixing when the source has more channels than the target profile can carry.
- Subtitles — Every text subtitle stream is extracted to WebVTT,
optional per-track time-shifting and time-scaling are applied, then an
X-TIMESTAMP-MAPheader is injected so the subtitle timeline matches the HLS video timeline. - Master playlist —
master.m3u8is generated with properEXT-X-MEDIAgroups, per-renditionBANDWIDTH/RESOLUTION/CODECSattributes, andDEFAULTmarking for the tracks you configured. - Resume — Any sub-playlist that already exists and is complete is skipped automatically. Interrupted sub-conversions are cleaned up on the next run.
FFmpeg is not found
[ERROR] Executable not found: ffmpeg
Install FFmpeg and add it to your PATH, or pass --ffmpeg and --ffprobe
explicitly:
hls-converter "movie.mkv" --ffmpeg "C:\ffmpeg\bin\ffmpeg.exe" --ffprobe "C:\ffmpeg\bin\ffprobe.exe"
Subtitle is out of sync
Try a global offset first:
hls-converter "movie.mkv" --subtitle-offset-ms 500
If the drift grows over time, use a time scale instead:
hls-converter "movie.mkv" --subtitle-time-scale 25/23.976
Master playlist has no audio
The master playlist generator needs at least one video or audio rendition
to be complete. Check the video/ and audio/ subfolders for
playlist.m3u8 files.
Disk full during encode
Encode mode multiplies the output size because each profile is a full copy of the video. For a 4 GB source with three profiles expect roughly 10–12 GB of output.
Progress output is silent when running non-interactively
The CLI falls back to print() when no progress callback is registered.
For scripted use, register a callback through the library API instead.
Re-running after changing options
Use --clean to wipe the output folder and start fresh:
hls-converter "movie.mkv" --clean --video-mode encode --video-profiles 720
Multi-audio files — pass --audio 1:encode,2:encode --audio-bitrate 1=128,2=224
Multi-video files — only the first video stream is used. Pass
--video-mode none to skip video entirely and build an audio-only stream.
This is a Beta release. The following may still change before 1.0:
- The public Python API (classes, method signatures, exceptions).
- Some
--helptext and default values. - The on-disk metadata format (
.converter.json).
Compatibility promise:
- New flags may be added in minor releases.
- Existing flags will not be removed without a deprecation cycle.
- Saved stream folders remain forward-compatible.
Known limitations in this Beta:
- Only the first video stream in the input file is used.
- Only text-based subtitles are supported. Bitmap subtitles (PGS, VobSub) are skipped with a warning.
- No hardware encoder auto-detection. Pass
--video-codec h264_nvenc,hevc_nvenc, orh264_videotoolboxexplicitly if you have the corresponding FFmpeg build. - Very long files (>6 hours) may exceed ffprobe packet-iteration limits during bitrate analysis.
Feedback and bug reports are welcome at https://github.com/devdasher/hls-converter-cli/issues.
MIT — see the LICENSE file for details.
Copyright © 2026 devdasher