m314dl is a fast, reliable HLS (.m3u8) and DASH (.mpd) video stream
downloader in a single static Go binary — with native, in-process CENC /
Widevine-style decryption. No mp4decrypt, no shaka-packager, no external
tools on the download path.
If you have reached for N_m3u8DL-RE,
vsd, youtube-dl/yt-dlp or ffmpeg to grab
an m3u8 or mpd stream, m314dl is a single-binary alternative that is
faster on every benchmark below and decrypts DRM-protected fMP4 in the same pass
it downloads it — no second mp4decrypt step.
m314dl -o out.mp4 https://example.com/master.m3u8 # download an HLS stream
m314dl -key KID:KEY -o out.mp4 https://example.com/video.mpd # download + decrypt a DASH stream- Quick start
- Why I built it
- Benchmarks
- How decryption works
- Features
- Install
- Usage
- Track selection
- Restream (
-serve) - Worker mode
- RPC server
- FAQ
- Tests
- License
# download an HLS or DASH stream (best video + audio per language + all subs)
m314dl -o out.mp4 https://example.com/master.m3u8
m314dl -o out.mp4 https://example.com/manifest.mpd
# decrypt a CENC / cbcs DASH stream with a known key (native, no mp4decrypt)
m314dl -key KID:KEY -o out.mp4 https://example.com/manifest.mpd
# list the streams in a manifest
m314dl -list https://example.com/manifest.mpd
# record a live stream for an hour (Ctrl-C anytime — the file is still muxed)
m314dl -live-duration 1h -o show.mp4 https://example.com/live.m3u8
# re-serve what you download as a live HLS/TS/DASH endpoint
m314dl -serve :8314 https://example.com/master.m3u8Run m314dl -h for the full flag list.
For about a year, my go-to tool was N_m3u8DL-RE — it's excellent, and it's what got me deep enough into this space to write my own downloader. Big shout-out to it. 🙏
I started m314dl because no single tool I tried covered everything I needed at once: download and decrypt in one pass, run as a server, and re-broadcast a live stream — all from one static binary. If your needs are simpler, N_m3u8DL-RE is still a great pick; m314dl is what you reach for when you want it all in one place.
Wall-clock to download + decrypt + mux a 5-minute 1080p stream (~150 MB, 75 segments/track), served from localhost with a simulated 50 ms CDN round-trip, best-of-3, all outputs verified full-length (155 MB). Lower is better.
| Case | m314dl | vsd 0.5.0 | N_m3u8DL-RE 0.6.0 |
|---|---|---|---|
| HLS TS (VOD) | 0.95 s | 1.60 s | 1.12 s |
| HLS fMP4 (VOD) | 0.96 s | 1.62 s | 1.08 s |
| DASH clear (VOD) | 0.91 s | 1.85 s | 1.45 s |
| DASH CENC — AES-CTR | 0.98 s | 1.95 s | 2.69 s |
| DASH cbcs — AES-CBC pattern | 0.99 s | 1.88 s | 1.82 s |
Three things to notice:
- m314dl is flat. Encryption adds ~0.07 s, because segments are decrypted
in memory as they download — the decrypt overlaps the next fetch and never
touches disk. Competitors pay ~1 s or more for the same content because they
decrypt in a separate pass (N_m3u8DL-RE shells out to
mp4decryptafter the merge; see below), or download streams strictly one at a time (vsd). - Concurrency auto-tunes. With no
-tflag, m314dl ramps its in-flight request count to fit the network (AIMD, 4→64) and backs off from rate-limited servers (honoringRetry-After) — hiding round-trip time without a knob to guess.-t Nuses exactly N and holds it (matching--thread-count), backing off only on real rate-limit pressure, then climbing straight back. - The numbers are reproducible. The corpus generator, the throttling
server, and the exact commands live in
bench/. Re-run them on your own hardware — don't trust a copied table.
Decryption throughput on its own is ~1.7 GB/s (CTR) / ~2.0 GB/s (CBC)
single-threaded (go test -bench . ./internal/mp4/) — orders of magnitude above
typical network bandwidth, across every worker thread. Decryption is free; the
network is the only bottleneck. Microbenchmarks for the parse and
manifest-render paths live alongside it:
go test ./internal/dash/ ./internal/hls/ ./internal/restream/ -run=^$ -bench=. -benchmem
M314_FIXTURES="$PWD/bench/fixtures" go test ./internal/mp4/ ./internal/engine/ -run=^$ -bench=.Most tools treat DRM as a post-processing step: download the whole encrypted
file, then run mp4decrypt/shaka-packager over it. m314dl decrypts each
fragment in the download worker, in RAM, the moment it arrives — the same place
it handles AES-128. Consequences:
- no encrypted temp file, no second full pass over the data, no subprocess spawn per segment;
- decryption is concurrent across all threads and overlapped with I/O, so it costs effectively nothing (see the flat benchmark row);
- CENC (
cencAES-CTR), cbcs (AES-CBC + crypt/skip pattern, constant IV), pluscens/cbc1are implemented from the ISO/IEC 23001-7 boxes (tenc,senc,saiz/saio,trun) — verified byte-exact againstmp4decryptand by hermetic round-trip tests.
You supply the content key; m314dl does not include a Widevine/PlayReady CDM (neither does vsd's or N_m3u8DL-RE's default download path — key extraction is a separate concern):
# KID:KEY in hex (KID dashes optional); repeatable for multi-key manifests
m314dl -key 00112233445566778899aabbccddeeff:0123456789abcdef0123456789abcdef \
-o out.mp4 https://example.com/manifest.mpdScope of decryption. m314dl decrypts when you provide the key. It does not
run a license/CDM handshake — key extraction is a separate concern. TLS
verification is on by default and only skipped with an explicit -insecure.
- HLS (TS + fMP4): master/media playlists, rendition groups
(
AUDIO=/SUBTITLES=honored — best audio belongs to the picked variant), byte ranges,EXT-X-MAPchanges mid-stream, AES-128 (in-process,data:key URIs supported), audio-only variant detection. - DASH: SegmentTemplate (
$Number$/$Time$+%0Nd), SegmentTimeline (negative@r), SegmentList, SegmentBase (split into subsegments by theirsidxso a whole on-demand file never sits in RAM), multi-period (merged with discontinuity markers), namespace-agnostic lenient XML,cenc:default_KID. - Smooth Streaming (MSS):
.ism/Manifest(VOD and live),{bitrate}/{start time}URL templates,<c t d r>timelines, H.264 + AAC (init segments are synthesized fromCodecPrivateData— the presentation ships none), PIFF fragments normalized for players, PlayReady KID read from theProtectionHeaderand decrypted with-key. - Plain files (like aria2c): point at a direct file URL (
.mp4,.mkv,.zip, anything) and it downloads over many byte-range connections at once, with the same resume/retry; a single connection when the server won't do ranges.
- Native CENC / cbcs / cens / cbc1 — no
mp4decrypt, noshaka-packager; per-fragment, in memory, byte-exact vs the reference tools. HLS SAMPLE-AES (AAC + H.264) is decrypted too, with a supplied key.
- One pipeline for VOD and live (feeder → worker pool → ordered writer): no duplicated code paths; segments stream to disk (one part file per in-flight segment, decrypted and appended in order) so peak memory is one segment, not one per thread — even for huge segments.
- Adaptive concurrency: with no
-t, in-flight requests auto-tune to the network (AIMD, 4→64) and back off from 429/5xx (honoringRetry-After) — no thread count to guess, and no hammering rate-limited CDNs;-t Npins exactly N (held, still backs off on rate limits). - One TCP connection per in-flight request (HTTP/1.1): a CDN offering
HTTP/2 would make Go multiplex every concurrent segment onto a single TCP
connection, and one flow can't fill a real network's pipe (RTT- and
loss-bound) — so downloads run over many independent HTTP/1.1 connections, the
way
N_m3u8DL-RE/aria2/yt-dlpdo, for full aggregate throughput. - Resume that survives a kill, not just a clean exit: segments stream to part files with HTTP byte-range resume, and the checkpoint is written on every commit — so an interrupted (or hard-killed) VOD download continues byte-exact, losing at most the one segment being written. A failed segment is never silently skipped.
- Retries with exponential backoff + jitter that cover mid-body read failures, not just connection setup; status-aware (404 fails fast, 5xx/429 retry).
- Live recording for both protocols: starts at the live edge
(
-live-from-startfor the whole DVR window), refresh failures retried forever, segment dedupe across refreshes (bounded, so a 24/7 channel doesn't grow without limit),-live-durationlimit, and Ctrl-C finishes the recording gracefully and muxes — you never lose what you already recorded. - Ad-segment skipping by regex (
-ad-keyword, applied on live refreshes too).
- WebVTT (concatenated segments deduped), TTML→SRT (lenient regex parsing —
survives non-compliant XML), stpp-in-fMP4 extracted natively (no ffmpeg TTML
gap), muxed with correct ISO 639-2 language tags — or written as sidecar files
with
-sub-external.
- Flexible input: an HTTP(S) URL (first or last argument), a local
.m3u8/.mpdfile orfile://path (for manifests signed per request and never published), or a web page to scrape. - Page scraping: point it at a web page; it finds
.m3u8/.mpdURLs (inline JSON and one iframe level included). - Paste a curl command (
-curl): for a URL that needs specific headers, hand it a browser's "Copy as cURL" and it takes the URL + all headers for you, instead of typing each-H. Pass it as a file or on stdin — a browser's curl has quotes the shell mangles inline:m314dl -curl - -o out.mp4 <<'CURL' curl 'https://host/master.m3u8' -H 'referer: https://site/' -H 'user-agent: …' CURL
- Custom headers (sent verbatim), Netscape
cookies.txt, HTTP/SOCKS proxy with auth. - Automation-friendly: plain-line progress on non-TTY (no ANSI garbage in logs), real exit codes, quiet machine-readable output.
- Update check: once a day it checks GitHub for a newer release and prints a
one-line notice if you're behind (never blocks the download;
-no-update-checkorM314DL_NO_UPDATE_CHECK=1turns it off).
Docker (ffmpeg bundled, nothing else to install):
docker build -t m314dl .
docker run --rm -v "$PWD:/data" m314dl -o out.mp4 https://example.com/master.m3u8Downloads land in the current directory (that's the -v "$PWD:/data" mount).
From source:
go build -o m314dl .Requires ffmpeg on PATH (or next to the binary, or passed with -ffmpeg <path>) for muxing.
# best video + best audio per language + all subtitles → out.mp4
m314dl -o out.mp4 https://example.com/master.m3u8
# decrypt a CENC/cbcs DASH stream with a known key (native, no mp4decrypt)
m314dl -key KID:KEY -o out.mp4 https://example.com/manifest.mpd
# list streams
m314dl -list https://example.com/manifest.mpd
# pick streams: filters are regexes, colon-separated; for= picks from matches
m314dl -sv 'res=1080' -sa 'lang=^en' -ss 'lang=en|de' -o out.mkv URL
m314dl -sv best -sa 'for=best2' URL
# record a live stream for 1 hour (or Ctrl-C anytime — file still muxed)
m314dl -live-duration 1h -o show.mp4 https://example.com/live.m3u8
# scrape a page for the stream
m314dl -o video.mp4 https://example.com/watch/12345
# resume an interrupted VOD download: just rerun the same commandRun m314dl -h for all flags.
-sv, -sa, -ss accept:
| Value | Meaning |
|---|---|
best, worst, all, best3 |
positional over sorted streams |
key=regex[:key=regex...] |
filter; keeps all matches |
key!=regex |
negate — keep streams that do not match (RE2 has no negative lookahead, so this is how you express a deny-list) |
...:for=bestN |
take N best from the filtered set |
none |
drop this type |
Regex keys: id, lang, name, codecs, res, range, channel.
Numeric keys: bwmin/bwmax (kbps), hmin/hmax/wmin/wmax (pixels),
segsmin/segsmax (segment count), plistdurmin/plistdurmax (seconds, or
20m). Numeric filters treat an unknown value as passing, so
segsmin/plistdurmin drop short ad/bumper periods in DASH without touching
HLS tracks whose length isn't known yet. A height/width bound that excludes
every rendition falls back to the nearest one, so a quality preference never
takes a channel off the air. Sorting: video by height→bandwidth, audio by
default-flag→channels→bandwidth.
# drop 1-segment SSAI bumpers, keep ≥720p AVC content
m314dl -sv 'segsmin=2:plistdurmin=20m:res=x720$:codecs=avc1|avc3:for=best' URL
# drop two mislabeled subtitle tracks by id (deny-list via negation)
m314dl -ss 'id!=^(a|b)$:for=all' URLInstead of writing a file, m314dl can republish what it downloads as a live stream over HTTP — decrypt a DRM source and re-serve it in the clear, pull one feed and fan it out to many viewers, or turn a live recording into a watchable endpoint.
# pull a stream and re-serve it as live HLS
m314dl -serve :8314 https://example.com/master.m3u8
# → http://localhost:8314/live.m3u8
# decrypt a DRM DASH source and restream it in the clear
m314dl -serve :8314 -key KID:KEY https://example.com/manifest.mpd
# re-serve as one continuous MPEG-TS instead (VLC, set-top boxes)
m314dl -serve :8314 -serve-format ts https://example.com/live.m3u8
# → http://localhost:8314/live.ts
# re-serve as live DASH (dash.js, Shaka, ExoPlayer)
m314dl -serve :8314 -serve-format dash https://example.com/manifest.mpd
# → http://localhost:8314/live.mpdOpen http://<host>:8314/live.m3u8 (HLS), /live.ts (MPEG-TS), or /live.mpd
(DASH) in VLC, mpv, hls.js, dash.js, or any player.
Why it's different from an FFmpeg restreamer: segments arrive already decrypted and in playback order from the normal download engine, and the packager sits after the selection/download/decryption path — so the source is fetched once, no subprocess runs on the copy path, and output segments are byte-identical to the source. One copy is held in a shared window and served to every viewer with Range/ETag support, not buffered per connection.
GET /live.m3u8— multivariant (master) playlistGET /{track}/index.m3u8— a track's media playlist (video,audio-en, …)GET /{track}/init.mp4— fMP4 init segment (EXT-X-MAP target)GET /{track}/000123.ts— a media segment
A live source rolls a bounded in-memory window with the right EXT-X-VERSION
for fMP4 vs TS, EXT-X-MAP for fMP4, and measured BANDWIDTH/CODECS/
RESOLUTION in the master. A finite (VOD) source is served as a seekable
growing VOD instead — segments spool to disk (RAM stays flat) and the whole
timeline is scrubbable, gaining EXT-X-ENDLIST when it completes.
GET /live.ts — one never-ending transport stream fanned out to every viewer.
The fan-out is non-blocking: a viewer that falls a full buffer behind is
dropped, and no other viewer ever waits on it (unlike a naive broadcaster that
stalls everyone on the slowest client).
- TS source → pure Go, no FFmpeg. The segments are already MPEG-TS, so they concatenate into a valid continuous stream with no re-mux; each viewer joins on a segment boundary (a clean, decodable PAT/PMT + keyframe start). Continuity counters are renumbered into one seamless per-PID sequence across boundaries.
- fMP4 source, or separate video+audio → one FFmpeg remux. When the source
isn't already a muxed TS, m314dl runs a single long-lived FFmpeg that
copy-remuxes the decrypted tracks into TS (separate audio is muxed in). One
process for the whole stream — not one per segment — and no
-repacing (segment arrival paces it). Because m314dl decrypts upstream, FFmpeg only sees a clear copy, so the DRM-demuxer memory leak that plagues FFmpeg restreamers never applies. Pass-serve-transcode '<ffmpeg args>'to re-encode instead of copy (e.g.-c:v libx264 -preset veryfast -c:a aac).
GET /live.mpd — a SegmentTemplate + SegmentTimeline manifest plus the same
in-memory fMP4 segments (one AdaptationSet per media type, one Representation
per track, each timeline built from real segment durations). A live source
produces a dynamic MPD with minimumUpdatePeriod; a finite one is served the
same way while it downloads and becomes a static, fully-seekable MPD once it
completes. Needs an fMP4 source (a TS source must be remuxed first — a later
phase).
Scope today: HLS and DASH output are copy-only (same container family, no FFmpeg); MPEG-TS output copies a TS source in pure Go and remuxes an fMP4 or separate-audio source through one FFmpeg (with optional transcode). TS→fMP4 for DASH/HLS-fMP4 output, and subtitle restreaming, are not wired up yet.
-serve runs one presentation. -worker runs a multi-channel restream
agent a controller drives over HTTP — start and stop channels on the fly, each
served at /{id}/…. One process holds them all: the media is already in RAM in
the packager, so there is no subprocess to babysit and no reverse proxy to a
child's port.
# on the streaming server (secret required on non-loopback binds)
m314dl -worker :7001 -worker-secret mytoken -worker-max-channels 32
# start a channel (source → live HLS at /news/live.m3u8)
curl -H 'Authorization: Bearer mytoken' -X POST http://server:7001/api/channels \
-d '{"id":"news","url":"https://…/manifest.mpd","format":"hls","keys":["KID:KEY"],
"headers":{"Referer":"https://…"}}'
# ts / dash channels the same way (format: "ts" | "dash")
# list / inspect / stop
curl -H 'Authorization: Bearer mytoken' http://server:7001/api/channels
curl -H 'Authorization: Bearer mytoken' http://server:7001/api/channels/news
curl -H 'Authorization: Bearer mytoken' -X DELETE http://server:7001/api/channels/news
curl -H 'Authorization: Bearer mytoken' http://server:7001/api/health
# viewers just play the media URL (unauthenticated, like any origin)
mpv http://server:7001/news/live.m3u8Each channel reuses the whole engine — selection, native decrypt, live refresh,
and the HLS/TS/DASH packagers above — so a channel is exactly a -serve stream
under an id. The control plane carries the production hardening: a bearer secret,
an admission cap (-worker-max-channels, 503 past it), and graceful shutdown
that stops every channel. State is in-memory by design: the worker is stateless,
so a controller's own reconciliation restarts channels after a restart.
Run m314dl on a server and submit normal download jobs over HTTP/JSON. Each job is a child m314dl process, so every flag, resume, and graceful-stop behavior works exactly as it does locally.
# on the server (a secret is required on non-loopback binds)
m314dl -rpc 0.0.0.0:8314 -rpc-secret mytoken -rpc-max-jobs 64
# submit a job (args = normal CLI flags)
curl -H 'Authorization: Bearer mytoken' -X POST http://server:8314/add \
-d '{"url":"https://example.com/master.m3u8","args":["-o","movie.mp4","-key","KID:KEY"]}'
# → {"id":1} (or HTTP 503 if -rpc-max-jobs are already running)
# watch: state, latest progress line, error if any
curl -H 'Authorization: Bearer mytoken' http://server:8314/jobs
curl -H 'Authorization: Bearer mytoken' http://server:8314/jobs/1 # + full log
curl -H 'Authorization: Bearer mytoken' http://server:8314/health # {"running":3,"total":57,"max":64}
# stop gracefully (live: mux what's recorded; VOD: save resume state);
# call twice to abort — same semantics as Ctrl-C
curl -H 'Authorization: Bearer mytoken' -X POST http://server:8314/jobs/1/stopOutput files land in the server's working directory unless -o gives a path.
An authenticated client has the full power of the CLI on the server — treat the
secret like an SSH key.
Built to run unattended in production:
- Bounded memory — finished jobs are reaped after
-rpc-retain(default 1h, with a hard ceiling), and each job's captured log is capped. A server left up for weeks doesn't grow without limit. - Admission cap —
-rpc-max-jobs(default 64) bounds concurrent jobs; past it,/addreturns503instead of fork-bombing the host.0= unlimited. - No lock contention — each job's output is buffered under its own lock, never a shared one, so hundreds of chatty jobs don't serialize on the server.
- Monotonic job ids — a reaped id is never reused, so a stale client can't address a recycled job.
- Graceful shutdown — SIGINT/SIGTERM stops accepting, signals every running
job to finish, then exits. On Linux, jobs also die with the server even on a
hard crash (
Pdeathsig), so a crash never leaves orphaned downloads. - Hardened endpoints — bounded request bodies, a read-header timeout, and constant-time secret comparison.
m314dl -o out.mp4 https://host/master.m3u8. m314dl fetches the master
playlist, picks the best video + audio + all subtitles by default, downloads
the segments in parallel and muxes to MP4 with ffmpeg.
Same command, with the MPD URL: m314dl -o out.mp4 https://host/manifest.mpd.
SegmentTemplate, SegmentTimeline, SegmentList, SegmentBase and multi-period
manifests are all supported.
Pass the content key: m314dl -key KID:KEY -o out.mp4 https://host/manifest.mpd.
Decryption (cenc AES-CTR and cbcs AES-CBC pattern) runs in-process, per
fragment, as the stream downloads — there is no mp4decrypt or shaka-packager
step. m314dl does not perform the license/CDM handshake, so you supply the key.
m314dl -live-duration 1h -o show.mp4 https://host/live.m3u8. Recording starts
at the live edge; press Ctrl-C anytime and the file is still finalized and muxed.
On the reproducible benchmark above, yes — across HLS, DASH and encrypted
content. See bench/ to run it yourself.
go test ./... # unit + integration
go test -bench . ./internal/mp4/ # decryption throughputThe CENC test suite proves decryption two independent ways: byte-exact
parity with mp4decrypt on real shaka-packaged fixtures (skips if the fixtures
or mp4decrypt are absent), and hermetic round-trips that build fragments
in-code and recover the plaintext with no external dependencies.
MIT — see LICENSE.
