<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: FlowPick Team</title>
    <description>The latest articles on DEV Community by FlowPick Team (@flowpick).</description>
    <link>https://dev.to/flowpick</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3615810%2F7f28f90c-e5c9-4c10-b585-08348152cc9c.png</url>
      <title>DEV Community: FlowPick Team</title>
      <link>https://dev.to/flowpick</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/flowpick"/>
    <language>en</language>
    <item>
      <title>🛠️ How to Run a Privacy-First, Browser-Based Stream Downloader (FlowPick) — A Hands-On Tutorial</title>
      <dc:creator>FlowPick Team</dc:creator>
      <pubDate>Sun, 26 Jul 2026 12:34:56 +0000</pubDate>
      <link>https://dev.to/flowpick/how-to-run-a-privacy-first-browser-based-stream-downloader-flowpick-a-hands-on-tutorial-ljd</link>
      <guid>https://dev.to/flowpick/how-to-run-a-privacy-first-browser-based-stream-downloader-flowpick-a-hands-on-tutorial-ljd</guid>
      <description>&lt;p&gt;Hey folks 👋&lt;/p&gt;

&lt;p&gt;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 &lt;em&gt;their&lt;/em&gt; server, or desktop apps that want you to sign up and upload stuff. Neither feels great when the whole point is &lt;em&gt;your&lt;/em&gt; content.&lt;/p&gt;

&lt;p&gt;I went looking for something better and ended up working with &lt;strong&gt;FlowPick&lt;/strong&gt; — 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.&lt;/p&gt;

&lt;p&gt;In this tutorial we'll:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Clone and run FlowPick locally&lt;/li&gt;
&lt;li&gt;Download our first HLS (&lt;code&gt;.m3u8&lt;/code&gt;) and DASH (&lt;code&gt;.mpd&lt;/code&gt;) stream&lt;/li&gt;
&lt;li&gt;Build and deploy it&lt;/li&gt;
&lt;li&gt;Poke at the internals so we can customize it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you just want to try it without installing anything, there's a hosted version at &lt;strong&gt;&lt;a href="https://flowpick.net" rel="noopener noreferrer"&gt;https://flowpick.net&lt;/a&gt;&lt;/strong&gt; (more below). The full source is on GitHub: &lt;strong&gt;&lt;a href="https://github.com/ezwebtools/flowpick" rel="noopener noreferrer"&gt;https://github.com/ezwebtools/flowpick&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🔗 Repo: &lt;a href="https://github.com/ezwebtools/flowpick" rel="noopener noreferrer"&gt;https://github.com/ezwebtools/flowpick&lt;/a&gt; · Live tools: &lt;a href="https://flowpick.net" rel="noopener noreferrer"&gt;https://flowpick.net&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  A 30-second primer: what are HLS and DASH?
&lt;/h2&gt;

&lt;p&gt;Before we touch code, two words you'll see everywhere in this space:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;HLS (HTTP Live Streaming)&lt;/strong&gt; uses a &lt;code&gt;.m3u8&lt;/code&gt; &lt;em&gt;manifest&lt;/em&gt; that lists small &lt;code&gt;.ts&lt;/code&gt; (or fMP4) segments. Common for live streams and a lot of video platforms.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DASH (Dynamic Adaptive Streaming over HTTP)&lt;/strong&gt; uses a &lt;code&gt;.mpd&lt;/code&gt; manifest; video and audio usually travel as separate &lt;code&gt;.m4s&lt;/code&gt; tracks. YouTube and Bilibili lean on this.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;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.&lt;/p&gt;




&lt;h2&gt;
  
  
  What FlowPick is, in one paragraph
&lt;/h2&gt;

&lt;p&gt;FlowPick is a Nuxt 4 app that ships in two shapes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A &lt;strong&gt;browser extension&lt;/strong&gt; that sniffs media from the current tab's network requests.&lt;/li&gt;
&lt;li&gt;An &lt;strong&gt;online tool website&lt;/strong&gt; with dedicated M3U8 and DASH downloaders.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;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.&lt;/p&gt;




