<?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>Forem: hego8</title>
    <description>The latest articles on Forem by hego8 (@hego8).</description>
    <link>https://forem.com/hego8</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%2F3503860%2F1f0a6469-2c36-4b3d-978e-dac51c1bc036.png</url>
      <title>Forem: hego8</title>
      <link>https://forem.com/hego8</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/hego8"/>
    <language>en</language>
    <item>
      <title>Build a YouTube Downloader CLI | Merge Video/Audio | Fix Noise</title>
      <dc:creator>hego8</dc:creator>
      <pubDate>Mon, 15 Sep 2025 13:39:31 +0000</pubDate>
      <link>https://forem.com/hego8/build-a-youtube-downloader-cli-merge-videoaudio-fix-noise-4obb</link>
      <guid>https://forem.com/hego8/build-a-youtube-downloader-cli-merge-videoaudio-fix-noise-4obb</guid>
      <description>&lt;p&gt;&lt;strong&gt;Short summary:&lt;/strong&gt; This writeup collects the practical problems I hit while building a lightweight Node.js CLI downloader and the concrete solutions I used. It’s intended as a hands-on guide you can copy into your project: detecting formats, downloading &lt;code&gt;video-only&lt;/code&gt; + &lt;code&gt;audio-only&lt;/code&gt;, merging with &lt;code&gt;ffmpeg&lt;/code&gt;, why merged audio can sound noisy, and how to clean it. At the end I add a short note about a ready tool that implements these ideas.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Problem overview: what actually goes wrong (and why you should care)
&lt;/h2&gt;

&lt;p&gt;When you try to download YouTube content at a specific quality you want, you’ll immediately encounter a few recurring issues:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Choosing quality does not always give the expected result.&lt;/strong&gt; Many combined (audio+video) formats are only available at low resolutions (e.g., 360p). Higher resolutions are offered as &lt;em&gt;separate&lt;/em&gt; streams (video-only + audio-only). If your downloader blindly picks “the best combined format”, you end up with 360p even when 1080p is available as video-only.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Merging separate streams is required&lt;/strong&gt; if you want higher resolution video + audio. That means download two streams and run an external tool like &lt;code&gt;ffmpeg&lt;/code&gt; to compose them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Post-merge audio quality can degrade.&lt;/strong&gt; After merging, some outputs exhibit background noise/hiss or other artifacts. This is surprising because you downloaded the original audio stream — the merging/processing step introduced or exposed artifacts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-platform packaging and dependencies.&lt;/strong&gt; Bundling or requiring &lt;code&gt;ffmpeg&lt;/code&gt; across Windows/macOS/Linux has pitfalls unless you handle binaries carefully.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Playlists, rate limits and UX.&lt;/strong&gt; Handling playlists, retries and user feedback (progress bars) requires additional design.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you or your team plan to build a downloader, solving these correctly makes the difference between a toy and a reliable tool.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Strategy &amp;amp; architecture (high level)
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Enumerate formats&lt;/strong&gt; for a URL (video-only, audio-only, combined).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Decide&lt;/strong&gt;: if user requested resolution is available as combined format → download combined; otherwise download video-only + audio-only.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Download streams concurrently&lt;/strong&gt; (with retries, backoff, and progress tracking).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Merge&lt;/strong&gt; using &lt;code&gt;ffmpeg&lt;/code&gt;. Prefer stream copy when possible (&lt;code&gt;-c copy&lt;/code&gt;) to avoid re-encoding, but re-encode if you must normalize or filter audio.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Post-process&lt;/strong&gt; audio with a light noise-reduction filter if artifacts are present.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Provide a good UX&lt;/strong&gt;: interactive mode and flags, progress bars, verbose logs for debugging.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bundle or locate ffmpeg&lt;/strong&gt; reliably (e.g., &lt;code&gt;ffmpeg-static&lt;/code&gt; or provide clear instructions).&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  3. Practical code snippets &amp;amp; notes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  3.1 — Detect available formats with &lt;code&gt;ytdl-core&lt;/code&gt; (Node.js)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Example: list formats and pick best video-only + audio-only&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ytdl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ytdl-core&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;listFormats&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;info&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ytdl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getInfo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;formats&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;info&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;formats&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;videoOnly&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;formats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hasVideo&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hasAudio&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                           &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;audioOnly&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;formats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hasAudio&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hasVideo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                           &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;audioBitrate&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;audioBitrate&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;combined&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;formats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hasVideo&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hasAudio&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                          &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;qualityLabel&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;localeCompare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;qualityLabel&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;videoOnly&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;audioOnly&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;combined&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Tip:&lt;/strong&gt; show these options to the user and allow explicit selection (or implement “smart selection” that picks the best available combination).&lt;/p&gt;




&lt;h3&gt;
  
  
  3.2 — Downloading streams (concurrent with progress)
&lt;/h3&gt;

