Hey folks ๐
If you've ever wanted to save a video lecture, a livestream replay, or a podcast episode for offline listening, you've probably run into the usual options: sketchy "online video parser" websites that ask you to paste your link into their server, or desktop apps that want you to sign up and upload stuff. Neither feels great when the whole point is your content.
I went looking for something better and ended up working with FlowPick โ an open-source, privacy-first media downloader that runs entirely in your browser. No uploads, no accounts, no telemetry. Everything (sniffing, downloading, merging, transcoding) happens client-side with FFmpeg compiled to WebAssembly.
In this tutorial we'll:
- Clone and run FlowPick locally
- Download our first HLS (
.m3u8) and DASH (.mpd) stream - Build and deploy it
- Poke at the internals so we can customize it
If you just want to try it without installing anything, there's a hosted version at https://flowpick.net (more below). The full source is on GitHub: https://github.com/ezwebtools/flowpick.
๐ Repo: https://github.com/ezwebtools/flowpick ยท Live tools: https://flowpick.net
A 30-second primer: what are HLS and DASH?
Before we touch code, two words you'll see everywhere in this space:
-
HLS (HTTP Live Streaming) uses a
.m3u8manifest that lists small.ts(or fMP4) segments. Common for live streams and a lot of video platforms. -
DASH (Dynamic Adaptive Streaming over HTTP) uses a
.mpdmanifest; video and audio usually travel as separate.m4stracks. YouTube and Bilibili lean on this.
The key idea: the "video" isn't one file. It's a playlist pointing at dozens (sometimes hundreds) of tiny segments. A downloader's job is to fetch all the segments, decrypt them if needed, and stitch them back into one playable file. That's exactly what FlowPick does โ in the browser.
What FlowPick is, in one paragraph
FlowPick is a Nuxt 4 app that ships in two shapes:
- A browser extension that sniffs media from the current tab's network requests.
- An online tool website with dedicated M3U8 and DASH downloaders.
Both share the same core download engine (a couple of Vue composables). It handles HLS, DASH, plain video/audio files, and images. Encrypted HLS (AES-128) is decrypted in-browser via the Web Crypto API โ the key never leaves your machine.
Prerequisites
- Node.js 20 LTS or newer
- pnpm (the repo scripts assume it; npm/yarn work if you align the lockfile)
- Git
node -v # should be v20.x or higher
pnpm -v
Step 1 โ Clone and install
git clone https://github.com/ezwebtools/flowpick.git
cd flowpick
pnpm install
Heads up: the postinstall script runs nuxt prepare automatically, which generates type declarations and the route manifest. The first install takes a bit longer โ that's expected. If it ever gets interrupted, just run pnpm exec nuxt prepare to catch up.
Step 2 โ Run it locally
pnpm dev
Open http://localhost:3000. The pages you care about:
-
/m3u8-downloaderโ for HLS streams -
/dash-downloaderโ for DASH streams -
/docsโ full documentation -
/blogโ tutorials and deep dives
Prefer not to install anything? The same tools are live at https://flowpick.net/m3u8-downloader and https://flowpick.net/dash-downloader. That's the fastest way to test the workflow before you self-host.
๐ Try it now: https://flowpick.net ยท M3U8 tool: https://flowpick.net/m3u8-downloader ยท DASH tool: https://flowpick.net/dash-downloader
Step 3 โ Download your first stream
HLS example
- Find a page that serves an
.m3u8stream. Open DevTools (F12) โ Network โ filterm3u8. - Play the video for a few seconds so the manifest actually gets requested.
- Right-click the
.m3u8request โ Copy link address. - Paste it into the M3U8 Downloader (localhost or the hosted one).
- Hit Parse. FlowPick reads the manifest and lists available qualities.
- Pick a quality, hit Start Download.
Under the hood FlowPick downloads every segment, decrypts if needed (AES-128), and merges them. For MP4 output it runs a remux โ
ffmpeg -i input.ts -c copy output.mp4
โ entirely in the browser. No re-encoding, no quality loss, and your file never touches a server.
DASH example
Paste the .mpd URL into the DASH Downloader, choose your video and audio tracks, and FlowPick auto-merges A/V for you. Because DASH keeps video and audio in separate streams, this merge step is what turns it into a single watchable file.
๐ก The hosted tools at https://flowpick.net are perfect when you're on a machine where you can't install the extension.
Step 4 โ Build and deploy
Production build:
pnpm build
pnpm preview
The default Nitro preset is cloudflare-pages-static, so pnpm build emits static assets in .output/. You can drop that on any static host โ Cloudflare Pages, Netlify, Vercel, or your own cloud. Want a Node server instead? Switch nitro.preset to node-server, rebuild, and run node .output/server/index.mjs.
One thing you must keep: the downloader routes set two response headers โ
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
These enable cross-origin isolation, which is what lets FFmpeg WASM use multiple threads (via SharedArrayBuffer) and lets large files stream to disk via StreamSaver. Drop them and big downloads silently degrade.
Step 5 โ Customize it
Low-effort tweaks that go a long way:
-
Edit docs / copy: everything in
content/is Markdown or YAML, validated by a Zod schema. Change text or add a page; it's live on reload. -
Tune the engine: open
app/composables/useStreamMerge.ts. Default concurrency is2(range1โ8); retries use exponential backoff (max3); writes fall backFSA โ StreamSaver โ Blobautomatically. -
Config:
nuxt.config.tsholds i18n (en / zh-Hans / zh-Hant / ja / ko), runtime config for analytics (NUXT_PUBLIC_GA_ID,NUXT_PUBLIC_CLARITY_ID), and the route headers above.
A concrete example โ bump the default concurrency for fast broadband by tweaking the settings defaults in the extension/online tool UI, or change the write buffer by editing useStreamMerge.ts. The composable's input shape is clean:
export interface StreamMergeOptions {
segments: ArrayBuffer[] | AsyncGenerator<ArrayBuffer>
totalSegments: number
filename: string
outputFormat?: 'mp4' | 'ts'
onProgress?: (progress: StreamMergeProgress) => void
signal?: AbortSignal
}
How it actually works (the fun part)
The engine is two composables:
-
useStreamMerge.tsโ downloads segments with a Worker Pool (shared counter, so no worker sits idle), decrypts AES-128 with Web Crypto, concatenates (TS) or remuxes (MP4) via FFmpeg, then writes using the best available strategy. -
useFFmpeg.tsโ loads@ffmpeg/ffmpeg(WASM), detects multithreading support, and runs merge/remux commands.
The write strategy is the clever bit:
| Strategy | When | Notes |
|---|---|---|
| FSA | Chrome/Edge 86+ | Streaming write, any size, constant memory |
| StreamSaver | Wide compatibility | Service Worker proxy, supports large files |
| Blob | Fallback | Memory-limited; files over ~1.5 GB rejected |
Caveats โ read before you ship
- DRM-protected content (Widevine, etc.) can't be decoded. By design.
- The web tool is subject to CORS. If the stream server doesn't send cross-origin headers, use the extension instead.
- FFmpeg WASM is ~8 MB on first load (cached afterward).
- Big-file experience is best on Chrome/Edge; Safari falls back to Blob mode.
Also: only download content you're allowed to. Respect each platform's terms and your local laws โ keep downloads for personal/offline use, don't redistribute.
Wrap-up
That's the whole loop: clone โ run โ download your first stream โ build โ deploy โ customize. FlowPick is open source on https://github.com/ezwebtools/flowpick, and you can try the hosted tools right now at https://flowpick.net (M3U8: https://flowpick.net/m3u8-downloader, DASH: https://flowpick.net/dash-downloader).
If this was useful, drop a โค๏ธ or a comment, and tell me what you'd build on top of it. Happy hacking!