<?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: Ertuğrul Kutluer</title>
    <description>The latest articles on DEV Community by Ertuğrul Kutluer (@erturul_kutluer_11ba8e80).</description>
    <link>https://dev.to/erturul_kutluer_11ba8e80</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3959162%2F11e24a41-c112-4538-a3d2-c6d96266bc39.jpg</url>
      <title>DEV Community: Ertuğrul Kutluer</title>
      <link>https://dev.to/erturul_kutluer_11ba8e80</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/erturul_kutluer_11ba8e80"/>
    <language>en</language>
    <item>
      <title>I built a YouTube downloader where the video bytes never touch my server</title>
      <dc:creator>Ertuğrul Kutluer</dc:creator>
      <pubDate>Fri, 29 May 2026 22:20:41 +0000</pubDate>
      <link>https://dev.to/erturul_kutluer_11ba8e80/i-built-a-youtube-downloader-where-the-video-bytes-never-touch-my-server-g1o</link>
      <guid>https://dev.to/erturul_kutluer_11ba8e80/i-built-a-youtube-downloader-where-the-video-bytes-never-touch-my-server-g1o</guid>
      <description>&lt;p&gt;I've been running &lt;a href="https://vidpickr.com" rel="noopener noreferrer"&gt;VidPickr&lt;/a&gt; for a few months and the thing that took me longest to get right wasn't the YouTube extraction part. It was deciding &lt;strong&gt;where&lt;/strong&gt; to do the actual mux.&lt;/p&gt;

&lt;p&gt;Every other YouTube downloader I looked at does it the same way:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[your browser]  →  "give me this video please"
                            ↓
                     [their server]
                            ↓
              downloads the YouTube streams
                            ↓
                 runs ffmpeg, muxes it
                            ↓
                  sends it back to you
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is fine when you have twelve users. Once you have any traffic, it gets brutal. You're paying bandwidth twice (YouTube → you → user), the server has to keep temp files around while it works, and the whole thing tips over the moment some popular video starts trending.&lt;/p&gt;

&lt;p&gt;I tried it that way first, of course. Worked great on my laptop. Then I left it on a small VPS overnight, woke up to a full disk because temp files weren't getting cleaned up fast enough. That was the moment I started looking for a different shape.&lt;/p&gt;

&lt;h2&gt;
  
  
  What if the browser just did it itself
&lt;/h2&gt;

&lt;p&gt;YouTube serves videos as separate streams. Video on one URL, audio on another, sometimes a third video stream at a different resolution. The "mux" step is interleaving those into an MP4 container with proper headers.&lt;/p&gt;

&lt;p&gt;ffmpeg does that. So does pretty much any decent JS muxer. And modern browsers have WebCodecs now.&lt;/p&gt;

&lt;p&gt;So the shape I ended up with looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[browser]  →  my API: "what are the stream URLs for this video id?"
[my API]   →  returns JSON: { video_url, audio_url, ...metadata }
[browser]  →  fetches BOTH streams directly from googlevideo.com
[browser]  →  muxes locally with a WebCodecs-based muxer
[browser]  →  saves the file
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;My server's job in the whole flow is returning around 5 KB of JSON. The actual video never gets close to my infra.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pipeline, more or less
&lt;/h2&gt;

&lt;p&gt;The download/mux loop ends up looking something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// fetch both streams in parallel&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;videoChunks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;audioChunks&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
  &lt;span class="nf"&gt;streamSegments&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;videoUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onVideoChunk&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="nf"&gt;streamSegments&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;audioUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onAudioChunk&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;]);&lt;/span&gt;

&lt;span class="c1"&gt;// feed chunks into the muxer as they arrive&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;onVideoChunk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;muxer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addVideoChunk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;onAudioChunk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;muxer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addAudioChunk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;muxer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;finalize&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nf"&gt;saveAs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;muxer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;output&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;video.mp4&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Real code has range requests, progress tracking, error recovery on flaky chunks, all the boring stuff. But the shape is exactly that.&lt;/p&gt;

&lt;h2&gt;
  
  
  The annoying bits
&lt;/h2&gt;

&lt;p&gt;A few things bit me that I would not have guessed up front.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Init segments are separate from data segments.&lt;/strong&gt; YouTube's segmented streams come with a tiny init segment that has the codec params, and you can't just concat it onto the front of the data. You have to feed it to the muxer first, &lt;em&gt;then&lt;/em&gt; start streaming the data chunks in. I spent two evenings convinced the muxer was busted before I figured this out.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CORS.&lt;/strong&gt; googlevideo.com does not return CORS headers for arbitrary origins. Your two options are run a proxy (which defeats the whole point of the architecture), or use a session-bound URL that the browser is pre-authorized for. I ended up doing the second thing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;WebCodecs support is uneven.&lt;/strong&gt; Chrome and Edge are great. Safari has partial support that keeps improving. Firefox does not have it. So I have a slower WASM-based fallback path for everything that isn't Chromium.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Memory.&lt;/strong&gt; A 4K hour-long video muxes out to something close to 10 GB. You cannot hold that in RAM and hope. The fix is streaming the muxer's output to disk as you go, using the File System Access API where it exists and chunked downloads where it doesn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it gets you
&lt;/h2&gt;

&lt;p&gt;Once this was working, the operational story flipped:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The server does basically nothing during a download. CPU is flat.&lt;/li&gt;
&lt;li&gt;I cannot log what people downloaded even if I wanted to, because the URLs don't pass through me.&lt;/li&gt;
&lt;li&gt;My bandwidth bill is for the API JSON, which is rounding-error money.&lt;/li&gt;
&lt;li&gt;"Scale" is whatever the user's laptop can handle. One user, ten thousand users, my infra does not notice.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The tradeoff is that it's a worse experience on a phone or an old ThinkPad. I keep a server-side path around as a paid fallback for people who actually need that.&lt;/p&gt;




&lt;p&gt;If you want to poke at it: &lt;a href="https://vidpickr.com" rel="noopener noreferrer"&gt;vidpickr.com&lt;/a&gt;. There's also a REST API at &lt;code&gt;api.vidpickr.com&lt;/code&gt; for the cases where you do actually want server-side extraction (cron jobs, agent pipelines, that kind of thing).&lt;/p&gt;

&lt;p&gt;Happy to answer anything in the comments about the WebCodecs side or the YouTube extraction side. Curious what the worst gotcha anyone else has hit doing browser-native media work is — mine was definitely the init-segment thing.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>showdev</category>
      <category>performance</category>
    </item>
  </channel>
</rss>