&lt;p&gt;Use streaming writes and a progress library (e.g., &lt;code&gt;cli-progress&lt;/code&gt;) so the user sees download progress:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ytdl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ytdl-core&lt;/span&gt;&lt;span class="dl"&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;downloadFormat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;format&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;outputPath&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;ytdl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;format&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="nx"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pipe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createWriteStream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;outputPath&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="nx"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;progress&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chunkLength&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;downloaded&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;total&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// update progress bar&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="nx"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;end&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="nx"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Retries:&lt;/strong&gt; wrap downloads with retry/backoff to handle transient network or YouTube throttling issues.&lt;/p&gt;




&lt;h3&gt;
  
  
  3.3 — Merge with FFmpeg (basic merge, copy streams)
&lt;/h3&gt;

&lt;p&gt;When formats are compatible (same container/codecs), prefer &lt;code&gt;-c copy&lt;/code&gt;.&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; video.mp4 &lt;span class="nt"&gt;-i&lt;/span&gt; audio.m4a &lt;span class="nt"&gt;-c&lt;/span&gt; copy merged.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;When to re-encode:&lt;/strong&gt; if audio codec/container mismatch or you want to apply filters, you must re-encode:&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; video.mp4 &lt;span class="nt"&gt;-i&lt;/span&gt; audio.m4a &lt;span class="nt"&gt;-c&lt;/span&gt;:v copy &lt;span class="nt"&gt;-c&lt;/span&gt;:a aac &lt;span class="nt"&gt;-b&lt;/span&gt;:a 192k merged.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; re-encoding audio may slightly change the quality; choose sensible defaults (AAC 128–192 kbps or keep original bitrate if possible).&lt;/p&gt;