&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Node.js 20 LTS&lt;/strong&gt; or newer&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;pnpm&lt;/strong&gt; (the repo scripts assume it; npm/yarn work if you align the lockfile)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Git&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node &lt;span class="nt"&gt;-v&lt;/span&gt;   &lt;span class="c"&gt;# should be v20.x or higher&lt;/span&gt;
pnpm &lt;span class="nt"&gt;-v&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 1 — Clone and install
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/ezwebtools/flowpick.git
&lt;span class="nb"&gt;cd &lt;/span&gt;flowpick
pnpm &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Heads up: the &lt;code&gt;postinstall&lt;/code&gt; script runs &lt;code&gt;nuxt prepare&lt;/code&gt; 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 &lt;code&gt;pnpm exec nuxt prepare&lt;/code&gt; to catch up.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 2 — Run it locally
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pnpm dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open &lt;strong&gt;&lt;a href="http://localhost:3000" rel="noopener noreferrer"&gt;http://localhost:3000&lt;/a&gt;&lt;/strong&gt;. The pages you care about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;/m3u8-downloader&lt;/code&gt; — for HLS streams&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/dash-downloader&lt;/code&gt; — for DASH streams&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/docs&lt;/code&gt; — full documentation&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/blog&lt;/code&gt; — tutorials and deep dives&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Prefer not to install anything? The same tools are live at &lt;strong&gt;&lt;a href="https://flowpick.net/m3u8-downloader" rel="noopener noreferrer"&gt;https://flowpick.net/m3u8-downloader&lt;/a&gt;&lt;/strong&gt; and &lt;strong&gt;&lt;a href="https://flowpick.net/dash-downloader" rel="noopener noreferrer"&gt;https://flowpick.net/dash-downloader&lt;/a&gt;&lt;/strong&gt;. That's the fastest way to test the workflow before you self-host.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🔗 Try it now: &lt;a href="https://flowpick.net" rel="noopener noreferrer"&gt;https://flowpick.net&lt;/a&gt; · M3U8 tool: &lt;a href="https://flowpick.net/m3u8-downloader" rel="noopener noreferrer"&gt;https://flowpick.net/m3u8-downloader&lt;/a&gt; · DASH tool: &lt;a href="https://flowpick.net/dash-downloader" rel="noopener noreferrer"&gt;https://flowpick.net/dash-downloader&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Step 3 — Download your first stream
&lt;/h2&gt;

&lt;h3&gt;
  
  
  HLS example
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Find a page that serves an &lt;code&gt;.m3u8&lt;/code&gt; stream. Open DevTools (F12) → &lt;strong&gt;Network&lt;/strong&gt; → filter &lt;code&gt;m3u8&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Play the video for a few seconds so the manifest actually gets requested.&lt;/li&gt;
&lt;li&gt;Right-click the &lt;code&gt;.m3u8&lt;/code&gt; request → &lt;strong&gt;Copy link address&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Paste it into the M3U8 Downloader (localhost or the hosted one).&lt;/li&gt;
&lt;li&gt;Hit &lt;strong&gt;Parse&lt;/strong&gt;. FlowPick reads the manifest and lists available qualities.&lt;/li&gt;
&lt;li&gt;Pick a quality, hit &lt;strong&gt;Start Download&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Under the hood FlowPick downloads every segment, decrypts if needed (AES-128), and merges them. For MP4 output it runs a remux —&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffmpeg &lt;span class="nt"&gt;-i&lt;/span&gt; input.ts &lt;span class="nt"&gt;-c&lt;/span&gt; copy output.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;— entirely in the browser. No re-encoding, no quality loss, and your file never touches a server.&lt;/p&gt;

&lt;h3&gt;
  
  
  DASH example
&lt;/h3&gt;

&lt;p&gt;Paste the &lt;code&gt;.mpd&lt;/code&gt; 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.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 The hosted tools at &lt;strong&gt;&lt;a href="https://flowpick.net" rel="noopener noreferrer"&gt;https://flowpick.net&lt;/a&gt;&lt;/strong&gt; are perfect when you're on a machine where you can't install the extension.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Step 4 — Build and deploy
&lt;/h2&gt;

&lt;p&gt;Production build:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pnpm build
pnpm preview
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The default Nitro preset is &lt;code&gt;cloudflare-pages-static&lt;/code&gt;, so &lt;code&gt;pnpm build&lt;/code&gt; emits static assets in &lt;code&gt;.output/&lt;/code&gt;. You can drop that on any static host — Cloudflare Pages, Netlify, Vercel, or your own cloud. Want a Node server instead? Switch &lt;code&gt;nitro.preset&lt;/code&gt; to &lt;code&gt;node-server&lt;/code&gt;, rebuild, and run &lt;code&gt;node .output/server/index.mjs&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One thing you must keep:&lt;/strong&gt; the downloader routes set two response headers —&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These enable &lt;strong&gt;cross-origin isolation&lt;/strong&gt;, which is what lets FFmpeg WASM use multiple threads (via &lt;code&gt;SharedArrayBuffer&lt;/code&gt;) and lets large files stream to disk via StreamSaver. Drop them and big downloads silently degrade.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 5 — Customize it
&lt;/h2&gt;

&lt;p&gt;Low-effort tweaks that go a long way:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Edit docs / copy&lt;/strong&gt;: everything in &lt;code&gt;content/&lt;/code&gt; is Markdown or YAML, validated by a Zod schema. Change text or add a page; it's live on reload.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tune the engine&lt;/strong&gt;: open &lt;code&gt;app/composables/useStreamMerge.ts&lt;/code&gt;. Default concurrency is &lt;code&gt;2&lt;/code&gt; (range &lt;code&gt;1–8&lt;/code&gt;); retries use exponential backoff (max &lt;code&gt;3&lt;/code&gt;); writes fall back &lt;code&gt;FSA → StreamSaver → Blob&lt;/code&gt; automatically.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Config&lt;/strong&gt;: &lt;code&gt;nuxt.config.ts&lt;/code&gt; holds i18n (en / zh-Hans / zh-Hant / ja / ko), runtime config for analytics (&lt;code&gt;NUXT_PUBLIC_GA_ID&lt;/code&gt;, &lt;code&gt;NUXT_PUBLIC_CLARITY_ID&lt;/code&gt;), and the route headers above.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;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 &lt;code&gt;useStreamMerge.ts&lt;/code&gt;. The composable's input shape is clean:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;StreamMergeOptions&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;segments&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;ArrayBuffer&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nx"&gt;AsyncGenerator&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;ArrayBuffer&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nx"&gt;totalSegments&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;
  &lt;span class="nx"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
  &lt;span class="nx"&gt;outputFormat&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mp4&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="nx"&gt;onProgress&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;progress&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;StreamMergeProgress&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;
  &lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;AbortSignal&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  How it actually works (the fun part)
&lt;/h2&gt;

&lt;p&gt;The engine is two composables:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;useStreamMerge.ts&lt;/code&gt; — downloads segments with a &lt;strong&gt;Worker Pool&lt;/strong&gt; (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.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;useFFmpeg.ts&lt;/code&gt; — loads &lt;code&gt;@ffmpeg/ffmpeg&lt;/code&gt; (WASM), detects multithreading support, and runs merge/remux commands.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The write strategy is the clever bit:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Strategy&lt;/th&gt;
&lt;th&gt;When&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;FSA&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Chrome/Edge 86+&lt;/td&gt;
&lt;td&gt;Streaming write, any size, constant memory&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;StreamSaver&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Wide compatibility&lt;/td&gt;
&lt;td&gt;Service Worker proxy, supports large files&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Blob&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Fallback&lt;/td&gt;
&lt;td&gt;Memory-limited; files over ~1.5 GB rejected&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Caveats — read before you ship
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;DRM-protected content (Widevine, etc.) can't be decoded.&lt;/strong&gt; By design.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The web tool is subject to CORS.&lt;/strong&gt; If the stream server doesn't send cross-origin headers, use the extension instead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;FFmpeg WASM is ~8 MB on first load&lt;/strong&gt; (cached afterward).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Big-file experience is best on Chrome/Edge&lt;/strong&gt;; Safari falls back to Blob mode.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;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.&lt;/p&gt;




&lt;h2&gt;
  
  
  Wrap-up
&lt;/h2&gt;

&lt;p&gt;That's the whole loop: clone → run → download your first stream → build → deploy → customize. FlowPick is open source on &lt;strong&gt;&lt;a href="https://github.com/ezwebtools/flowpick" rel="noopener noreferrer"&gt;https://github.com/ezwebtools/flowpick&lt;/a&gt;&lt;/strong&gt;, and you can try the hosted tools right now at &lt;strong&gt;&lt;a href="https://flowpick.net" rel="noopener noreferrer"&gt;https://flowpick.net&lt;/a&gt;&lt;/strong&gt; (M3U8: &lt;strong&gt;&lt;a href="https://flowpick.net/m3u8-downloader" rel="noopener noreferrer"&gt;https://flowpick.net/m3u8-downloader&lt;/a&gt;&lt;/strong&gt;, DASH: &lt;strong&gt;&lt;a href="https://flowpick.net/dash-downloader" rel="noopener noreferrer"&gt;https://flowpick.net/dash-downloader&lt;/a&gt;&lt;/strong&gt;).&lt;/p&gt;

&lt;p&gt;If this was useful, drop a ❤️ or a comment, and tell me what you'd build on top of it. Happy hacking!&lt;/p&gt;

</description>
      <category>productivity</category>
      <category>opensource</category>
      <category>ffmpeg</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