&lt;h3&gt;
  
  
  3.4 — The audio noise problem and solutions
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt; merged audio has extra background noise / hiss that wasn’t obvious in the original stream.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Possible causes:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Mismatch in sample rates or codecs requiring ffmpeg to resample/re-encode.&lt;/li&gt;
&lt;li&gt;Bad muxing path when using copy and then later rewrapping.&lt;/li&gt;
&lt;li&gt;Inadvertent transforms (volume normalization, resampling) when using filters or wrong flags.&lt;/li&gt;
&lt;li&gt;Source streams themselves may be compressed with artifacts that become more noticeable after merging.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Practical fixes:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Prefer native ffmpeg denoisers&lt;/strong&gt; if artifacts are present. Example:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# afftdn: frequency-domain denoiser&lt;/span&gt;
ffmpeg &lt;span class="nt"&gt;-i&lt;/span&gt; merged.mp4 &lt;span class="nt"&gt;-af&lt;/span&gt; &lt;span class="s2"&gt;"afftdn=nf=-25"&lt;/span&gt; final_clean.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Highpass/lowpass&lt;/strong&gt; to remove low-frequency rumble:
&lt;/li&gt;
&lt;/ol&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; merged.mp4 &lt;span class="nt"&gt;-af&lt;/span&gt; &lt;span class="s2"&gt;"highpass=f=70, lowpass=f=12000"&lt;/span&gt; final_clean.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Custom filters / internal filter&lt;/strong&gt;
If you have a custom filter or tuned profile (for me, named &lt;code&gt;mp.rnnn&lt;/code&gt; internally), run it as an &lt;code&gt;-af&lt;/code&gt; parameter:
&lt;/li&gt;
&lt;/ol&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; merged.mp4 &lt;span class="nt"&gt;-af&lt;/span&gt; &lt;span class="s2"&gt;"mp.rnnn"&lt;/span&gt; final_clean.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Advice:&lt;/strong&gt; start with &lt;code&gt;afftdn&lt;/code&gt; and simple highpass/lowpass. Only add complex or proprietary filters after testing. Always keep the original audio saved so you can A/B test.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Ensure consistent sampling&lt;/strong&gt;
Force a sample rate to avoid hidden resampling artifacts:
&lt;/li&gt;
&lt;/ol&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; merged.mp4 &lt;span class="nt"&gt;-ar&lt;/span&gt; 48000 &lt;span class="nt"&gt;-ac&lt;/span&gt; 2 &lt;span class="nt"&gt;-af&lt;/span&gt; &lt;span class="s2"&gt;"afftdn=nf=-25"&lt;/span&gt; final_clean.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Automate detection + conditional filtering&lt;/strong&gt;
Run a quick audio quality check (e.g., compute RMS or detect noise floor) and apply the filter conditionally when noise passes a threshold.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  4. Playlist handling &amp;amp; ranges
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;List extraction:&lt;/strong&gt; get playlist video IDs, then loop over them with pagination.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Range downloads:&lt;/strong&gt; accept &lt;code&gt;"start-end"&lt;/code&gt; strings and map them to indices.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Concurrency limits:&lt;/strong&gt; for large playlists, download in small batches (e.g., 3 concurrent downloads) to avoid bandwidth spikes or hitting YouTube rate limits.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resume support:&lt;/strong&gt; keep partial files and resume if possible (ytdl supports range requests in many cases).&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  5. Packaging ffmpeg &amp;amp; cross-platform considerations
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Use &lt;a href="https://www.npmjs.com/package/ffmpeg-static" rel="noopener noreferrer"&gt;&lt;code&gt;ffmpeg-static&lt;/code&gt;&lt;/a&gt; as a convenient bundled binary for most platforms. It simplifies CI and end-user installs.&lt;/li&gt;
&lt;li&gt;If you need full control or want smaller distribution, document how to install a system &lt;code&gt;ffmpeg&lt;/code&gt; and allow an environment variable override (e.g., &lt;code&gt;FFMPEG_PATH&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Watch license: &lt;code&gt;ffmpeg&lt;/code&gt; has LGPL/GPL components depending on build; pick builds accordingly and document licenses.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  6. UX &amp;amp; CLI tips
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Interactive mode&lt;/strong&gt; for beginners: prompt for URL, quality, format.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Flags&lt;/strong&gt; for automation: &lt;code&gt;--url&lt;/code&gt;, &lt;code&gt;--type&lt;/code&gt;, &lt;code&gt;--quality&lt;/code&gt;, &lt;code&gt;--output&lt;/code&gt;, &lt;code&gt;--no-progress&lt;/code&gt;, &lt;code&gt;--verbose&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verbose logs&lt;/strong&gt;: include format selection details (which format IDs you picked and why). This is invaluable for debugging format/quality issues.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Progress bars&lt;/strong&gt;: show per-stream progress and an overall task progress for playlists.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exit codes&lt;/strong&gt;: make exit codes meaningful (network error, ffmpeg error, invalid URL, permission denied).&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  7. Testing &amp;amp; validation
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A/B test&lt;/strong&gt;: compare original audio stream and final audio after merge + filter. Keep both and allow quick playback.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sample videos&lt;/strong&gt;: maintain a test set containing different encodings/resolutions (360p combined, 720p+audio separate, DASH formats).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automated CI&lt;/strong&gt;: run unit tests on format selection logic and integration tests for small merges. For &lt;code&gt;ffmpeg&lt;/code&gt; steps, you can run smoke checks (exists file, duration matches input within small delta).&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  8. Troubleshooting checklist (quick)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;If the downloader keeps giving 360p:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Inspect available formats (&lt;code&gt;ytdl-core&lt;/code&gt; info) and look for video-only formats at desired resolution.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;If merge fails:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Check &lt;code&gt;ffmpeg&lt;/code&gt; stdout, container/codec mismatches, and try re-encoding instead of &lt;code&gt;-c copy&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;If audio noisy after merge:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Try &lt;code&gt;afftdn&lt;/code&gt;, highpass/lowpass; check sample rates and re-encode audio to a stable sample rate (e.g., 48k).&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;If downloads slow or fail intermittently:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Implement retries with exponential backoff; reduce concurrency.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;




&lt;h2&gt;
  
  
  9. Example end-to-end shell flow (what the tool automates)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# download best video-only at requested resolution&lt;/span&gt;
ytdl &lt;span class="s1"&gt;'https://youtube.com/watch?v=ID'&lt;/span&gt; &lt;span class="nt"&gt;--format&lt;/span&gt; videoonly &lt;span class="nt"&gt;-o&lt;/span&gt; video.mp4

&lt;span class="c"&gt;# download best audio-only&lt;/span&gt;
ytdl &lt;span class="s1"&gt;'https://youtube.com/watch?v=ID'&lt;/span&gt; &lt;span class="nt"&gt;--format&lt;/span&gt; audioonly &lt;span class="nt"&gt;-o&lt;/span&gt; audio.m4a

&lt;span class="c"&gt;# merge (prefer copy if compatible)&lt;/span&gt;
ffmpeg &lt;span class="nt"&gt;-i&lt;/span&gt; video.mp4 &lt;span class="nt"&gt;-i&lt;/span&gt; audio.m4a &lt;span class="nt"&gt;-c&lt;/span&gt;:v copy &lt;span class="nt"&gt;-c&lt;/span&gt;:a copy merged.mp4

&lt;span class="c"&gt;# if artifacts exist -&amp;gt; filter audio&lt;/span&gt;
ffmpeg &lt;span class="nt"&gt;-i&lt;/span&gt; merged.mp4 &lt;span class="nt"&gt;-af&lt;/span&gt; &lt;span class="s2"&gt;"afftdn=nf=-25"&lt;/span&gt; final_clean.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  11. Small section: about my ready tool that i built
&lt;/h2&gt;

&lt;p&gt;If you’d rather try a ready implementation of the above ideas, I created a small CLI called &lt;strong&gt;Downtube&lt;/strong&gt; that automates the workflow described here: format detection, video-only + audio-only downloading, &lt;code&gt;ffmpeg&lt;/code&gt; merging, and an optional noise-filter step. It also supports playlists, range downloads, progress indicators, and a simple interactive CLI.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Try it / see source:&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://github.com/ahegazy0/downtube" rel="noopener noreferrer"&gt;https://github.com/ahegazy0/downtube&lt;/a&gt;&lt;/p&gt;




</description>
      <category>node</category>
      <category>npm</category>
      <category>javascript</category>
      <category>cli</category>
    </item>
  </channel>
</rss>
