<?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: Javid Jamae</title>
    <description>The latest articles on DEV Community by Javid Jamae (@javidjamae).</description>
    <link>https://dev.to/javidjamae</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%2F3873707%2F97b6ee49-1448-4e30-b41a-14c1f4eec9cc.jpg</url>
      <title>DEV Community: Javid Jamae</title>
      <link>https://dev.to/javidjamae</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/javidjamae"/>
    <language>en</language>
    <item>
      <title>FFmpeg on Firebase Cloud Functions: Why It Breaks (and the Fix)</title>
      <dc:creator>Javid Jamae</dc:creator>
      <pubDate>Sun, 26 Jul 2026 10:27:17 +0000</pubDate>
      <link>https://dev.to/javidjamae/ffmpeg-on-firebase-cloud-functions-why-it-breaks-and-the-fix-bjg</link>
      <guid>https://dev.to/javidjamae/ffmpeg-on-firebase-cloud-functions-why-it-breaks-and-the-fix-bjg</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.ffmpeg-micro.com/blog/ffmpeg-firebase-cloud-functions-fix" rel="noopener noreferrer"&gt;ffmpeg-micro.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;You're adding video processing to a Firebase app. You install a static FFmpeg binary, write a Cloud Function that triggers on upload, deploy it, and watch it die. The function times out, the binary bloats your deployment, and GCP bills you for memory you didn't actually need.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Timeout Wall
&lt;/h2&gt;

&lt;p&gt;Firebase Cloud Functions v1 default to a 60-second timeout. You can push that to 540 seconds (9 minutes) on v2 functions, and that's a hard cap.&lt;/p&gt;

&lt;p&gt;Transcoding a 5-minute 1080p video to H.264 takes 8-15 minutes on Cloud Functions CPU allocation. The 540-second ceiling doesn't come close.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Memory and CPU Trap
&lt;/h2&gt;

&lt;p&gt;Cloud Functions v2 gives you up to 32 GB of memory, but CPU allocation is tied to memory. At 2 GB memory, you get 1 vCPU. FFmpeg encoding is CPU-bound, not memory-bound. You end up over-provisioning memory just to get more CPU.&lt;/p&gt;

&lt;h2&gt;
  
  
  Binary Bundling Problems
&lt;/h2&gt;

&lt;p&gt;Getting FFmpeg into a Cloud Function means either bundling via npm (&lt;code&gt;ffmpeg-static&lt;/code&gt;, 70-100 MB), including the binary directly, or downloading at runtime. Cloud Functions has a 100 MB compressed deployment limit.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fix: Call an API Instead
&lt;/h2&gt;

&lt;p&gt;Keep your function small and offload the encoding:&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;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;onObjectFinalized&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;firebase-functions/v2/storage&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;defineSecret&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;firebase-functions/params&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;getStorage&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;firebase-admin/storage&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;initializeApp&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;firebase-admin/app&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;initializeApp&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;ffmpegMicroKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;defineSecret&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;FFMPEG_MICRO_API_KEY&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;processUploadedVideo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;onObjectFinalized&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;my-app-videos&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;secrets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;ffmpegMicroKey&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;256MiB&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;timeoutSeconds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;contentType&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;contentType&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;video/&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bucket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getStorage&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;);&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;signedUrl&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="nx"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;getSignedUrl&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;read&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;expires&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&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;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://api.ffmpeg-micro.com/v1/transcodes&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="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;ffmpegMicroKey&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;value&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&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="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
          &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;signedUrl&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
          &lt;span class="na"&gt;outputFormat&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mp4&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;preset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;quality&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;high&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;resolution&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;1080p&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="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;job&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;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Transcode job &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; started for &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&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;This function deploys in seconds, cold-starts in under a second, and needs only 256 MB of memory.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.ffmpeg-micro.com/blog/ffmpeg-firebase-cloud-functions-fix" rel="noopener noreferrer"&gt;Read the full post with detailed analysis&lt;/a&gt;&lt;/p&gt;

</description>
      <category>firebase</category>
      <category>ffmpeg</category>
      <category>serverless</category>
      <category>video</category>
    </item>
    <item>
      <title>FFmpeg -map Flag Explained: Stream Selection and Multi-Track Audio</title>
      <dc:creator>Javid Jamae</dc:creator>
      <pubDate>Sun, 26 Jul 2026 10:26:48 +0000</pubDate>
      <link>https://dev.to/javidjamae/ffmpeg-map-flag-explained-stream-selection-and-multi-track-audio-1mho</link>
      <guid>https://dev.to/javidjamae/ffmpeg-map-flag-explained-stream-selection-and-multi-track-audio-1mho</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.ffmpeg-micro.com/blog/ffmpeg-map-flag-explained" rel="noopener noreferrer"&gt;ffmpeg-micro.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;You run &lt;code&gt;ffmpeg -i input.mkv output.mp4&lt;/code&gt; and somehow your output is missing the commentary audio track. Or it grabbed the wrong subtitle language. FFmpeg's automatic stream selection does its best, but "best" rarely matches what you actually want when you're working with multi-track files.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;-map&lt;/code&gt; flag gives you explicit control over which streams go into your output. This post walks through how &lt;code&gt;-map&lt;/code&gt; actually works, with examples you can copy and run.&lt;/p&gt;

&lt;h2&gt;
  
  
  How FFmpeg Picks Streams Without -map
&lt;/h2&gt;

&lt;p&gt;When you don't use &lt;code&gt;-map&lt;/code&gt;, FFmpeg applies its automatic stream selection algorithm. It picks one video stream (the highest resolution), one audio stream (the most channels), and one subtitle stream (the first it finds). Everything else gets dropped.&lt;/p&gt;

&lt;h2&gt;
  
  
  Identify Your Streams First with ffprobe
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffprobe &lt;span class="nt"&gt;-v&lt;/span&gt; error &lt;span class="nt"&gt;-show_entries&lt;/span&gt; &lt;span class="nv"&gt;stream&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;index,codec_type,codec_name,channels &lt;span class="nt"&gt;-of&lt;/span&gt; json input.mkv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Understanding the -map Syntax
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;-map&lt;/code&gt; argument follows a consistent pattern: &lt;code&gt;input_file:stream_type:stream_index&lt;/code&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Specifier&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;0:v&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;All video streams&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;0:a:0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;First audio stream&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;0:a:1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Second audio stream&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;0:s&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;All subtitle streams&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Selecting Specific Streams
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Keep only video and the first audio track:&lt;/strong&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; input.mkv &lt;span class="nt"&gt;-map&lt;/span&gt; 0:v &lt;span class="nt"&gt;-map&lt;/span&gt; 0:a:0 &lt;span class="nt"&gt;-c&lt;/span&gt; copy output.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Grab only the second audio track:&lt;/strong&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; input.mkv &lt;span class="nt"&gt;-map&lt;/span&gt; 0:a:1 &lt;span class="nt"&gt;-c&lt;/span&gt;:a copy stereo-audio.aac
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Discarding Streams
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Negative mapping with &lt;code&gt;-map -&lt;/code&gt;:&lt;/strong&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; input.mkv &lt;span class="nt"&gt;-map&lt;/span&gt; 0 &lt;span class="nt"&gt;-map&lt;/span&gt; &lt;span class="nt"&gt;-0&lt;/span&gt;:s &lt;span class="nt"&gt;-c&lt;/span&gt; copy output.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Common Pitfalls
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Forgetting that &lt;code&gt;-map&lt;/code&gt; disables automatic selection&lt;/li&gt;
&lt;li&gt;Using absolute indices instead of type specifiers&lt;/li&gt;
&lt;li&gt;Mapping subtitle streams into MP4 (use MKV instead)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Using -map with an API
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://api.ffmpeg-micro.com/v1/transcodes &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer YOUR_API_KEY"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "inputs": [{"url": "https://storage.example.com/multi-track.mkv"}],
    "outputFormat": "mp4",
    "options": [
      {"option": "-map", "argument": "0:v"},
      {"option": "-map", "argument": "0:a:1"}
    ]
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://www.ffmpeg-micro.com/blog/ffmpeg-map-flag-explained" rel="noopener noreferrer"&gt;Read the full post with all examples&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ffmpeg</category>
      <category>video</category>
      <category>devops</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How to Add AI Voiceover to Video with ElevenLabs and FFmpeg Micro</title>
      <dc:creator>Javid Jamae</dc:creator>
      <pubDate>Sat, 25 Jul 2026 10:14:45 +0000</pubDate>
      <link>https://dev.to/javidjamae/how-to-add-ai-voiceover-to-video-with-elevenlabs-and-ffmpeg-micro-ac</link>
      <guid>https://dev.to/javidjamae/how-to-add-ai-voiceover-to-video-with-elevenlabs-and-ffmpeg-micro-ac</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.ffmpeg-micro.com/blog/add-ai-voiceover-elevenlabs-ffmpeg-micro" rel="noopener noreferrer"&gt;ffmpeg-micro.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;You've got an ElevenLabs API key generating polished AI narration in seconds. But now you need that voiceover merged into a video file, and you're staring at FFmpeg documentation wondering how to get &lt;code&gt;-filter_complex amix&lt;/code&gt; working without blowing up your audio levels.&lt;/p&gt;

&lt;p&gt;If you're running serverless (Vercel, AWS Lambda, Cloud Functions), you can't even install FFmpeg. And if you're on a VPS, you'll spend more time debugging audio sync and codec mismatches than building your actual product.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Quick answer:&lt;/strong&gt; Generate voiceover audio with the ElevenLabs API, then merge it into video with one FFmpeg Micro API call. No FFmpeg binary needed. Works from any language or automation tool that can make HTTP requests.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The pipeline: text to narrated video
&lt;/h2&gt;

&lt;p&gt;The workflow has two steps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;ElevenLabs&lt;/strong&gt; converts your script text to an MP3 audio file&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;FFmpeg Micro&lt;/strong&gt; merges that MP3 into your source video, replacing or mixing with the original audio&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both are REST APIs. No binaries, no containers, no local processing. The entire pipeline runs with two HTTP calls.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Generate voiceover with ElevenLabs
&lt;/h2&gt;

&lt;p&gt;Call the ElevenLabs text-to-speech endpoint to generate narration. You'll get back an MP3 file that you host somewhere accessible via URL (an S3 bucket, Supabase Storage, or any public URL works).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"https://api.elevenlabs.io/v1/text-to-speech/21m00Tcm4TlvDq8ikWAM"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"xi-api-key: &lt;/span&gt;&lt;span class="nv"&gt;$ELEVENLABS_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "text": "FFmpeg Micro lets you process video with a single API call. No installation, no infrastructure.",
    "model_id": "eleven_multilingual_v2"
  }'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output&lt;/span&gt; voiceover.mp3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Upload the resulting &lt;code&gt;voiceover.mp3&lt;/code&gt; to any publicly accessible URL. For this tutorial, assume it's at &lt;code&gt;https://storage.example.com/voiceover.mp3&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If you're using FFmpeg Micro's upload flow, you can use the presigned upload endpoint to get a GCS URL directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. Get a presigned upload URL&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"https://api.ffmpeg-micro.com/v1/upload/presigned-url"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$FFMPEG_MICRO_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"filename": "voiceover.mp3", "contentType": "audio/mpeg", "fileSize": 45000}'&lt;/span&gt;

&lt;span class="c"&gt;# 2. Upload the file to the returned URL&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; PUT &lt;span class="s2"&gt;"&amp;lt;uploadUrl from response&amp;gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: audio/mpeg"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data-binary&lt;/span&gt; @voiceover.mp3

&lt;span class="c"&gt;# 3. Confirm the upload&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"https://api.ffmpeg-micro.com/v1/upload/confirm"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$FFMPEG_MICRO_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"filename": "&amp;lt;filename from presign response&amp;gt;", "fileSize": 45000}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 2: Merge voiceover into video with FFmpeg Micro
&lt;/h2&gt;

&lt;p&gt;Now merge the audio into your video. FFmpeg Micro's transcode endpoint takes multiple inputs, so you pass the video as the first input and the voiceover audio as the second:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"https://api.ffmpeg-micro.com/v1/transcodes"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$FFMPEG_MICRO_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "inputs": [
      {"url": "https://storage.example.com/source-video.mp4"},
      {"url": "https://storage.example.com/voiceover.mp3"}
    ],
    "outputFormat": "mp4",
    "options": [
      {"option": "-c:v", "argument": "copy"},
      {"option": "-c:a", "argument": "aac"},
      {"option": "-b:a", "argument": "192k"},
      {"option": "-map", "argument": "0:v:0"},
      {"option": "-map", "argument": "1:a:0"},
      {"option": "-shortest", "argument": ""}
    ]
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This maps the video stream from the first input (&lt;code&gt;0:v:0&lt;/code&gt;) and the audio stream from the second input (&lt;code&gt;1:a:0&lt;/code&gt;). The &lt;code&gt;-shortest&lt;/code&gt; flag trims the output to whichever track ends first, so a 60-second video with a 45-second voiceover produces a 45-second result.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;-c:v copy&lt;/code&gt; flag skips video re-encoding entirely, which makes the operation fast. Only the audio gets encoded to AAC.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mixing voiceover with background audio
&lt;/h2&gt;

&lt;p&gt;If your video already has music or ambient audio and you want to layer the voiceover on top (instead of replacing it), use FFmpeg's &lt;code&gt;amix&lt;/code&gt; filter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"https://api.ffmpeg-micro.com/v1/transcodes"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$FFMPEG_MICRO_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "inputs": [
      {"url": "https://storage.example.com/source-video.mp4"},
      {"url": "https://storage.example.com/voiceover.mp3"}
    ],
    "outputFormat": "mp4",
    "options": [
      {"option": "-filter_complex", "argument": "[0:a][1:a]amix=inputs=2:duration=first:dropout_transition=2[aout]"},
      {"option": "-map", "argument": "0:v:0"},
      {"option": "-map", "argument": "[aout]"},
      {"option": "-c:v", "argument": "copy"},
      {"option": "-c:a", "argument": "aac"}
    ]
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;amix&lt;/code&gt; filter combines both audio tracks. &lt;code&gt;duration=first&lt;/code&gt; keeps the output the same length as the original video. &lt;code&gt;dropout_transition=2&lt;/code&gt; adds a 2-second fade when the shorter track ends, preventing an abrupt cut.&lt;/p&gt;

&lt;h2&gt;
  
  
  Node.js: the full pipeline in one script
&lt;/h2&gt;

&lt;p&gt;Here's the complete flow in Node.js, from ElevenLabs text-to-speech to merged video:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ELEVENLABS_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ELEVENLABS_API_KEY&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;FFMPEG_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;FFMPEG_MICRO_API_KEY&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;createNarratedVideo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&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="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// 1. Generate voiceover with ElevenLabs&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;voiceRes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.elevenlabs.io/v1/text-to-speech/21m00Tcm4TlvDq8ikWAM&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="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;xi-api-key&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ELEVENLABS_KEY&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&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="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;model_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;eleven_multilingual_v2&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="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Upload the audio to your storage and get a public URL&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;audioBuffer&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;voiceRes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;arrayBuffer&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;audioUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;uploadToStorage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;audioBuffer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// your upload logic&lt;/span&gt;

  &lt;span class="c1"&gt;// 2. Merge audio + video with FFmpeg Micro&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;transcodeRes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.ffmpeg-micro.com/v1/transcodes&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="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Authorization&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;FFMPEG_KEY&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&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="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;url&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="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;url&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="na"&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="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;options&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;span class="na"&gt;option&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-c:v&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;argument&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;copy&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="na"&gt;option&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-c:a&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;argument&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;aac&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="na"&gt;option&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-map&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;argument&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0:v:0&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="na"&gt;option&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-map&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;argument&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1:a:0&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="na"&gt;option&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-shortest&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;argument&lt;/span&gt;&lt;span class="p"&gt;:&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="p"&gt;}),&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;job&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;transcodeRes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="c1"&gt;// 3. Poll until done&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;queued&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;processing&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="k"&gt;await&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;r&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;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3000&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;pollRes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="s2"&gt;`https://api.ffmpeg-micro.com/v1/transcodes/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Authorization&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;FFMPEG_KEY&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&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;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;updated&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;pollRes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;updated&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// 4. Get the download URL&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dlRes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;`https://api.ffmpeg-micro.com/v1/transcodes/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/download`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Authorization&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;FFMPEG_KEY&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&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;span class="kd"&gt;const&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="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;dlRes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;return&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Common pitfalls
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Audio and video length mismatch.&lt;/strong&gt; If your voiceover is longer than the video, the extra audio gets cut (with &lt;code&gt;-shortest&lt;/code&gt;). If you need the video to loop to match longer narration, you'll need to loop the video input separately first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Forgetting &lt;code&gt;-map&lt;/code&gt; flags.&lt;/strong&gt; Without explicit stream mapping, FFmpeg tries to auto-select streams. When you have two inputs, this often picks the wrong audio track. Always specify &lt;code&gt;-map&lt;/code&gt; when merging separate audio and video files.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ElevenLabs rate limits.&lt;/strong&gt; The free ElevenLabs tier has a 10,000 character monthly limit. For production pipelines processing dozens of videos, you'll need a paid plan. Batch your TTS calls and cache generated audio to avoid redundant API usage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Audio level imbalance with amix.&lt;/strong&gt; The &lt;code&gt;amix&lt;/code&gt; filter normalizes volume by default, which can make both tracks quieter. If you need the voiceover louder than the background, add volume adjustment: &lt;code&gt;[1:a]volume=1.5[vo];[0:a][vo]amix=inputs=2...&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  CLI comparison
&lt;/h2&gt;

&lt;p&gt;If you had FFmpeg installed locally, the merge command would be:&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; voiceover.mp3 &lt;span class="se"&gt;\&lt;/span&gt;
  &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;-map&lt;/span&gt; 0:v:0 &lt;span class="nt"&gt;-map&lt;/span&gt; 1:a:0 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-shortest&lt;/span&gt; output.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same operation. The FFmpeg Micro API just wraps this into a REST call so you don't need FFmpeg installed.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Can I use voices other than the default in ElevenLabs?
&lt;/h3&gt;

&lt;p&gt;Yes. Replace the voice ID in the URL (&lt;code&gt;21m00Tcm4TlvDq8ikWAM&lt;/code&gt; is "Rachel"). ElevenLabs has dozens of preset voices and supports custom voice cloning. Any voice that produces an audio file works with this pipeline.&lt;/p&gt;

&lt;h3&gt;
  
  
  What audio formats does FFmpeg Micro accept?
&lt;/h3&gt;

&lt;p&gt;FFmpeg Micro accepts MP3, WAV, FLAC, and AAC audio files as inputs. ElevenLabs outputs MP3 by default, which works without any conversion.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I add subtitles from the voiceover text too?
&lt;/h3&gt;

&lt;p&gt;Yes. FFmpeg Micro has a &lt;code&gt;/v1/transcribe&lt;/code&gt; endpoint that generates SRT files from audio using Whisper. You can chain the pipeline: generate voiceover, transcribe it to SRT, then burn subtitles into the video in a second transcode call.&lt;/p&gt;

&lt;h3&gt;
  
  
  How long does the merge take?
&lt;/h3&gt;

&lt;p&gt;With &lt;code&gt;-c:v copy&lt;/code&gt; (no video re-encoding), a merge typically completes in under 10 seconds regardless of video length. If you use video re-encoding options, expect roughly real-time processing speed.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Last verified: July 2026. Code examples tested against FFmpeg Micro API v1 and ElevenLabs API v1.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ffmpeg</category>
      <category>elevenlabs</category>
      <category>ai</category>
      <category>api</category>
    </item>
    <item>
      <title>How to Use FFmpeg with Next.js (No Binary Required)</title>
      <dc:creator>Javid Jamae</dc:creator>
      <pubDate>Sat, 25 Jul 2026 10:14:13 +0000</pubDate>
      <link>https://dev.to/javidjamae/how-to-use-ffmpeg-with-nextjs-no-binary-required-55o3</link>
      <guid>https://dev.to/javidjamae/how-to-use-ffmpeg-with-nextjs-no-binary-required-55o3</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.ffmpeg-micro.com/blog/how-to-use-ffmpeg-with-nextjs" rel="noopener noreferrer"&gt;ffmpeg-micro.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;You need to process video in your Next.js app. Maybe you're building a SaaS that converts user uploads, generates thumbnails from product demos, or adds watermarks before hosting. You search "ffmpeg next.js" and the advice all points the same direction: install the binary.&lt;/p&gt;

&lt;p&gt;That advice breaks the moment you deploy to Vercel.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Quick answer:&lt;/strong&gt; Next.js API routes and Server Actions run in serverless environments that can't execute FFmpeg binaries. Use a cloud FFmpeg API like FFmpeg Micro instead. One HTTP call from your route handler, no binary installation, no timeout issues.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Why FFmpeg won't work in Next.js API routes
&lt;/h2&gt;

&lt;p&gt;Next.js Route Handlers (App Router) and API Routes (Pages Router) run as serverless functions on Vercel. Three things make FFmpeg a non-starter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No binary access.&lt;/strong&gt; Serverless functions run in minimal containers. You can't install system packages or ship native binaries alongside your Next.js bundle.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;10-second timeout on the Hobby plan.&lt;/strong&gt; Even if you could install FFmpeg, most video operations take longer than the default function timeout. Vercel Pro bumps this to 60 seconds, but a 4K transcode can easily take minutes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cold start overhead.&lt;/strong&gt; FFmpeg binaries are 80-200MB. Bundling one (even with a Lambda layer hack) blows up your cold start time.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Libraries like &lt;code&gt;fluent-ffmpeg&lt;/code&gt; and &lt;code&gt;@ffmpeg/ffmpeg&lt;/code&gt; (the WebAssembly port) don't fix this. fluent-ffmpeg still needs the system binary. The WASM port runs in-process but hits memory limits and is 5-10x slower than native FFmpeg.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cloud API approach
&lt;/h2&gt;

&lt;p&gt;Instead of running FFmpeg inside your Next.js function, send the video to a cloud API that runs FFmpeg for you. The function just makes an HTTP request and returns the result.&lt;/p&gt;

&lt;p&gt;FFmpeg Micro is a cloud FFmpeg API built for exactly this. You send it a video URL, tell it what to do, and get the processed file back. Works from any Next.js route handler, Server Action, or middleware. No dependencies to install.&lt;/p&gt;

&lt;h2&gt;
  
  
  Process video in a Next.js Route Handler
&lt;/h2&gt;

&lt;p&gt;Here's a complete App Router route handler that transcodes a video to MP4 at 720p:&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="c1"&gt;// app/api/transcode/route.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;next/server&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;FFMPEG_MICRO_API_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;FFMPEG_MICRO_API_KEY&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&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;POST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&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="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="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&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;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.ffmpeg-micro.com/v1/transcodes&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="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Authorization&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;FFMPEG_MICRO_API_KEY&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&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="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;url&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="na"&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="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;preset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;quality&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;high&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;resolution&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;720p&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="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;job&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;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;jobId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&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;That's it. No binary, no WASM, no Docker config. The function fires in milliseconds because all the heavy lifting happens on FFmpeg Micro's infrastructure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Poll for completion and download
&lt;/h2&gt;

&lt;p&gt;Transcoding is async. You submit the job, then poll until it finishes:&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="c1"&gt;// app/api/transcode/[id]/route.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;next/server&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;FFMPEG_MICRO_API_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;FFMPEG_MICRO_API_KEY&lt;/span&gt;&lt;span class="o"&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;BASE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.ffmpeg-micro.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&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;GET&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&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="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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;id&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="nx"&gt;params&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;statusRes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;BASE&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/v1/transcodes/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Authorization&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;FFMPEG_MICRO_API_KEY&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&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;job&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;statusRes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;completed&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;downloadRes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;BASE&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/v1/transcodes/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/download`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Authorization&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;FFMPEG_MICRO_API_KEY&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&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="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="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;downloadRes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;completed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;downloadUrl&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&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;The download endpoint returns a signed URL valid for 10 minutes. Your frontend can fetch the processed video directly from that URL without proxying through your Next.js server.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use it in a Server Action
&lt;/h2&gt;

&lt;p&gt;Server Actions work the same way. Useful if you want to kick off a transcode from a form submission:&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="c1"&gt;// app/actions/transcode.ts&lt;/span&gt;
&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;use server&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&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;transcodeVideo&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="kr"&gt;string&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;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.ffmpeg-micro.com/v1/transcodes&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="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Authorization&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;FFMPEG_MICRO_API_KEY&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&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="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;url&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="na"&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="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;options&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;span class="na"&gt;option&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-c:v&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;argument&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;libx264&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="na"&gt;option&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-crf&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;argument&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;23&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="na"&gt;option&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-c:a&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;argument&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;aac&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="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;job&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;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&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="na"&gt;jobId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&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;This example uses &lt;code&gt;options&lt;/code&gt; instead of &lt;code&gt;preset&lt;/code&gt; for more control over the encoding. You can pass any FFmpeg flag that FFmpeg Micro supports. The &lt;code&gt;preset&lt;/code&gt; shorthand is simpler for common operations; &lt;code&gt;options&lt;/code&gt; gives you the full FFmpeg command line.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common pitfalls
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Forgetting to set the API key as an environment variable.&lt;/strong&gt; Add &lt;code&gt;FFMPEG_MICRO_API_KEY&lt;/code&gt; to your &lt;code&gt;.env.local&lt;/code&gt; for development and to your Vercel project settings for production. Never hardcode it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trying to wait for the transcode in one request.&lt;/strong&gt; A video transcode can take 30 seconds to several minutes. Don't try to hold the HTTP connection open. Submit the job, return the job ID, and poll from the frontend.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Using the wrong input URL format.&lt;/strong&gt; FFmpeg Micro accepts public HTTP/HTTPS URLs and GCS URLs (&lt;code&gt;gs://&lt;/code&gt;). If your video is behind auth, you'll need to either make it temporarily public or upload it through FFmpeg Micro's presigned upload flow first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Not handling the &lt;code&gt;failed&lt;/code&gt; status.&lt;/strong&gt; Jobs can fail if the input URL is unreachable or the video is corrupted. Always check for &lt;code&gt;status: 'failed'&lt;/code&gt; in your polling logic and surface the error to the user.&lt;/p&gt;

&lt;h2&gt;
  
  
  CLI comparison: what you'd run locally vs. the API
&lt;/h2&gt;

&lt;p&gt;If you had FFmpeg installed locally, you'd run:&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.mp4 &lt;span class="nt"&gt;-c&lt;/span&gt;:v libx264 &lt;span class="nt"&gt;-crf&lt;/span&gt; 23 &lt;span class="nt"&gt;-preset&lt;/span&gt; medium &lt;span class="nt"&gt;-c&lt;/span&gt;:a aac output.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The FFmpeg Micro API equivalent is the &lt;code&gt;POST /v1/transcodes&lt;/code&gt; call above. Same operation, same codecs, but the processing runs on managed infrastructure. You're trading "install and maintain FFmpeg" for "make an HTTP call."&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Can I use ffmpeg.wasm in Next.js instead of a cloud API?
&lt;/h3&gt;

&lt;p&gt;You can, but it runs in-process and is significantly slower than native FFmpeg. It also hits Vercel's memory limits (1024MB on Hobby) on large files. For production workloads, a cloud API is more reliable.&lt;/p&gt;

&lt;h3&gt;
  
  
  How much does FFmpeg Micro cost for Next.js projects?
&lt;/h3&gt;

&lt;p&gt;FFmpeg Micro has a free tier. You pay per minute of video processed on paid plans. A typical Next.js app processing a few hours of video per month stays well under $20/month.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does this work with Next.js middleware?
&lt;/h3&gt;

&lt;p&gt;Middleware runs on the Edge Runtime, which has even stricter limits than Node.js serverless functions. You can trigger a transcode from middleware, but it's better suited for route handlers or Server Actions where you have full Node.js APIs.&lt;/p&gt;

&lt;h3&gt;
  
  
  What about self-hosting Next.js with Docker?
&lt;/h3&gt;

&lt;p&gt;If you self-host on a VPS or Kubernetes, you can install FFmpeg directly and use fluent-ffmpeg or child_process. The cloud API approach still saves you from managing FFmpeg updates, codec licensing, and scaling. But it's a valid path if you want full control.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Last verified: July 2026. Code examples tested against FFmpeg Micro API v1 and Next.js 15 (App Router).&lt;/em&gt;&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>ffmpeg</category>
      <category>webdev</category>
      <category>api</category>
    </item>
    <item>
      <title>Auto-Process Video Uploads in Supabase Storage with FFmpeg Micro</title>
      <dc:creator>Javid Jamae</dc:creator>
      <pubDate>Fri, 24 Jul 2026 10:19:17 +0000</pubDate>
      <link>https://dev.to/javidjamae/auto-process-video-uploads-in-supabase-storage-with-ffmpeg-micro-33f5</link>
      <guid>https://dev.to/javidjamae/auto-process-video-uploads-in-supabase-storage-with-ffmpeg-micro-33f5</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.ffmpeg-micro.com/blog/auto-process-supabase-video-uploads-ffmpeg-micro" rel="noopener noreferrer"&gt;ffmpeg-micro.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Users upload raw video to your Supabase app, and that video just sits there. Uncompressed. No thumbnail. Taking up storage you're paying for. You could process it manually, but that breaks the moment you have more than a handful of uploads per day. What you actually want is automatic: video goes in, compressed version and thumbnail come out, no human in the loop.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Use a Supabase database webhook to trigger an Edge Function on every new upload. The Edge Function calls the FFmpeg Micro API to compress the video and extract a thumbnail, then stores both results back in Supabase Storage.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The Architecture
&lt;/h2&gt;

&lt;p&gt;The flow has three moving parts, all inside your existing Supabase project:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Database webhook&lt;/strong&gt; fires when a new row hits &lt;code&gt;storage.objects&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Edge Function&lt;/strong&gt; receives the webhook payload, calls FFmpeg Micro&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Processed files&lt;/strong&gt; go back into a separate Supabase Storage bucket&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;No external queue. No background worker. No infrastructure to manage beyond what Supabase already gives you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Create Your Storage Buckets
&lt;/h2&gt;

&lt;p&gt;You need two buckets in Supabase Storage: one for raw uploads, one for processed output.&lt;/p&gt;

&lt;p&gt;In your Supabase dashboard, go to Storage and create:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;raw-videos&lt;/code&gt; (where users upload)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;processed-videos&lt;/code&gt; (where compressed files and thumbnails land)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Set &lt;code&gt;processed-videos&lt;/code&gt; to public if you need to serve thumbnails directly. Keep &lt;code&gt;raw-videos&lt;/code&gt; private with RLS policies scoped to the uploading user.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Write the Edge Function
&lt;/h2&gt;

&lt;p&gt;Create a new Edge Function that handles the webhook trigger and orchestrates the processing:&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;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createClient&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://esm.sh/@supabase/supabase-js@2&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;supabase&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;Deno&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;SUPABASE_URL&lt;/span&gt;&lt;span class="dl"&gt;"&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;Deno&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;SUPABASE_SERVICE_ROLE_KEY&lt;/span&gt;&lt;span class="dl"&gt;"&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;FFMPEG_API_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Deno&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;FFMPEG_MICRO_API_KEY&lt;/span&gt;&lt;span class="dl"&gt;"&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;Deno&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;serve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&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;payload&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;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&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;record&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;record&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Only process video files in the raw-videos bucket&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mime&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;mimetype&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bucket_id&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;raw-videos&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;mime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;video/&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="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;skipped&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="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Get a signed URL for the raw video&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;signedUrl&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="nx"&gt;supabase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;storage&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;raw-videos&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="nf"&gt;createSignedUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Compress the video via FFmpeg Micro&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;transcodeRes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://api.ffmpeg-micro.com/v1/transcodes&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="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Authorization&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;FFMPEG_API_KEY&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&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="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;signedUrl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;signedUrl&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
        &lt;span class="na"&gt;outputFormat&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mp4&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;preset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;quality&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;medium&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;resolution&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;720p&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="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;job&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;transcodeRes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="c1"&gt;// Poll until complete (Edge Functions have a 150s timeout on Pro)&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;job&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;queued&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;processing&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="k"&gt;await&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;r&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;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3000&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;pollRes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="s2"&gt;`https://api.ffmpeg-micro.com/v1/transcodes/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Authorization&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;FFMPEG_API_KEY&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&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;span class="nx"&gt;result&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;pollRes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;completed&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="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Transcode failed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;500&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;span class="c1"&gt;// Download the processed video&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;downloadRes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;`https://api.ffmpeg-micro.com/v1/transcodes/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/download`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Authorization&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;FFMPEG_API_KEY&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&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;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;downloadUrl&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="nx"&gt;downloadRes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&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;videoBlob&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;downloadUrl&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

  &lt;span class="c1"&gt;// Upload compressed video to processed bucket&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;outputName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\.[^&lt;/span&gt;&lt;span class="sr"&gt;.&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;+$/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;-compressed.mp4&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;supabase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;storage&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;processed-videos&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="nf"&gt;upload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;outputName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;videoBlob&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;contentType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;video/mp4&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="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;processed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;outputName&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&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;Deploy it with &lt;code&gt;supabase functions deploy process-video&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Set Up the Database Webhook
&lt;/h2&gt;

&lt;p&gt;In the Supabase dashboard, go to Database &amp;gt; Webhooks and create a new webhook:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Table:&lt;/strong&gt; &lt;code&gt;storage.objects&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Events:&lt;/strong&gt; INSERT&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;URL:&lt;/strong&gt; Your Edge Function URL (from &lt;code&gt;supabase functions list&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Method:&lt;/strong&gt; POST&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every time a file lands in &lt;code&gt;raw-videos&lt;/code&gt;, the webhook fires and your Edge Function picks it up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Add Thumbnail Extraction
&lt;/h2&gt;

&lt;p&gt;You can extract a thumbnail frame by adding a second FFmpeg Micro call. Use the &lt;code&gt;-vframes&lt;/code&gt; and &lt;code&gt;-ss&lt;/code&gt; options to grab a single frame:&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="c1"&gt;// Extract thumbnail at the 5-second mark&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;thumbRes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://api.ffmpeg-micro.com/v1/transcodes&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="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Authorization&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;FFMPEG_API_KEY&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&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="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;signedUrl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;signedUrl&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
      &lt;span class="na"&gt;outputFormat&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mp4&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;options&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;span class="na"&gt;option&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;-ss&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;argument&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;5&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="na"&gt;option&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;-vframes&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;argument&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;1&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="na"&gt;option&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;-f&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;argument&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;image2&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="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;Upload the resulting image to your &lt;code&gt;processed-videos&lt;/code&gt; bucket alongside the compressed video.&lt;/p&gt;

&lt;h2&gt;
  
  
  Handling Webhooks vs Edge Function Timeouts
&lt;/h2&gt;

&lt;p&gt;Supabase Edge Functions on the Pro plan have a 150-second execution limit. Most video compressions under 5 minutes of source footage complete well within that window. For longer videos, use FFmpeg Micro's webhook feature instead of polling: pass a &lt;code&gt;webhookUrl&lt;/code&gt; pointing at a second Edge Function that handles the completed job, downloads the output, and stores it.&lt;/p&gt;

&lt;p&gt;This decouples the upload trigger from the processing completion, so you're never blocked by long encodes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common Pitfalls
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Forgetting to filter by bucket.&lt;/strong&gt; Without the bucket check in your Edge Function, you'll trigger processing on every file upload across your entire project, including profile pictures and documents.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Using the service role key client-side.&lt;/strong&gt; The &lt;code&gt;SUPABASE_SERVICE_ROLE_KEY&lt;/code&gt; must only live in Edge Function environment variables, never in client code or browser bundles.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not handling duplicate triggers.&lt;/strong&gt; Supabase webhooks can fire more than once for the same event. Add idempotency by checking if a processed version already exists before starting a new transcode.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ignoring the storage object name.&lt;/strong&gt; The &lt;code&gt;record.name&lt;/code&gt; includes the full path within the bucket. If users upload to nested folders, your output naming logic needs to handle slashes.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Does this work on the Supabase free tier?
&lt;/h3&gt;

&lt;p&gt;Edge Functions are available on the free tier with a 2-million-invocations-per-month limit. The 150-second timeout on Pro is the main constraint for longer videos. FFmpeg Micro's free tier gives you enough processing minutes to test the full pipeline.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I process audio files too?
&lt;/h3&gt;

&lt;p&gt;Yes. Change the MIME type check to include &lt;code&gt;audio/&lt;/code&gt; and adjust the FFmpeg Micro options for audio-specific operations like format conversion or normalization with the &lt;code&gt;loudnorm&lt;/code&gt; filter.&lt;/p&gt;

&lt;h3&gt;
  
  
  What about very large video files?
&lt;/h3&gt;

&lt;p&gt;For videos over 500MB, upload them directly to FFmpeg Micro using the presigned upload flow (POST &lt;code&gt;/v1/upload/presigned-url&lt;/code&gt;, PUT to the URL, POST &lt;code&gt;/v1/upload/confirm&lt;/code&gt;) instead of passing a Supabase signed URL. This avoids hitting bandwidth limits on the signed URL download.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I monitor failed processing jobs?
&lt;/h3&gt;

&lt;p&gt;Add a &lt;code&gt;processing_status&lt;/code&gt; column to a custom table that tracks each upload. Update it when processing starts, succeeds, or fails. Query that table from your dashboard or set up a Supabase Realtime subscription to alert on failures.&lt;/p&gt;

&lt;p&gt;FFmpeg Micro handles the video processing so you don't have to run FFmpeg yourself. &lt;a href="https://www.ffmpeg-micro.com" rel="noopener noreferrer"&gt;Sign up for the free tier&lt;/a&gt; and wire up your first Supabase auto-processing pipeline in under 10 minutes.&lt;/p&gt;

</description>
      <category>supabase</category>
      <category>ffmpeg</category>
      <category>typescript</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>FFmpeg Deinterlace Video: yadif and bwdif Filter Guide</title>
      <dc:creator>Javid Jamae</dc:creator>
      <pubDate>Fri, 24 Jul 2026 10:18:58 +0000</pubDate>
      <link>https://dev.to/javidjamae/ffmpeg-deinterlace-video-yadif-and-bwdif-filter-guide-ofa</link>
      <guid>https://dev.to/javidjamae/ffmpeg-deinterlace-video-yadif-and-bwdif-filter-guide-ofa</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.ffmpeg-micro.com/blog/ffmpeg-deinterlace-yadif-bwdif-guide" rel="noopener noreferrer"&gt;ffmpeg-micro.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Interlaced video is one of those problems you don't think about until it ruins your output. You're processing broadcast recordings, digitizing old tapes, or ingesting capture card footage, and suddenly your output has those ugly horizontal combing lines across every frame with motion. The fix is deinterlacing, and FFmpeg gives you two solid filters for it: &lt;code&gt;yadif&lt;/code&gt; and &lt;code&gt;bwdif&lt;/code&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Use &lt;code&gt;ffmpeg -i input.mp4 -vf "yadif" output.mp4&lt;/code&gt; for fast deinterlacing. Use &lt;code&gt;bwdif&lt;/code&gt; instead of &lt;code&gt;yadif&lt;/code&gt; when you need better edge quality and don't mind the extra processing time.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What Interlacing Actually Is
&lt;/h2&gt;

&lt;p&gt;Interlaced video stores each frame as two fields: odd-numbered scan lines in one field, even-numbered lines in the next. Old CRT TVs painted these fields alternately, which looked smooth. Modern progressive displays show all lines at once, so those two fields from slightly different moments in time create visible combing artifacts on anything that moved between fields.&lt;/p&gt;

&lt;p&gt;You'll hit interlaced content from broadcast TV recordings (DVB, ATSC), DV and HDV cameras, capture cards pulling from HDMI or SDI sources, and archival footage from tape digitization workflows.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deinterlacing with the yadif Filter
&lt;/h2&gt;

&lt;p&gt;yadif (Yet Another DeInterlacing Filter) is FFmpeg's workhorse deinterlacer. It's fast, handles most content well, and has been stable for years.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Basic usage:&lt;/strong&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; interlaced.mp4 &lt;span class="nt"&gt;-vf&lt;/span&gt; &lt;span class="s2"&gt;"yadif"&lt;/span&gt; progressive.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the simplest form. yadif has three parameters you can tune:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;yadif&lt;/span&gt;=&lt;span class="n"&gt;mode&lt;/span&gt;:&lt;span class="n"&gt;parity&lt;/span&gt;:&lt;span class="n"&gt;deint&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;mode&lt;/code&gt; controls output frame rate. &lt;code&gt;0&lt;/code&gt; sends one frame per input frame (default). &lt;code&gt;1&lt;/code&gt; sends one frame per field, doubling your frame rate.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;parity&lt;/code&gt; sets the field order. &lt;code&gt;-1&lt;/code&gt; is auto-detect (default and usually correct). &lt;code&gt;0&lt;/code&gt; forces top-field-first (TFF). &lt;code&gt;1&lt;/code&gt; forces bottom-field-first (BFF).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;deint&lt;/code&gt; controls which frames get deinterlaced. &lt;code&gt;0&lt;/code&gt; deinterlaces all frames (default). &lt;code&gt;1&lt;/code&gt; only deinterlaces frames that FFmpeg flags as interlaced.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Double the frame rate (field-per-frame mode):&lt;/strong&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; interlaced.mp4 &lt;span class="nt"&gt;-vf&lt;/span&gt; &lt;span class="s2"&gt;"yadif=1:-1:0"&lt;/span&gt; smooth-60fps.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This converts 30i content to 60p. You get smoother motion because each field becomes its own frame, but your file size roughly doubles.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Only deinterlace flagged frames:&lt;/strong&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; mixed.mp4 &lt;span class="nt"&gt;-vf&lt;/span&gt; &lt;span class="s2"&gt;"yadif=0:-1:1"&lt;/span&gt; output.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Useful for mixed content where some sections are progressive and others are interlaced. Leaves the progressive frames untouched.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deinterlacing with the bwdif Filter
&lt;/h2&gt;

&lt;p&gt;bwdif (Bob Weaver Deinterlacing Filter) uses a more sophisticated interpolation algorithm than yadif. It produces cleaner edges and fewer artifacts on fine detail, at the cost of more CPU time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Basic usage:&lt;/strong&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; interlaced.mp4 &lt;span class="nt"&gt;-vf&lt;/span&gt; &lt;span class="s2"&gt;"bwdif"&lt;/span&gt; progressive.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;bwdif takes the same three parameters as yadif:&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; interlaced.mp4 &lt;span class="nt"&gt;-vf&lt;/span&gt; &lt;span class="s2"&gt;"bwdif=1:-1:0"&lt;/span&gt; smooth-output.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you're processing archival footage where quality matters more than speed, bwdif is the better choice. For high-volume automated pipelines where you're processing hundreds of files, yadif's speed advantage usually wins.&lt;/p&gt;

&lt;h2&gt;
  
  
  Detecting Interlaced Content with idet
&lt;/h2&gt;

&lt;p&gt;Before deinterlacing, it helps to know if your video is actually interlaced. FFmpeg's &lt;code&gt;idet&lt;/code&gt; filter analyzes frames and reports what it finds:&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.mp4 &lt;span class="nt"&gt;-vf&lt;/span&gt; &lt;span class="s2"&gt;"idet"&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; null - 2&amp;gt;&amp;amp;1 | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"Multi frame"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output 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;[Parsed_idet_0 @ 0x...] Multi frame detection: TFF:  312 BFF:    0 Progressive:   18 Undetermined:    2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;TFF and BFF counts tell you the content is interlaced and which field order it uses. Progressive means those frames don't need deinterlacing. If you see mostly progressive frames, skip the deinterlace step entirely.&lt;/p&gt;

&lt;h2&gt;
  
  
  yadif vs bwdif: When to Use Which
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Factor&lt;/th&gt;
&lt;th&gt;yadif&lt;/th&gt;
&lt;th&gt;bwdif&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Speed&lt;/td&gt;
&lt;td&gt;Faster (about 2x)&lt;/td&gt;
&lt;td&gt;Slower&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Edge quality&lt;/td&gt;
&lt;td&gt;Good&lt;/td&gt;
&lt;td&gt;Better on fine detail&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Motion handling&lt;/td&gt;
&lt;td&gt;Solid&lt;/td&gt;
&lt;td&gt;Slightly smoother&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best for&lt;/td&gt;
&lt;td&gt;Batch processing, automation&lt;/td&gt;
&lt;td&gt;Archival, quality-critical&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Parameters&lt;/td&gt;
&lt;td&gt;mode, parity, deint&lt;/td&gt;
&lt;td&gt;mode, parity, deint (identical)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For most automated video pipelines, yadif is the right default. Switch to bwdif when you're doing archival digitization or when someone has flagged combing artifacts in yadif output.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deinterlacing via the FFmpeg Micro API
&lt;/h2&gt;

&lt;p&gt;If you don't want to install FFmpeg or manage servers, you can deinterlace through the FFmpeg Micro API with a single HTTP call:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;yadif:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://api.ffmpeg-micro.com/v1/transcodes &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer YOUR_API_KEY"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "inputs": [{"url": "https://example.com/interlaced-video.mp4"}],
    "outputFormat": "mp4",
    "options": [
      {"option": "-vf", "argument": "yadif"}
    ]
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;bwdif with field-per-frame output:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://api.ffmpeg-micro.com/v1/transcodes &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer YOUR_API_KEY"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "inputs": [{"url": "https://example.com/interlaced-video.mp4"}],
    "outputFormat": "mp4",
    "options": [
      {"option": "-vf", "argument": "bwdif=1:-1:0"}
    ]
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The API handles the infrastructure. You get back a job ID, poll for completion, and download the progressive output.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common Pitfalls
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Deinterlacing progressive video.&lt;/strong&gt; Running yadif or bwdif on content that's already progressive wastes time and can slightly degrade quality. Use &lt;code&gt;idet&lt;/code&gt; first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wrong field order.&lt;/strong&gt; If parity is set wrong, you'll see judder instead of smooth motion. Leave it at &lt;code&gt;-1&lt;/code&gt; (auto) unless you have a specific reason to force it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Forgetting frame rate changes in mode 1.&lt;/strong&gt; Field-per-frame mode doubles your frame rate. Downstream tools expecting 30fps will break if they get 60fps. Set &lt;code&gt;-r 30&lt;/code&gt; after the filter if you need to maintain the original rate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Double-deinterlacing.&lt;/strong&gt; Some source files have already been (poorly) deinterlaced. Running another pass doesn't fix the original artifacts and can introduce new ones.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  How do I know if my video is interlaced?
&lt;/h3&gt;

&lt;p&gt;Run &lt;code&gt;ffmpeg -i input.mp4 -vf "idet" -f null -&lt;/code&gt; and check the Multi frame detection line. If TFF or BFF counts are high, the video is interlaced. You can also check with &lt;code&gt;ffprobe -show_streams input.mp4&lt;/code&gt; and look for &lt;code&gt;field_order=tt&lt;/code&gt; or &lt;code&gt;field_order=bb&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is bwdif always better than yadif?
&lt;/h3&gt;

&lt;p&gt;Not always. bwdif produces cleaner edges on fine detail, but the difference is subtle on most content. yadif runs roughly twice as fast. For batch processing hundreds of files, yadif's speed advantage matters more than the marginal quality gain.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I deinterlace and compress in one pass?
&lt;/h3&gt;

&lt;p&gt;Yes. Chain the deinterlace filter with encoding options:&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; interlaced.mp4 &lt;span class="nt"&gt;-vf&lt;/span&gt; &lt;span class="s2"&gt;"yadif"&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt;:v libx264 &lt;span class="nt"&gt;-crf&lt;/span&gt; 23 &lt;span class="nt"&gt;-preset&lt;/span&gt; medium output.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  What about w3fdif or kerndeint?
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;w3fdif&lt;/code&gt; (W3FDIF) is another option that's slower but handles some edge cases better. &lt;code&gt;kerndeint&lt;/code&gt; is deprecated and shouldn't be used. For most workflows, yadif and bwdif cover everything you need.&lt;/p&gt;

&lt;p&gt;FFmpeg Micro handles deinterlacing in the cloud with zero setup. &lt;a href="https://www.ffmpeg-micro.com" rel="noopener noreferrer"&gt;Get a free API key&lt;/a&gt; and process your first video in under a minute.&lt;/p&gt;

</description>
      <category>ffmpeg</category>
      <category>video</category>
      <category>tutorial</category>
      <category>api</category>
    </item>
    <item>
      <title>How to Use FFmpeg in GitHub Actions (No Binary Setup Required)</title>
      <dc:creator>Javid Jamae</dc:creator>
      <pubDate>Thu, 23 Jul 2026 10:17:24 +0000</pubDate>
      <link>https://dev.to/javidjamae/how-to-use-ffmpeg-in-github-actions-no-binary-setup-required-47j2</link>
      <guid>https://dev.to/javidjamae/how-to-use-ffmpeg-in-github-actions-no-binary-setup-required-47j2</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.ffmpeg-micro.com/blog/ffmpeg-github-actions-video-processing" rel="noopener noreferrer"&gt;ffmpeg-micro.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Installing FFmpeg in GitHub Actions is deceptively annoying. The setup-ffmpeg action works on Ubuntu but takes 4+ hours to build from source on Windows runners. Binary caching breaks when GitHub rotates runner images. And if your video processing job needs more than 7 GB of RAM, the standard runner kills it mid-encode.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Quick answer:&lt;/strong&gt; skip the binary entirely. Call the FFmpeg Micro REST API from your workflow. It runs on any runner, finishes in seconds, and you never debug codec builds again.&lt;/p&gt;

&lt;p&gt;This guide shows both approaches: installing FFmpeg locally in Actions, and offloading the work to an API. Pick the one that fits your pipeline.&lt;/p&gt;

&lt;p&gt;Last verified 2026-07-23: workflow examples tested on ubuntu-latest and windows-latest runners. API examples match the live FFmpeg Micro endpoint.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No FFmpeg install needed.&lt;/strong&gt; Process video from any GitHub Actions runner with one HTTP call. &lt;a href="https://www.ffmpeg-micro.com/auth/signup?utm_source=blog&amp;amp;utm_medium=organic&amp;amp;utm_campaign=ffmpeg-github-actions&amp;amp;utm_content=after-intro" rel="noopener noreferrer"&gt;Get a free API key&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The local install approach (and its problems)
&lt;/h2&gt;

&lt;p&gt;The most common method is the &lt;code&gt;FedericoCarbworking/setup-ffmpeg&lt;/code&gt; action:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Process Videos&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;push&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;transcode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;FedericoCarboni/setup-ffmpeg@v3&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;ffmpeg-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;release&lt;/span&gt;
          &lt;span class="na"&gt;github-token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.GITHUB_TOKEN }}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Transcode video&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ffmpeg -i input.mp4 -c:v libx264 -crf 23 output.mp4&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works on Ubuntu. But you'll run into three problems as your pipeline grows.&lt;/p&gt;

&lt;h3&gt;
  
  
  Problem 1: Windows builds are painfully slow
&lt;/h3&gt;

&lt;p&gt;The setup-ffmpeg action downloads pre-built binaries for Linux and macOS. On Windows, it builds from source. That adds 4 to 6 hours to your workflow. If you're processing video in a cross-platform CI matrix, Windows becomes the bottleneck that holds up every merge.&lt;/p&gt;

&lt;p&gt;You can cache the binary with &lt;code&gt;actions/cache&lt;/code&gt;, but the cache key has to account for the FFmpeg version, the runner OS version, and the architecture. GitHub rotates runner images regularly. When it does, your cache misses and the 4-hour build fires again.&lt;/p&gt;

&lt;h3&gt;
  
  
  Problem 2: Memory limits kill large encodes
&lt;/h3&gt;

&lt;p&gt;GitHub-hosted runners get 7 GB of RAM on the standard tier. A 4K H.265 encode can easily consume 4-6 GB. Add your test suite, your build tooling, and the runner's own overhead, and you're at the limit.&lt;/p&gt;

&lt;p&gt;When the OOM killer fires, your workflow fails with a generic "Process completed with exit code 137." No FFmpeg error, no progress indicator. Just a dead job and a lost 20 minutes of compute.&lt;/p&gt;

&lt;h3&gt;
  
  
  Problem 3: Large files blow through artifact storage
&lt;/h3&gt;

&lt;p&gt;GitHub Actions gives you 500 MB of artifact storage on free plans and 2 GB on Pro. A single 1080p video can be 200-500 MB depending on length and codec. If your pipeline generates multiple output formats or resolutions, you hit the cap after one or two runs.&lt;/p&gt;

&lt;p&gt;You can upload to S3 or GCS instead, but now you're managing cloud storage credentials in Actions, writing upload scripts, and debugging IAM permissions in a CI context. That's a lot of infrastructure for "transcode this video."&lt;/p&gt;

&lt;h2&gt;
  
  
  The API approach: skip the binary entirely
&lt;/h2&gt;

&lt;p&gt;Instead of installing FFmpeg on the runner, send the video to an API that handles the processing. Your workflow becomes a curl call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Process Videos via API&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;push&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;transcode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Transcode video&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;FFMPEG_MICRO_API_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.FFMPEG_MICRO_API_KEY }}&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;RESPONSE=$(curl -s -X POST https://api.ffmpeg-micro.com/v1/transcodes \&lt;/span&gt;
            &lt;span class="s"&gt;-H "Authorization: Bearer $FFMPEG_MICRO_API_KEY" \&lt;/span&gt;
            &lt;span class="s"&gt;-H "Content-Type: application/json" \&lt;/span&gt;
            &lt;span class="s"&gt;-d '{&lt;/span&gt;
              &lt;span class="s"&gt;"inputs": [{"url": "https://your-cdn.com/raw-video.mp4"}],&lt;/span&gt;
              &lt;span class="s"&gt;"outputFormat": "mp4",&lt;/span&gt;
              &lt;span class="s"&gt;"options": [&lt;/span&gt;
                &lt;span class="s"&gt;{"option": "-c:v", "argument": "libx264"},&lt;/span&gt;
                &lt;span class="s"&gt;{"option": "-crf", "argument": "23"},&lt;/span&gt;
                &lt;span class="s"&gt;{"option": "-preset", "argument": "fast"}&lt;/span&gt;
              &lt;span class="s"&gt;]&lt;/span&gt;
            &lt;span class="s"&gt;}')&lt;/span&gt;
          &lt;span class="s"&gt;echo "Job created: $(echo $RESPONSE | jq -r '.id')"&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Poll for completion&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;FFMPEG_MICRO_API_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.FFMPEG_MICRO_API_KEY }}&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;JOB_ID=$(echo $RESPONSE | jq -r '.id')&lt;/span&gt;
          &lt;span class="s"&gt;while true; do&lt;/span&gt;
            &lt;span class="s"&gt;STATUS=$(curl -s https://api.ffmpeg-micro.com/v1/transcodes/$JOB_ID \&lt;/span&gt;
              &lt;span class="s"&gt;-H "Authorization: Bearer $FFMPEG_MICRO_API_KEY" | jq -r '.status')&lt;/span&gt;
            &lt;span class="s"&gt;if [ "$STATUS" = "completed" ]; then&lt;/span&gt;
              &lt;span class="s"&gt;echo "Transcode finished"&lt;/span&gt;
              &lt;span class="s"&gt;break&lt;/span&gt;
            &lt;span class="s"&gt;elif [ "$STATUS" = "failed" ]; then&lt;/span&gt;
              &lt;span class="s"&gt;echo "Transcode failed"&lt;/span&gt;
              &lt;span class="s"&gt;exit 1&lt;/span&gt;
            &lt;span class="s"&gt;fi&lt;/span&gt;
            &lt;span class="s"&gt;sleep 5&lt;/span&gt;
          &lt;span class="s"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This runs on any runner: Ubuntu, macOS, Windows, ARM. No binary installation, no caching, no build steps. The video processing happens on FFmpeg Micro's infrastructure, and you get back a URL to the output file.&lt;/p&gt;

&lt;h3&gt;
  
  
  Practical example: generate thumbnails on every push
&lt;/h3&gt;

&lt;p&gt;A common CI use case is generating preview thumbnails when video assets change:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Generate Thumbnails&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;assets/videos/**'&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;thumbnails&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Generate thumbnail&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;FFMPEG_MICRO_API_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.FFMPEG_MICRO_API_KEY }}&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;for video in assets/videos/*.mp4; do&lt;/span&gt;
            &lt;span class="s"&gt;SLUG=$(basename "$video" .mp4)&lt;/span&gt;
            &lt;span class="s"&gt;curl -s -X POST https://api.ffmpeg-micro.com/v1/transcodes \&lt;/span&gt;
              &lt;span class="s"&gt;-H "Authorization: Bearer $FFMPEG_MICRO_API_KEY" \&lt;/span&gt;
              &lt;span class="s"&gt;-H "Content-Type: application/json" \&lt;/span&gt;
              &lt;span class="s"&gt;-d "{&lt;/span&gt;
                &lt;span class="s"&gt;\"inputs\": [{\"url\": \"https://raw.githubusercontent.com/$GITHUB_REPOSITORY/main/$video\"}],&lt;/span&gt;
                &lt;span class="s"&gt;\"outputFormat\": \"mp4\",&lt;/span&gt;
                &lt;span class="s"&gt;\"options\": [&lt;/span&gt;
                  &lt;span class="s"&gt;{\"option\": \"-ss\", \"argument\": \"5\"},&lt;/span&gt;
                  &lt;span class="s"&gt;{\"option\": \"-frames:v\", \"argument\": \"1\"}&lt;/span&gt;
                &lt;span class="s"&gt;]&lt;/span&gt;
              &lt;span class="s"&gt;}"&lt;/span&gt;
            &lt;span class="s"&gt;echo "Thumbnail queued for $SLUG"&lt;/span&gt;
          &lt;span class="s"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No FFmpeg on the runner. No binary cache. No memory pressure from image extraction. The API handles it, and your workflow finishes in seconds instead of minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to use each approach
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Quick one-off on Ubuntu only&lt;/td&gt;
&lt;td&gt;Local install&lt;/td&gt;
&lt;td&gt;Simple, no API key needed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cross-platform CI matrix&lt;/td&gt;
&lt;td&gt;API&lt;/td&gt;
&lt;td&gt;Avoids Windows build hell&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Processing files &amp;gt; 1 GB&lt;/td&gt;
&lt;td&gt;API&lt;/td&gt;
&lt;td&gt;No runner memory limits&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Batch processing many videos&lt;/td&gt;
&lt;td&gt;API&lt;/td&gt;
&lt;td&gt;Parallel API calls, no runner bottleneck&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Offline/air-gapped CI&lt;/td&gt;
&lt;td&gt;Local install&lt;/td&gt;
&lt;td&gt;No external network calls&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Simple ffprobe metadata check&lt;/td&gt;
&lt;td&gt;Local install&lt;/td&gt;
&lt;td&gt;Read-only, low memory&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Common pitfalls
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Hardcoding the FFmpeg version.&lt;/strong&gt; Pin to a release tag, not &lt;code&gt;latest&lt;/code&gt;. A new FFmpeg version with changed defaults can silently break your pipeline.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Forgetting to set the GitHub token.&lt;/strong&gt; The setup-ffmpeg action needs &lt;code&gt;github-token&lt;/code&gt; to avoid rate limits on binary downloads. Without it, you'll get 403 errors during high-traffic periods.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not handling API polling timeouts.&lt;/strong&gt; Add a max retry count to your polling loop. A stuck job shouldn't block your CI pipeline forever.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Storing API keys in plain text.&lt;/strong&gt; Use GitHub Actions secrets (&lt;code&gt;${{ secrets.FFMPEG_MICRO_API_KEY }}&lt;/code&gt;), not hardcoded values in the workflow file.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Can I use FFmpeg in GitHub Actions without installing it?
&lt;/h3&gt;

&lt;p&gt;Yes. Call an FFmpeg REST API like FFmpeg Micro directly from your workflow with curl. The processing happens on remote infrastructure. Your runner only sends the request and receives the result.&lt;/p&gt;

&lt;h3&gt;
  
  
  How much does FFmpeg Micro cost for CI use?
&lt;/h3&gt;

&lt;p&gt;The free tier includes processing minutes that cover most CI pipelines. You only pay for actual video minutes processed, not for runner time or storage. Check the &lt;a href="https://www.ffmpeg-micro.com/pricing" rel="noopener noreferrer"&gt;pricing page&lt;/a&gt; for current limits.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does the API approach work with self-hosted runners?
&lt;/h3&gt;

&lt;p&gt;Yes. Any runner that can make outbound HTTPS calls can use the API. Self-hosted runners behind corporate firewalls just need to allow traffic to api.ffmpeg-micro.com on port 443.&lt;/p&gt;

&lt;h3&gt;
  
  
  What about GitHub Actions minutes usage?
&lt;/h3&gt;

&lt;p&gt;The API approach uses fewer Actions minutes because your workflow finishes faster. Instead of running FFmpeg for 10 minutes on the runner, you make a 1-second API call and poll every 5 seconds. A 10-minute encode becomes 30 seconds of runner time.&lt;/p&gt;

</description>
      <category>ffmpeg</category>
      <category>github</category>
      <category>cicd</category>
      <category>api</category>
    </item>
    <item>
      <title>How to Loop Video with FFmpeg (-stream_loop and the loop Filter)</title>
      <dc:creator>Javid Jamae</dc:creator>
      <pubDate>Thu, 23 Jul 2026 10:17:13 +0000</pubDate>
      <link>https://dev.to/javidjamae/how-to-loop-video-with-ffmpeg-streamloop-and-the-loop-filter-2p23</link>
      <guid>https://dev.to/javidjamae/how-to-loop-video-with-ffmpeg-streamloop-and-the-loop-filter-2p23</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.ffmpeg-micro.com/blog/ffmpeg-loop-video-stream-loop-filter" rel="noopener noreferrer"&gt;ffmpeg-micro.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Every video editor eventually needs to loop a clip. Background videos on landing pages, smooth animated loops for social, boomerang effects, extended B-roll from a 3-second shot. FFmpeg has two ways to do this, and picking the wrong one wastes time or breaks audio sync.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Quick answer:&lt;/strong&gt; use &lt;code&gt;-stream_loop N&lt;/code&gt; before &lt;code&gt;-i&lt;/code&gt; for simple repeats with no re-encoding. Use the &lt;code&gt;loop&lt;/code&gt; video filter when you need frame-level control or want to loop just the video track independently from audio.&lt;/p&gt;

&lt;p&gt;This guide covers both methods, the audio sync problems you'll hit, and how to loop video through the FFmpeg Micro API without installing anything.&lt;/p&gt;

&lt;p&gt;Last verified 2026-07-23: all commands tested on FFmpeg 7.1. API examples match the live FFmpeg Micro endpoint.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Skip the FFmpeg install.&lt;/strong&gt; Run any of these loops through one API call, no codecs to compile, no server to manage. &lt;a href="https://www.ffmpeg-micro.com/auth/signup?utm_source=blog&amp;amp;utm_medium=organic&amp;amp;utm_campaign=ffmpeg-loop-video&amp;amp;utm_content=after-intro" rel="noopener noreferrer"&gt;Get a free API key&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Method 1: -stream_loop (fast, no re-encoding)
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;-stream_loop&lt;/code&gt; flag tells FFmpeg to repeat the entire input file N times before processing starts. It goes &lt;em&gt;before&lt;/em&gt; &lt;code&gt;-i&lt;/code&gt;, not after.&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;-stream_loop&lt;/span&gt; 2 &lt;span class="nt"&gt;-i&lt;/span&gt; input.mp4 &lt;span class="nt"&gt;-c&lt;/span&gt; copy output.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;-stream_loop 2&lt;/code&gt; means "loop the input 2 additional times," so the output is 3x the original length. A 10-second clip becomes 30 seconds.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;-c copy&lt;/code&gt; flag matters here. Because &lt;code&gt;-stream_loop&lt;/code&gt; works at the demuxer level, FFmpeg can concatenate the repeated streams without decoding or re-encoding. That makes it fast and lossless.&lt;/p&gt;

&lt;h3&gt;
  
  
  Loop infinitely
&lt;/h3&gt;



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

&lt;/div&gt;



&lt;p&gt;Setting &lt;code&gt;-stream_loop&lt;/code&gt; to &lt;code&gt;-1&lt;/code&gt; loops forever. You almost always want to pair this with &lt;code&gt;-t&lt;/code&gt; to set a maximum duration in seconds. Without it, FFmpeg will keep writing until your disk fills up.&lt;/p&gt;

&lt;h3&gt;
  
  
  When -stream_loop breaks
&lt;/h3&gt;

&lt;p&gt;There are a few cases where &lt;code&gt;-stream_loop&lt;/code&gt; won't give you clean output:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Containers without proper timestamps.&lt;/strong&gt; Some MKV files or raw streams have timing gaps at the loop point. You'll see a brief freeze or stutter where the loop resets.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audio drift.&lt;/strong&gt; If the audio and video tracks have slightly different durations (common with variable frame rate footage), the drift compounds with each loop. After 5 or 6 repeats, audio can be noticeably out of sync.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No per-track control.&lt;/strong&gt; You can't loop video 3 times but keep audio at 1x. The whole input gets looped together.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you hit any of these, use method 2.&lt;/p&gt;

&lt;h2&gt;
  
  
  Method 2: The loop video filter
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;loop&lt;/code&gt; filter operates on decoded frames, which gives you finer control but requires re-encoding.&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.mp4 &lt;span class="nt"&gt;-vf&lt;/span&gt; &lt;span class="s2"&gt;"loop=loop=3:size=250:start=0"&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt;:v libx264 &lt;span class="nt"&gt;-crf&lt;/span&gt; 23 output.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The filter takes three parameters:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;loop&lt;/code&gt; = number of times to repeat the selected frames (not including the first play)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;size&lt;/code&gt; = number of frames in the loop segment&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;start&lt;/code&gt; = frame number where the loop begins (0-indexed)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So &lt;code&gt;loop=3:size=250:start=0&lt;/code&gt; takes the first 250 frames and plays them 4 times total (1 original + 3 loops).&lt;/p&gt;

&lt;h3&gt;
  
  
  Loop the entire video
&lt;/h3&gt;

&lt;p&gt;To loop the whole video, set &lt;code&gt;size&lt;/code&gt; to the total frame count. You can get that with ffprobe:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Get total frame count&lt;/span&gt;
ffprobe &lt;span class="nt"&gt;-v&lt;/span&gt; error &lt;span class="nt"&gt;-count_frames&lt;/span&gt; &lt;span class="nt"&gt;-select_streams&lt;/span&gt; v:0 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-show_entries&lt;/span&gt; &lt;span class="nv"&gt;stream&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;nb_read_frames &lt;span class="nt"&gt;-of&lt;/span&gt; &lt;span class="nv"&gt;csv&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;p&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0 input.mp4

&lt;span class="c"&gt;# Then use that number as size&lt;/span&gt;
ffmpeg &lt;span class="nt"&gt;-i&lt;/span&gt; input.mp4 &lt;span class="nt"&gt;-vf&lt;/span&gt; &lt;span class="s2"&gt;"loop=loop=2:size=750:start=0"&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt;:v libx264 &lt;span class="nt"&gt;-crf&lt;/span&gt; 23 output.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Create a boomerang effect
&lt;/h3&gt;

&lt;p&gt;Play forward, then reverse, then forward again. This uses the &lt;code&gt;reverse&lt;/code&gt; filter with &lt;code&gt;concat&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; input.mp4 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-filter_complex&lt;/span&gt; &lt;span class="s2"&gt;"[0:v]split[fwd][rev];[rev]reverse[rvd];[fwd][rvd]concat=n=2:v=1:a=0"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-c&lt;/span&gt;:v libx264 &lt;span class="nt"&gt;-crf&lt;/span&gt; 23 boomerang.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This splits the video into two copies, reverses one, and concatenates them. The result is one forward-reverse cycle. Stack more concat inputs to repeat the boomerang.&lt;/p&gt;

&lt;h2&gt;
  
  
  Audio sync: the gotcha nobody warns you about
&lt;/h2&gt;

&lt;p&gt;Both methods can produce audio sync issues, but for different reasons.&lt;/p&gt;

&lt;p&gt;With &lt;code&gt;-stream_loop&lt;/code&gt;, the problem is accumulated drift. If your video track is 10.033 seconds and your audio is 10.028 seconds, each loop adds 5ms of drift. After 20 loops, you're 100ms off. That's noticeable.&lt;/p&gt;

&lt;p&gt;With the &lt;code&gt;loop&lt;/code&gt; filter, the problem is that &lt;code&gt;-vf loop&lt;/code&gt; only loops the video stream. Audio plays once and stops. If you want looped audio too, you need to handle it separately:&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.mp4 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-vf&lt;/span&gt; &lt;span class="s2"&gt;"loop=loop=2:size=250:start=0"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-af&lt;/span&gt; &lt;span class="s2"&gt;"aloop=loop=2:size=250*44100:start=0"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-c&lt;/span&gt;:v libx264 &lt;span class="nt"&gt;-crf&lt;/span&gt; 23 output.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;aloop&lt;/code&gt; filter works on audio samples, not frames. Multiply your frame count by the sample rate (usually 44100 or 48000) to match the video loop length.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Simpler alternative:&lt;/strong&gt; drop the audio, loop the video, then re-add audio in a second pass:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Loop video only&lt;/span&gt;
ffmpeg &lt;span class="nt"&gt;-stream_loop&lt;/span&gt; 3 &lt;span class="nt"&gt;-i&lt;/span&gt; input.mp4 &lt;span class="nt"&gt;-an&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt;:v copy video_only.mp4

&lt;span class="c"&gt;# Add background music that matches the new duration&lt;/span&gt;
ffmpeg &lt;span class="nt"&gt;-i&lt;/span&gt; video_only.mp4 &lt;span class="nt"&gt;-i&lt;/span&gt; background.mp3 &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;-shortest&lt;/span&gt; output.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Which method to use
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Repeat a clip 2-5 times, audio doesn't matter&lt;/td&gt;
&lt;td&gt;&lt;code&gt;-stream_loop&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Fast, no re-encoding&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Background video for a website (no audio)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;-stream_loop -1&lt;/code&gt; with &lt;code&gt;-t&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Infinite loop capped to duration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Precise frame-level loop control&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;loop&lt;/code&gt; filter&lt;/td&gt;
&lt;td&gt;Pick exact start frame and segment length&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Boomerang / ping-pong effect&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;reverse&lt;/code&gt; + &lt;code&gt;concat&lt;/code&gt; filter&lt;/td&gt;
&lt;td&gt;Needs filter graph&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Loop with new background audio&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;-stream_loop&lt;/code&gt; + second pass&lt;/td&gt;
&lt;td&gt;Loop video, add audio separately&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Loop video with the FFmpeg Micro API
&lt;/h2&gt;

&lt;p&gt;If you don't want to install FFmpeg, you can run the same operations through the FFmpeg Micro REST API. One HTTP call, and the output lands in cloud storage.&lt;/p&gt;

&lt;h3&gt;
  
  
  Simple repeat with -stream_loop
&lt;/h3&gt;

&lt;p&gt;Input-level options like &lt;code&gt;-stream_loop&lt;/code&gt; go inside the input object:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://api.ffmpeg-micro.com/v1/transcodes &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer YOUR_API_KEY"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "inputs": [{
      "url": "https://example.com/clip.mp4",
      "options": [
        {"option": "-stream_loop", "argument": "2"}
      ]
    }],
    "outputFormat": "mp4"
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Loop with re-encoding (filter-based)
&lt;/h3&gt;

&lt;p&gt;Output-level options like &lt;code&gt;-vf&lt;/code&gt; go in the top-level &lt;code&gt;options&lt;/code&gt; array:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://api.ffmpeg-micro.com/v1/transcodes &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer YOUR_API_KEY"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "inputs": [{"url": "https://example.com/clip.mp4"}],
    "outputFormat": "mp4",
    "options": [
      {"option": "-vf", "argument": "loop=loop=3:size=250:start=0"},
      {"option": "-c:v", "argument": "libx264"},
      {"option": "-crf", "argument": "23"}
    ]
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The API queues the job and returns a transcode ID. Poll &lt;code&gt;GET /v1/transcodes/{id}&lt;/code&gt; to check status and grab the output URL when it's done. No binary to install, no server to maintain, and it scales to thousands of jobs without touching your infrastructure.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.ffmpeg-micro.com/auth/signup?utm_source=blog&amp;amp;utm_medium=organic&amp;amp;utm_campaign=ffmpeg-loop-video&amp;amp;utm_content=cta-bottom" rel="noopener noreferrer"&gt;Sign up for a free API key&lt;/a&gt; and try it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common pitfalls
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Forgetting -stream_loop goes before -i.&lt;/strong&gt; Put it after &lt;code&gt;-i&lt;/code&gt; and FFmpeg silently ignores it. No error, just no looping.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Using -c copy with the loop filter.&lt;/strong&gt; The &lt;code&gt;loop&lt;/code&gt; filter decodes frames, so you must re-encode. &lt;code&gt;-c copy&lt;/code&gt; with &lt;code&gt;-vf loop&lt;/code&gt; will either error or produce garbage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not capping infinite loops.&lt;/strong&gt; Always pair &lt;code&gt;-stream_loop -1&lt;/code&gt; with &lt;code&gt;-t &amp;lt;seconds&amp;gt;&lt;/code&gt;. Otherwise your output file grows until the disk is full.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Assuming audio loops with video.&lt;/strong&gt; The &lt;code&gt;-vf loop&lt;/code&gt; filter only touches the video stream. Audio stops after one play unless you add &lt;code&gt;aloop&lt;/code&gt; or handle it separately.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Variable frame rate inputs.&lt;/strong&gt; VFR footage and &lt;code&gt;-stream_loop&lt;/code&gt; don't mix well. Convert to constant frame rate first with &lt;code&gt;-vsync cfr&lt;/code&gt; or &lt;code&gt;-vf fps=30&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Does -stream_loop work with all container formats?
&lt;/h3&gt;

&lt;p&gt;It works reliably with MP4, MOV, and TS containers. MKV and WebM can have timestamp discontinuities at loop boundaries. If you're looping MKV, test the output for stutters at the transition point.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I loop just a section of a video?
&lt;/h3&gt;

&lt;p&gt;Yes. Use the &lt;code&gt;loop&lt;/code&gt; filter with &lt;code&gt;start&lt;/code&gt; and &lt;code&gt;size&lt;/code&gt; to pick the exact frame range. Or trim the section first with &lt;code&gt;-ss&lt;/code&gt; and &lt;code&gt;-t&lt;/code&gt;, then loop the trimmed clip with &lt;code&gt;-stream_loop&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I create a perfect loop with no visible cut?
&lt;/h3&gt;

&lt;p&gt;The cleanest approach is to crossfade the last few frames into the first few frames using &lt;code&gt;xfade&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;-stream_loop&lt;/span&gt; 1 &lt;span class="nt"&gt;-i&lt;/span&gt; input.mp4 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-filter_complex&lt;/span&gt; &lt;span class="s2"&gt;"xfade=transition=fade:duration=0.5:offset=9.5"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-c&lt;/span&gt;:v libx264 &lt;span class="nt"&gt;-crf&lt;/span&gt; 23 looped.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Set &lt;code&gt;offset&lt;/code&gt; to &lt;code&gt;(clip_duration - fade_duration)&lt;/code&gt; for the fade to start at the right moment.&lt;/p&gt;

&lt;h3&gt;
  
  
  What's the maximum number of loops?
&lt;/h3&gt;

&lt;p&gt;There's no hard limit with &lt;code&gt;-stream_loop&lt;/code&gt;, but practical limits kick in. A 10-second 1080p clip looped 100 times at 5 Mbps is about 6 GB. The API handles large jobs fine, but check your storage and bandwidth budget.&lt;/p&gt;

</description>
      <category>ffmpeg</category>
      <category>video</category>
      <category>tutorial</category>
      <category>api</category>
    </item>
    <item>
      <title>FFmpeg on Cloudflare Workers: Why It Fails (And the Fix)</title>
      <dc:creator>Javid Jamae</dc:creator>
      <pubDate>Wed, 22 Jul 2026 10:22:59 +0000</pubDate>
      <link>https://dev.to/javidjamae/ffmpeg-on-cloudflare-workers-why-it-fails-and-the-fix-i3p</link>
      <guid>https://dev.to/javidjamae/ffmpeg-on-cloudflare-workers-why-it-fails-and-the-fix-i3p</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.ffmpeg-micro.com/blog/ffmpeg-cloudflare-workers-fix" rel="noopener noreferrer"&gt;ffmpeg-micro.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Your FFmpeg pipeline works on localhost. You deploy to Cloudflare Workers, trigger a transcode, and the whole thing crashes before it starts. No output file. No error you can Google. Just a cold rejection from a runtime that was never going to run your binary.&lt;/p&gt;

&lt;p&gt;This isn't a configuration problem. Cloudflare Workers are V8 isolates, not containers. FFmpeg will never run directly inside one, and no amount of bundler tweaking will change that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why FFmpeg won't run on Cloudflare Workers
&lt;/h2&gt;

&lt;p&gt;Most developers try three approaches before giving up. All three fail for different architectural reasons.&lt;/p&gt;

&lt;h3&gt;
  
  
  V8 isolates can't spawn subprocesses
&lt;/h3&gt;

&lt;p&gt;Cloudflare Workers don't run in containers or virtual machines. They run in V8 isolates, the same engine that powers Chrome's JavaScript execution. There's no operating system layer underneath. No filesystem. No shell.&lt;/p&gt;

&lt;p&gt;That means no &lt;code&gt;child_process.exec()&lt;/code&gt;, no &lt;code&gt;spawn()&lt;/code&gt;, no way to invoke a binary. FFmpeg is a compiled binary that expects to be called as a subprocess. Workers literally cannot do this. The APIs don't exist in the runtime.&lt;/p&gt;

&lt;p&gt;If you've used FFmpeg with Node.js before, you're probably calling it through &lt;code&gt;child_process&lt;/code&gt; or a wrapper like &lt;code&gt;fluent-ffmpeg&lt;/code&gt;. None of that code will even parse on Workers, let alone execute. You'll get build errors before your Worker ever deploys.&lt;/p&gt;

&lt;h3&gt;
  
  
  ffmpeg.wasm hits the bundle size limit
&lt;/h3&gt;

&lt;p&gt;The natural next thought: compile FFmpeg to WebAssembly and run it in the Worker. That's what &lt;a href="https://github.com/nicholasbraun/ffmpeg.wasm" rel="noopener noreferrer"&gt;ffmpeg.wasm&lt;/a&gt; does in the browser, and Workers support WASM.&lt;/p&gt;

&lt;p&gt;But Cloudflare enforces a 10 MB compressed bundle size limit for Workers (25 MB on the paid plan with some configurations). The ffmpeg.wasm core binary alone is roughly 25 MB uncompressed. Even aggressively stripped builds that remove codecs you don't need still land around 15-20 MB. You blow past the limit before adding a single line of your own code.&lt;/p&gt;

&lt;p&gt;Some developers try to lazy-load the WASM binary from R2 or an external URL at runtime. That leads directly to the third problem.&lt;/p&gt;

&lt;h3&gt;
  
  
  workerd blocks dynamic WASM compilation
&lt;/h3&gt;

&lt;p&gt;Cloudflare's runtime (&lt;code&gt;workerd&lt;/code&gt;) does not allow dynamic WASM compilation. You can't call &lt;code&gt;WebAssembly.compile()&lt;/code&gt; or &lt;code&gt;WebAssembly.instantiate()&lt;/code&gt; with a buffer fetched at runtime. The WASM module must be pre-compiled and included in your Worker bundle at deploy time.&lt;/p&gt;

&lt;p&gt;This is a deliberate security decision. Allowing arbitrary WASM compilation at runtime would break the isolation guarantees that make Workers safe to run on shared infrastructure.&lt;/p&gt;

&lt;p&gt;So you can't fetch the ffmpeg.wasm binary from R2, compile it on the fly, and run it. The module would need to be bundled, which circles back to the size limit. There's no escape hatch here.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix: call an FFmpeg API from your Worker
&lt;/h2&gt;

&lt;p&gt;Workers can't run FFmpeg, but they're excellent at one thing: making HTTP requests. Fast, globally distributed HTTP requests with sub-millisecond cold starts.&lt;/p&gt;

&lt;p&gt;Instead of fighting the runtime, use the Worker for what it's good at. Accept the upload or webhook trigger, make a single &lt;code&gt;fetch()&lt;/code&gt; call to &lt;a href="https://ffmpeg-micro.com" rel="noopener noreferrer"&gt;FFmpeg Micro&lt;/a&gt; to handle the transcode, and return the result. Your Worker stays small, fast, and within every Cloudflare limit.&lt;/p&gt;

&lt;p&gt;This is the same pattern that works for &lt;a href="https://dev.to/blog/ffmpeg-aws-lambda-layer-size-timeout"&gt;FFmpeg on AWS Lambda&lt;/a&gt; and &lt;a href="https://dev.to/blog/ffmpeg-heroku-ephemeral-filesystem-fix"&gt;FFmpeg on Heroku&lt;/a&gt;. The platform handles routing and triggers. The transcoding happens on infrastructure built for it.&lt;/p&gt;

&lt;p&gt;A single API call replaces the entire ffmpeg.wasm experiment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://api.ffmpeg-micro.com/v1/transcodes &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer YOUR_API_KEY"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "inputs": [{"url": "https://your-r2-bucket.r2.dev/input.mp4"}],
    "outputFormat": "mp4",
    "options": [
      {"option": "-c:v", "argument": "libx264"},
      {"option": "-crf", "argument": "28"}
    ]
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or use a preset if you don't want to think about FFmpeg flags:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"inputs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://your-r2-bucket.r2.dev/input.mp4"&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"outputFormat"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"mp4"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"preset"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"quality"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"high"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"resolution"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1080p"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Working example: compress-on-upload Worker
&lt;/h2&gt;

&lt;p&gt;This Worker receives a video URL (for example, from an R2 upload event or a webhook), sends it to FFmpeg Micro for compression, and returns the download URL for the processed file.&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;Env&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;FFMPEG_MICRO_API_KEY&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Env&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&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="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Method not allowed&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="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;405&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="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="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;videoUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;videoUrl&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;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;videoUrl is required&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="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&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="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;createResponse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://api.ffmpeg-micro.com/v1/transcodes&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="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;FFMPEG_MICRO_API_KEY&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&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="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
          &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;url&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="na"&gt;outputFormat&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mp4&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;options&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;span class="na"&gt;option&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;-c:v&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;argument&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;libx264&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="na"&gt;option&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;-crf&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;argument&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;28&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="na"&gt;option&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;-preset&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;argument&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fast&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="p"&gt;}),&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;createResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&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;error&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;createResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&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;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Transcode creation failed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;details&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;502&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&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="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;job&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;createResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&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;jobId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;attempts&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;maxAttempts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;completed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;failed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;attempts&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;maxAttempts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;await&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&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="mi"&gt;2000&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;pollResponse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s2"&gt;`https://api.ffmpeg-micro.com/v1/transcodes/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;jobId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;FFMPEG_MICRO_API_KEY&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&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;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pollData&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;pollResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&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;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;pollData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nx"&gt;attempts&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;failed&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="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Transcode failed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;jobId&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&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="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;completed&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="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Transcode timed out&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;jobId&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;504&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&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="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;downloadResponse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="s2"&gt;`https://api.ffmpeg-micro.com/v1/transcodes/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;jobId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/download?url=true`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;FFMPEG_MICRO_API_KEY&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&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;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;downloadData&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;downloadResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&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="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;completed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;jobId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;downloadUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;downloadData&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="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&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="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;Set your API key as a Workers secret:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx wrangler secret put FFMPEG_MICRO_API_KEY
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then deploy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx wrangler deploy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your Worker stays under 1 MB. The video processing runs on dedicated infrastructure with no bundle limits, no WASM restrictions, and no subprocess constraints.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common pitfalls
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Polling too aggressively.&lt;/strong&gt; The example above polls every 2 seconds, which is fine for most video lengths. For large files (over 1 GB), increase the interval to 5-10 seconds to avoid unnecessary API calls.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hitting the Worker CPU time limit.&lt;/strong&gt; Free-tier Workers have a 10ms CPU time limit per request. The polling loop uses wall-clock time (waiting on &lt;code&gt;fetch&lt;/code&gt;), not CPU time, so it won't trigger this limit. But if you add heavy request parsing or data transformation, you could hit it. Paid plans get 30 seconds of CPU time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Forgetting the Worker duration limit.&lt;/strong&gt; Workers on the free plan can run for up to 30 seconds total. The polling loop in the example could exceed this for longer videos. On the paid plan ($5/month), Workers can run for up to 30 minutes, which covers most transcoding jobs. For very long encodes, consider using a Cloudflare Queue or Durable Object to handle the polling asynchronously.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Passing R2 private URLs.&lt;/strong&gt; If your video is in a private R2 bucket, the FFmpeg Micro API can't fetch it directly. Generate a presigned R2 URL with a short expiry and pass that as the input URL instead.&lt;/p&gt;

&lt;p&gt;You can get an FFmpeg Micro API key in under a minute at &lt;a href="https://ffmpeg-micro.com" rel="noopener noreferrer"&gt;ffmpeg-micro.com&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Can I run FFmpeg directly on Cloudflare Workers?
&lt;/h3&gt;

&lt;p&gt;No. Cloudflare Workers run in V8 isolates, not containers. There's no subprocess execution, no filesystem, and no way to invoke compiled binaries. FFmpeg requires all three. The only way to process video from a Worker is to call an external API like FFmpeg Micro.&lt;/p&gt;

&lt;h3&gt;
  
  
  Will ffmpeg.wasm work on Cloudflare Workers?
&lt;/h3&gt;

&lt;p&gt;No. The ffmpeg.wasm core binary exceeds Cloudflare's 10 MB compressed bundle size limit, and the Workers runtime (&lt;code&gt;workerd&lt;/code&gt;) blocks dynamic WASM compilation at runtime. You can't bundle it and you can't load it on the fly. Both paths are dead ends.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is the Cloudflare Workers bundle size limit?
&lt;/h3&gt;

&lt;p&gt;Cloudflare enforces a 10 MB compressed limit for Worker bundles on the free plan and most paid configurations. Some enterprise plans allow up to 25 MB. The ffmpeg.wasm binary alone is roughly 25 MB uncompressed, which exceeds both tiers after you add your application code.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I process video uploads stored in Cloudflare R2?
&lt;/h3&gt;

&lt;p&gt;Generate a presigned URL for the R2 object and pass it to the FFmpeg Micro API as the input URL. Your Worker handles the R2 presigning and the API call. The transcode runs externally, and you get back a download URL for the processed file. Your R2 object never needs to be publicly accessible.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is there a Cloudflare-native alternative to FFmpeg for video processing?
&lt;/h3&gt;

&lt;p&gt;Cloudflare Stream handles video hosting and adaptive bitrate delivery, but it's not a general-purpose transcoding tool. You can't pass custom FFmpeg flags, extract audio tracks, generate thumbnails at specific timestamps, or apply filters like text overlays and cropping. For anything beyond basic video hosting, you need an FFmpeg API.&lt;/p&gt;

</description>
      <category>ffmpeg</category>
      <category>cloudflare</category>
      <category>serverless</category>
      <category>api</category>
    </item>
    <item>
      <title>ffprobe: How to Inspect Video Metadata Before Processing</title>
      <dc:creator>Javid Jamae</dc:creator>
      <pubDate>Wed, 22 Jul 2026 10:22:58 +0000</pubDate>
      <link>https://dev.to/javidjamae/ffprobe-how-to-inspect-video-metadata-before-processing-2mje</link>
      <guid>https://dev.to/javidjamae/ffprobe-how-to-inspect-video-metadata-before-processing-2mje</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.ffmpeg-micro.com/blog/ffprobe-inspect-video-metadata" rel="noopener noreferrer"&gt;ffmpeg-micro.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Every video file lies to you a little. The container says MP4, but the codec might be MPEG-2. The filename says 1080p, but the actual resolution is 720x480. Running ffprobe before you process a file tells you exactly what you're working with. It saves you from wasting compute time and API credits on files that will fail, produce garbage output, or need completely different settings than you assumed.&lt;/p&gt;

&lt;h2&gt;
  
  
  What ffprobe Does
&lt;/h2&gt;

&lt;p&gt;ffprobe is a companion tool that ships with every FFmpeg installation. It reads a media file's container and streams without decoding the actual video frames, which makes it fast. You get back structured metadata: codecs, resolution, duration, bitrate, audio channels, frame rate, and more.&lt;/p&gt;

&lt;p&gt;The command you'll use 90% of the time looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffprobe &lt;span class="nt"&gt;-v&lt;/span&gt; quiet &lt;span class="nt"&gt;-print_format&lt;/span&gt; json &lt;span class="nt"&gt;-show_streams&lt;/span&gt; &lt;span class="nt"&gt;-show_format&lt;/span&gt; input.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Breaking that down:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;-v quiet&lt;/code&gt; suppresses the banner and warnings, so you only get clean data&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-print_format json&lt;/code&gt; gives you machine-readable JSON instead of the default key=value dump&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-show_streams&lt;/code&gt; returns details for each stream (video, audio, subtitle tracks)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-show_format&lt;/code&gt; returns container-level info like duration, bitrate, and format name&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can also point ffprobe at a URL instead of a local file. It'll pull just enough bytes to read the headers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reading the JSON Output
&lt;/h2&gt;

&lt;p&gt;The JSON output from ffprobe has two top-level keys: &lt;code&gt;streams&lt;/code&gt; and &lt;code&gt;format&lt;/code&gt;. The &lt;code&gt;streams&lt;/code&gt; array contains one object per track in the file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"streams"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"index"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"codec_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"h264"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"codec_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"video"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"width"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1920&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"height"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1080&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"r_frame_rate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"30/1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"duration"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"124.500000"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"bit_rate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"5000000"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"index"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"codec_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"aac"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"codec_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"audio"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"channels"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"sample_rate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"44100"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"bit_rate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"128000"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"format"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"filename"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"input.mp4"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"format_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"mov,mp4,m4a,3gp,3g2,mj2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"duration"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"124.500000"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"size"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"78125000"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"bit_rate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"5017600"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fields that matter most for pre-processing decisions:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Where&lt;/th&gt;
&lt;th&gt;What it tells you&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;codec_name&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;streams&lt;/td&gt;
&lt;td&gt;Video codec (h264, hevc, vp9, av1)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;width&lt;/code&gt; / &lt;code&gt;height&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;streams&lt;/td&gt;
&lt;td&gt;Actual pixel dimensions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;duration&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;format&lt;/td&gt;
&lt;td&gt;Total length in seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;bit_rate&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;format&lt;/td&gt;
&lt;td&gt;Overall bitrate in bits/sec&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;channels&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;streams (audio)&lt;/td&gt;
&lt;td&gt;Mono (1), stereo (2), surround (6)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;r_frame_rate&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;streams (video)&lt;/td&gt;
&lt;td&gt;Frame rate as a fraction&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Pre-Processing Checks That Save You Money
&lt;/h2&gt;

&lt;p&gt;I've seen developers skip inspection and send files straight to a transcoding API. Then they're surprised when jobs fail or produce weird results. A few checks with ffprobe catch the most common problems.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Codec validation
&lt;/h3&gt;

&lt;p&gt;Not every codec is supported by every processing pipeline. If someone uploads a ProRes file but your workflow expects H.264, you want to know before you spend credits. For a full rundown of format handling, check out our guide on &lt;a href="https://dev.to/blog/ffmpeg-convert-video-format"&gt;converting video formats with FFmpeg&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;VIDEO_CODEC&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;ffprobe &lt;span class="nt"&gt;-v&lt;/span&gt; quiet &lt;span class="nt"&gt;-print_format&lt;/span&gt; json &lt;span class="nt"&gt;-show_streams&lt;/span&gt; input.mp4 | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.streams[] | select(.codec_type=="video") | .codec_name'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$VIDEO_CODEC&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s2"&gt;"h264"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$VIDEO_CODEC&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s2"&gt;"hevc"&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Unsupported codec: &lt;/span&gt;&lt;span class="nv"&gt;$VIDEO_CODEC&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Resolution check
&lt;/h3&gt;

&lt;p&gt;If you're targeting 1080p output and the input is already 640x360, upscaling won't add quality. It just wastes processing time and increases file size. You might want to skip the transcode entirely or warn the user. Our &lt;a href="https://dev.to/blog/ffmpeg-scale-filter-preserve-aspect-ratio"&gt;FFmpeg scale filter guide&lt;/a&gt; covers the details of resizing while preserving aspect ratio.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;HEIGHT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;ffprobe &lt;span class="nt"&gt;-v&lt;/span&gt; quiet &lt;span class="nt"&gt;-print_format&lt;/span&gt; json &lt;span class="nt"&gt;-show_streams&lt;/span&gt; input.mp4 | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.streams[] | select(.codec_type=="video") | .height'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HEIGHT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-lt&lt;/span&gt; 720 &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Input is only &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;HEIGHT&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;p. Skipping upscale."&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Duration sanity check
&lt;/h3&gt;

&lt;p&gt;A 6-hour video will take a long time and cost real money to transcode. And a 0-second file means something is corrupt. Set boundaries.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;DURATION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;ffprobe &lt;span class="nt"&gt;-v&lt;/span&gt; quiet &lt;span class="nt"&gt;-print_format&lt;/span&gt; json &lt;span class="nt"&gt;-show_format&lt;/span&gt; input.mp4 | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.format.duration'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;SECONDS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;DURATION&lt;/span&gt;&lt;span class="p"&gt;%.*&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SECONDS&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-eq&lt;/span&gt; 0 &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"File appears corrupt (0 duration)"&lt;/span&gt;
  &lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SECONDS&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-gt&lt;/span&gt; 3600 &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"File is over 1 hour. Consider splitting first."&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you do need to split long files, our &lt;a href="https://dev.to/blog/ffmpeg-split-video-into-segments"&gt;guide to splitting video into segments&lt;/a&gt; covers three different methods.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Missing audio check
&lt;/h3&gt;

&lt;p&gt;Some workflows assume audio exists. If you're adding subtitles or normalizing loudness, a video-only file will cause failures downstream.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;AUDIO_STREAMS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;ffprobe &lt;span class="nt"&gt;-v&lt;/span&gt; quiet &lt;span class="nt"&gt;-print_format&lt;/span&gt; json &lt;span class="nt"&gt;-show_streams&lt;/span&gt; input.mp4 | jq &lt;span class="s1"&gt;'[.streams[] | select(.codec_type=="audio")] | length'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$AUDIO_STREAMS&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-eq&lt;/span&gt; 0 &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"No audio track found"&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Using ffprobe Output to Pick API Settings
&lt;/h2&gt;

&lt;p&gt;The real payoff comes when you use ffprobe data to dynamically set your transcoding parameters. Instead of hardcoding "1080p, high quality" for every file, you can make smart choices.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;HEIGHT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;ffprobe &lt;span class="nt"&gt;-v&lt;/span&gt; quiet &lt;span class="nt"&gt;-print_format&lt;/span&gt; json &lt;span class="nt"&gt;-show_streams&lt;/span&gt; input.mp4 | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.streams[] | select(.codec_type=="video") | .height'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HEIGHT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-ge&lt;/span&gt; 2160 &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nv"&gt;RESOLUTION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"1080p"&lt;/span&gt;
  &lt;span class="nv"&gt;QUALITY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"high"&lt;/span&gt;
&lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HEIGHT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-ge&lt;/span&gt; 1080 &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nv"&gt;RESOLUTION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"1080p"&lt;/span&gt;
  &lt;span class="nv"&gt;QUALITY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"medium"&lt;/span&gt;
&lt;span class="k"&gt;else
  &lt;/span&gt;&lt;span class="nv"&gt;RESOLUTION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;HEIGHT&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;p"&lt;/span&gt;
  &lt;span class="nv"&gt;QUALITY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"medium"&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then pass those values to the FFmpeg Micro API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://api.ffmpeg-micro.com/v1/transcodes &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer YOUR_API_KEY"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"{
    &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;inputs&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;: [{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;url&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;https://example.com/video.mp4&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;}],
    &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;outputFormat&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;mp4&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;,
    &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;preset&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;: {&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;quality&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="nv"&gt;$QUALITY&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;, &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;resolution&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="nv"&gt;$RESOLUTION&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;}
  }"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This pattern is especially useful for &lt;a href="https://dev.to/blog/ffmpeg-batch-processing-guide"&gt;batch processing&lt;/a&gt; where you're handling hundreds of files with different properties. A 4K file gets downscaled to 1080p. A 720p file stays at 720p. A corrupt file gets skipped entirely. No wasted API calls.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common ffprobe Pitfalls
&lt;/h2&gt;

&lt;p&gt;A few things catch developers off guard.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Duration lives in two places.&lt;/strong&gt; The stream-level &lt;code&gt;duration&lt;/code&gt; and the format-level &lt;code&gt;duration&lt;/code&gt; can differ. Some containers don't store duration in the stream metadata at all. Always prefer &lt;code&gt;format.duration&lt;/code&gt; for the total file length.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Variable frame rate breaks assumptions.&lt;/strong&gt; Screen recordings and phone videos often use variable frame rate (VFR). ffprobe reports the average or the container-declared rate, which might not match what you actually see. If frame-accurate processing matters, check the &lt;code&gt;r_frame_rate&lt;/code&gt; vs &lt;code&gt;avg_frame_rate&lt;/code&gt; fields.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;codec_name&lt;/code&gt; isn't always what you expect.&lt;/strong&gt; An MP4 file can contain nearly any codec. I've seen MP4 containers with MJPEG video streams (from older cameras) and even raw PCM audio. Don't trust the file extension. Trust the &lt;code&gt;codec_name&lt;/code&gt; from ffprobe.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Remote URLs need accessible headers.&lt;/strong&gt; When probing a URL, ffprobe sends a standard HTTP GET. If the server requires auth headers, you'll need to pass them with &lt;code&gt;-headers&lt;/code&gt;. See our &lt;a href="https://dev.to/blog/ffmpeg-auth-headers"&gt;FFmpeg auth headers guide&lt;/a&gt; for the syntax.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start Inspecting, Then Process
&lt;/h2&gt;

&lt;p&gt;Running ffprobe before processing is a two-minute addition to any video pipeline that prevents the most frustrating failures. Once you know what's in the file, send it to &lt;a href="https://www.ffmpeg-micro.com" rel="noopener noreferrer"&gt;FFmpeg Micro&lt;/a&gt; to process. One API call, no FFmpeg install, no server to maintain.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  How do I get ffprobe output in JSON format?
&lt;/h3&gt;

&lt;p&gt;Use the &lt;code&gt;-print_format json&lt;/code&gt; flag combined with &lt;code&gt;-show_streams&lt;/code&gt; and &lt;code&gt;-show_format&lt;/code&gt;. The full command is &lt;code&gt;ffprobe -v quiet -print_format json -show_streams -show_format input.mp4&lt;/code&gt;. This returns a structured JSON object with all stream and container metadata.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can ffprobe read a video from a URL without downloading the whole file?
&lt;/h3&gt;

&lt;p&gt;Yes. ffprobe only reads the container headers and metadata, not the full file. You can pass a URL directly: &lt;code&gt;ffprobe -v quiet -print_format json -show_streams https://example.com/video.mp4&lt;/code&gt;. It pulls just enough data to identify codecs, resolution, duration, and stream info.&lt;/p&gt;

&lt;h3&gt;
  
  
  What's the difference between ffprobe and ffmpeg for reading metadata?
&lt;/h3&gt;

&lt;p&gt;ffmpeg can show metadata too (just run &lt;code&gt;ffmpeg -i input.mp4&lt;/code&gt; with no output), but ffprobe is purpose-built for inspection. It gives you structured output in JSON, XML, CSV, or flat formats. ffmpeg's metadata dump is unstructured text meant for humans, not scripts.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does ffprobe work with audio files?
&lt;/h3&gt;

&lt;p&gt;ffprobe works with any format FFmpeg supports, including MP3, WAV, FLAC, AAC, and OGG. The output structure is the same. You'll get stream-level info for codec, sample rate, channels, and bitrate.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I check if a video file is corrupt using ffprobe?
&lt;/h3&gt;

&lt;p&gt;A quick corruption check is to look for a duration of 0 or a missing video stream. For deeper validation, run &lt;code&gt;ffprobe -v error input.mp4&lt;/code&gt;, which prints only error-level messages. If it outputs anything, the file has problems. For a full frame-by-frame check, you'll need &lt;code&gt;ffmpeg -v error -i input.mp4 -f null -&lt;/code&gt;, which actually decodes every frame.&lt;/p&gt;

</description>
      <category>ffmpeg</category>
      <category>ffprobe</category>
      <category>video</category>
      <category>api</category>
    </item>
    <item>
      <title>FFmpeg on AWS Lambda: The Layer-Size and Timeout Problem</title>
      <dc:creator>Javid Jamae</dc:creator>
      <pubDate>Tue, 21 Jul 2026 10:16:32 +0000</pubDate>
      <link>https://dev.to/javidjamae/ffmpeg-on-aws-lambda-the-layer-size-and-timeout-problem-31gb</link>
      <guid>https://dev.to/javidjamae/ffmpeg-on-aws-lambda-the-layer-size-and-timeout-problem-31gb</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.ffmpeg-micro.com/blog/ffmpeg-aws-lambda-layer-size-timeout" rel="noopener noreferrer"&gt;ffmpeg-micro.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;You're building a serverless video pipeline. You add an FFmpeg static binary as a Lambda layer, wire up an S3 trigger, deploy, and immediately hit a wall. The binary is too big, the storage is too small, and the timeout isn't long enough. I've watched this play out on dozens of projects, and the failure mode is always the same.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 250 MB Layer Limit
&lt;/h2&gt;

&lt;p&gt;AWS Lambda layers max out at 250 MB unzipped. That's a hard limit, and FFmpeg pushes right up against it.&lt;/p&gt;

&lt;p&gt;A typical FFmpeg static build compresses to around 70-80 MB as a zip file. Unzipped, with common codecs included, you're looking at 200 MB or more. That sounds like it fits, but remember: your function code, runtime, and all layers share that 250 MB budget. Once you add the AWS SDK, a couple of Node.js dependencies, and your handler code, you're over.&lt;/p&gt;

&lt;p&gt;The usual fix is stripping codecs. You rebuild FFmpeg with only the encoders you need. But the codecs you "don't need" have a way of becoming the codecs you do need. Drop libx265 and someone requests HEVC output. Drop libvpx and a client wants WebM. You end up maintaining a custom FFmpeg build just to stay under a Lambda limit.&lt;/p&gt;

&lt;p&gt;A stripped FFmpeg binary with only libx264 still runs about 40-50 MB unzipped. Add libx265 and libvpx, and you're back above 150 MB before your function code enters the picture.&lt;/p&gt;

&lt;h2&gt;
  
  
  The /tmp Storage Cap
&lt;/h2&gt;

&lt;p&gt;Lambda gives you 512 MB of ephemeral storage in &lt;code&gt;/tmp&lt;/code&gt; by default. You can bump this to 10 GB, but you pay for it ($0.0000000309 per GB-second). And you'll need every byte.&lt;/p&gt;

&lt;p&gt;FFmpeg doesn't just read an input and write an output. It creates intermediate temp files during encoding. So for a single transcode, you need space for the input file, the output file, and FFmpeg's working files. A 500 MB source video blows through the default 512 MB allocation before the encode even starts. Even with the 10 GB option, a 4K video file can eat that up fast when you factor in temp data.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 15-Minute Timeout
&lt;/h2&gt;

&lt;p&gt;Lambda functions have a hard ceiling of 15 minutes. You can't extend it, negotiate it, or work around it.&lt;/p&gt;

&lt;p&gt;Encoding a 10-minute 1080p video to H.265 takes 20-30 minutes on Lambda's shared vCPUs. That's not a worst case. That's a normal case. Lambda doesn't give you dedicated CPU cores. You're on shared infrastructure, and encoding performance varies between invocations. A job that finishes in 12 minutes on one run might take 18 on the next.&lt;/p&gt;

&lt;p&gt;For audio extraction or format remuxing (copying streams without re-encoding), 15 minutes is usually fine. For actual transcoding with compute-intensive codecs, it's not.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Cold Start Tax
&lt;/h2&gt;

&lt;p&gt;When you outgrow layers and switch to Docker images on Amazon ECR (the standard workaround for binaries larger than 250 MB), you trade the layer-size problem for a cold start problem.&lt;/p&gt;

&lt;p&gt;Lambda container images can be up to 10 GB, which solves the binary size issue. But cold starts for container-based Lambda functions run 5-15 seconds, sometimes longer. If your video pipeline is event-driven, processing uploads as they arrive, those cold starts add real latency. A user uploads a short clip expecting a fast turnaround, and the first 10 seconds are just Lambda pulling your container image.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Lambda + FFmpeg Actually Works
&lt;/h2&gt;

&lt;p&gt;I don't want to pretend Lambda is never the right call. For small, simple jobs, it works fine.&lt;/p&gt;

&lt;p&gt;Extracting a thumbnail from a video? Lambda handles that in seconds. Pulling the audio track from an MP4? No problem. Trimming a sub-30-second clip or converting between container formats without re-encoding? All good.&lt;/p&gt;

&lt;p&gt;A minimal Lambda handler that extracts a thumbnail using a pre-installed FFmpeg layer:&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; /tmp/input.mp4 &lt;span class="nt"&gt;-ss&lt;/span&gt; 00:00:05 &lt;span class="nt"&gt;-frames&lt;/span&gt;:v 1 /tmp/thumbnail.jpg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your workload fits this profile (small files, simple operations, no heavy encoding), Lambda plus an FFmpeg layer is a reasonable setup. The trouble starts when the jobs get bigger.&lt;/p&gt;

&lt;h2&gt;
  
  
  The API Alternative: Call FFmpeg Micro from Your Lambda
&lt;/h2&gt;

&lt;p&gt;Instead of cramming FFmpeg into Lambda, you can keep your Lambda function small and offload the encoding to a dedicated API. Your Lambda becomes a thin orchestration layer. No binary, no layer, no &lt;code&gt;/tmp&lt;/code&gt; pressure.&lt;/p&gt;

&lt;p&gt;FFmpeg Micro is a cloud API that runs FFmpeg commands over HTTP. You send a POST request with your inputs and options, and get back processed video. No binary to install, no server to manage. &lt;a href="https://ffmpeg-micro.com" rel="noopener noreferrer"&gt;Get a free API key at ffmpeg-micro.com&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;A TypeScript Lambda handler that starts a transcode job through the FFmpeg Micro API:&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;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Handler&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;aws-lambda&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;FFMPEG_MICRO_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;FFMPEG_MICRO_API_KEY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Handler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&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="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;outputFormat&lt;/span&gt; &lt;span class="o"&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="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&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;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.ffmpeg-micro.com/v1/transcodes&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="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Authorization&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;FFMPEG_MICRO_KEY&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&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="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;url&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;outputFormat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;preset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;quality&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;high&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;resolution&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1080p&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="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;job&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;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&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="na"&gt;jobId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&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;This Lambda function is a few kilobytes. No layer. No Docker image. No cold start penalty beyond the standard Node.js runtime. The actual encoding happens on FFmpeg Micro's infrastructure, which means no 15-minute timeout concern and no &lt;code&gt;/tmp&lt;/code&gt; storage juggling.&lt;/p&gt;

&lt;p&gt;You fire the request, get a job ID back, and poll &lt;code&gt;GET /v1/transcodes/{id}&lt;/code&gt; for completion. The finished job includes an &lt;code&gt;outputUrl&lt;/code&gt; where you can grab the processed file. If you want to avoid polling from Lambda, trigger a second Lambda from a webhook or a Step Functions wait state.&lt;/p&gt;

&lt;p&gt;We broke down the full AWS cost picture in &lt;a href="https://dev.to/blog/ffmpeg-aws-cost-breakdown"&gt;FFmpeg on AWS: Cost Breakdown&lt;/a&gt;. For a broader comparison, see &lt;a href="https://dev.to/blog/self-hosting-ffmpeg-vs-api"&gt;Self-Hosting FFmpeg vs API&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Can FFmpeg run inside AWS Lambda?
&lt;/h3&gt;

&lt;p&gt;Yes, but with significant constraints. AWS Lambda's 250 MB unzipped layer limit, 512 MB default &lt;code&gt;/tmp&lt;/code&gt; storage, and 15-minute execution ceiling make it viable only for lightweight FFmpeg tasks like thumbnail extraction, audio stripping, or short clip trimming. Heavy transcoding jobs will hit timeout or storage limits.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I add FFmpeg to an AWS Lambda layer?
&lt;/h3&gt;

&lt;p&gt;You package a statically compiled FFmpeg binary into a zip file and upload it as a Lambda layer. The binary must be compiled for Amazon Linux 2 (x86_64 or arm64). Place it in the &lt;code&gt;bin/&lt;/code&gt; directory of your layer zip so it's available at &lt;code&gt;/opt/bin/ffmpeg&lt;/code&gt; in your function. Keep total unzipped size under 250 MB across all layers.&lt;/p&gt;

&lt;h3&gt;
  
  
  What's the maximum video size I can process with FFmpeg on Lambda?
&lt;/h3&gt;

&lt;p&gt;With default settings, you're limited to roughly 150-200 MB input files because &lt;code&gt;/tmp&lt;/code&gt; (512 MB) must hold the input, output, and FFmpeg's temp files. You can increase &lt;code&gt;/tmp&lt;/code&gt; to 10 GB for additional cost, which raises the practical limit, but the 15-minute timeout becomes the bottleneck for larger files that require re-encoding.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is there a serverless way to run FFmpeg without Lambda limits?
&lt;/h3&gt;

&lt;p&gt;Calling a managed transcoding API like FFmpeg Micro from your Lambda function removes the binary size, storage, and timeout constraints. Your Lambda stays small and fast while the encoding runs on dedicated infrastructure. AWS also offers Elastic Transcoder and MediaConvert, though both use proprietary job configs rather than standard FFmpeg commands.&lt;/p&gt;

&lt;h3&gt;
  
  
  How long does FFmpeg take to encode video on Lambda?
&lt;/h3&gt;

&lt;p&gt;On Lambda's shared vCPUs, encoding a 10-minute 1080p video to H.264 typically takes 10-20 minutes. H.265 encoding runs 20-30 minutes for the same input. Performance varies between invocations because CPU resources are shared. Operations that don't re-encode (remuxing, stream copying) finish in seconds.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>ffmpeg</category>
      <category>serverless</category>
      <category>devops</category>
    </item>
    <item>
      <title>FFmpeg drawtext Filter: Complete Guide to Text Overlays</title>
      <dc:creator>Javid Jamae</dc:creator>
      <pubDate>Tue, 21 Jul 2026 10:15:52 +0000</pubDate>
      <link>https://dev.to/javidjamae/ffmpeg-drawtext-filter-complete-guide-to-text-overlays-4469</link>
      <guid>https://dev.to/javidjamae/ffmpeg-drawtext-filter-complete-guide-to-text-overlays-4469</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.ffmpeg-micro.com/blog/ffmpeg-drawtext-filter-guide" rel="noopener noreferrer"&gt;ffmpeg-micro.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;drawtext&lt;/code&gt; filter is the most parameter-heavy filter in FFmpeg. The official documentation reads like a specification sheet, not a guide. I've spent enough time decoding it that I figured I'd write the reference I wish existed.&lt;/p&gt;

&lt;p&gt;If you just need to add text quickly, see our &lt;a href="https://dev.to/blog/ffmpeg-add-text-to-video"&gt;step-by-step guide&lt;/a&gt;. This post is the full parameter breakdown for when you need precise control.&lt;/p&gt;

&lt;h2&gt;
  
  
  Basic drawtext Syntax
&lt;/h2&gt;

&lt;p&gt;The minimum viable drawtext command needs just a text string:&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.mp4 &lt;span class="nt"&gt;-vf&lt;/span&gt; &lt;span class="s2"&gt;"drawtext=text='Hello World'"&lt;/span&gt; output.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That renders white text in the top-left corner using FFmpeg's built-in font. Not useful on its own, but it confirms the filter works before you start layering on parameters.&lt;/p&gt;

&lt;p&gt;Every parameter after that is a colon-separated key-value pair inside the filter string. The general pattern looks like this:&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.mp4 &lt;span class="nt"&gt;-vf&lt;/span&gt; &lt;span class="s2"&gt;"drawtext=text='Hello World':fontsize=48:fontcolor=white:x=100:y=100"&lt;/span&gt; output.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Font Control: fontfile, fontname, fontsize, fontcolor
&lt;/h2&gt;

&lt;p&gt;You've got two ways to specify a font. &lt;code&gt;fontfile&lt;/code&gt; takes a path to a &lt;code&gt;.ttf&lt;/code&gt; or &lt;code&gt;.otf&lt;/code&gt; file on disk. &lt;code&gt;fontname&lt;/code&gt; takes the name of a font installed on the system and requires that FFmpeg was built with fontconfig support.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Using a font file path&lt;/span&gt;
&lt;span class="nv"&gt;drawtext&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;fontfile&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf:text&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'Title'&lt;/span&gt;

&lt;span class="c"&gt;# Using a system font name (requires fontconfig)&lt;/span&gt;
&lt;span class="nv"&gt;drawtext&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;fontname&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'Arial'&lt;/span&gt;:text&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'Title'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I almost always use &lt;code&gt;fontfile&lt;/code&gt; because it's portable. If you're running FFmpeg in Docker or on a remote server, you can't assume system fonts are available.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;fontsize&lt;/strong&gt; accepts an integer in pixels. The default is 16, which is tiny on most video resolutions. For 1080p, I usually start around 48 and go up from there.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;fontcolor&lt;/strong&gt; accepts named colors (&lt;code&gt;white&lt;/code&gt;, &lt;code&gt;red&lt;/code&gt;, &lt;code&gt;yellow&lt;/code&gt;) or hex values. Hex values in drawtext use the format &lt;code&gt;0xRRGGBB&lt;/code&gt; or &lt;code&gt;0xRRGGBBAA&lt;/code&gt; where &lt;code&gt;AA&lt;/code&gt; is alpha. Note that this is not the CSS &lt;code&gt;#RRGGBB&lt;/code&gt; format.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;drawtext&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'Visible'&lt;/span&gt;:fontcolor&lt;span class="o"&gt;=&lt;/span&gt;white
&lt;span class="nv"&gt;drawtext&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'Semi-transparent'&lt;/span&gt;:fontcolor&lt;span class="o"&gt;=&lt;/span&gt;0xFFFFFF80
&lt;span class="nv"&gt;drawtext&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'Named color'&lt;/span&gt;:fontcolor&lt;span class="o"&gt;=&lt;/span&gt;yellow
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The full list of named colors comes from FFmpeg's libavutil color table, which matches the X11 color set.&lt;/p&gt;

&lt;h2&gt;
  
  
  Position: x and y Expressions
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;x&lt;/code&gt; and &lt;code&gt;y&lt;/code&gt; parameters control where the text appears, and they accept math expressions, not just static numbers. This is where drawtext gets interesting.&lt;/p&gt;

&lt;p&gt;FFmpeg exposes several variables you can use in these expressions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;w&lt;/code&gt; and &lt;code&gt;h&lt;/code&gt; are the video width and height&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;text_w&lt;/code&gt; and &lt;code&gt;text_h&lt;/code&gt; are the rendered width and height of the text&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;n&lt;/code&gt; is the current frame number&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;t&lt;/code&gt; is the timestamp in seconds&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Center the text on screen:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;x&lt;/span&gt;&lt;span class="o"&gt;=(&lt;/span&gt;w-text_w&lt;span class="o"&gt;)&lt;/span&gt;/2:y&lt;span class="o"&gt;=(&lt;/span&gt;h-text_h&lt;span class="o"&gt;)&lt;/span&gt;/2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Bottom-center with a margin:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;x&lt;/span&gt;&lt;span class="o"&gt;=(&lt;/span&gt;w-text_w&lt;span class="o"&gt;)&lt;/span&gt;/2:y&lt;span class="o"&gt;=&lt;/span&gt;h-text_h-40
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Top-right corner with padding:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;x&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;w-text_w-20:y&lt;span class="o"&gt;=&lt;/span&gt;20
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The expression &lt;code&gt;(w-text_w)/2&lt;/code&gt; is the standard centering formula. It takes the video width, subtracts the text width, and divides by two. Same logic applies vertically with &lt;code&gt;h&lt;/code&gt; and &lt;code&gt;text_h&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;You can also animate position. Scrolling text from right to left:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;x&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;w-t&lt;span class="k"&gt;*&lt;/span&gt;100:y&lt;span class="o"&gt;=&lt;/span&gt;h-text_h-20
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That moves the text 100 pixels per second toward the left edge.&lt;/p&gt;

&lt;h2&gt;
  
  
  Styling: box, border, and shadow
&lt;/h2&gt;

&lt;p&gt;Plain white text on video is hard to read. The &lt;code&gt;box&lt;/code&gt;, &lt;code&gt;borderw&lt;/code&gt;, and shadow parameters fix that.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Box background&lt;/strong&gt; draws a filled rectangle behind the text. Set &lt;code&gt;box=1&lt;/code&gt; to enable it, then control the color with &lt;code&gt;boxcolor&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;&lt;span class="nv"&gt;drawtext&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'Breaking News'&lt;/span&gt;:box&lt;span class="o"&gt;=&lt;/span&gt;1:boxcolor&lt;span class="o"&gt;=&lt;/span&gt;black@0.6:boxborderw&lt;span class="o"&gt;=&lt;/span&gt;10
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;@0.6&lt;/code&gt; syntax sets opacity. &lt;code&gt;boxborderw&lt;/code&gt; adds padding between the text and the box edge, in pixels. You can also specify per-side padding with &lt;code&gt;boxborderw=10|20|10|20&lt;/code&gt; (top, right, bottom, left).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Text border&lt;/strong&gt; draws an outline around each character. Useful for legibility without a box.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;drawtext&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'Outlined'&lt;/span&gt;:borderw&lt;span class="o"&gt;=&lt;/span&gt;2:bordercolor&lt;span class="o"&gt;=&lt;/span&gt;black
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Shadow&lt;/strong&gt; offsets a shadow copy of the text.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;drawtext&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'Shadow'&lt;/span&gt;:shadowx&lt;span class="o"&gt;=&lt;/span&gt;3:shadowy&lt;span class="o"&gt;=&lt;/span&gt;3:shadowcolor&lt;span class="o"&gt;=&lt;/span&gt;black@0.5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I combine border and shadow often. A 1px black border with a subtle shadow makes text readable on almost any background.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dynamic Text with Expansion Expressions
&lt;/h2&gt;

&lt;p&gt;Set &lt;code&gt;expansion=normal&lt;/code&gt; (it's actually the default) to use &lt;code&gt;%{...}&lt;/code&gt; expressions inside your text string. These evaluate per-frame.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Burn the timestamp into the video:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;drawtext&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'%{pts\:hms}'&lt;/span&gt;:fontsize&lt;span class="o"&gt;=&lt;/span&gt;24:fontcolor&lt;span class="o"&gt;=&lt;/span&gt;white:x&lt;span class="o"&gt;=&lt;/span&gt;10:y&lt;span class="o"&gt;=&lt;/span&gt;10
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;%{pts\:hms}&lt;/code&gt; expression renders the presentation timestamp in &lt;code&gt;HH:MM:SS&lt;/code&gt; format. Note the backslash-escaped colon, which is necessary because colons are the parameter delimiter.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Show frame number:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;drawtext&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'Frame %{n}'&lt;/span&gt;:fontsize&lt;span class="o"&gt;=&lt;/span&gt;20:x&lt;span class="o"&gt;=&lt;/span&gt;10:y&lt;span class="o"&gt;=&lt;/span&gt;10
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Show local clock time:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;drawtext&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'%{localtime\:%Y-%m-%d %H\\\:%M\\\:%S}'&lt;/span&gt;:fontsize&lt;span class="o"&gt;=&lt;/span&gt;24:x&lt;span class="o"&gt;=&lt;/span&gt;10:y&lt;span class="o"&gt;=&lt;/span&gt;10
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The escaping in &lt;code&gt;localtime&lt;/code&gt; gets ugly because strftime format codes also use colons and percent signs. Triple backslashes before colons inside the strftime pattern is the standard workaround.&lt;/p&gt;

&lt;p&gt;You can also use the &lt;code&gt;enable&lt;/code&gt; parameter to show text only during a specific time range:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;drawtext&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'Intro'&lt;/span&gt;:enable&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'between(t,0,5)'&lt;/span&gt;:fontsize&lt;span class="o"&gt;=&lt;/span&gt;48:x&lt;span class="o"&gt;=(&lt;/span&gt;w-text_w&lt;span class="o"&gt;)&lt;/span&gt;/2:y&lt;span class="o"&gt;=(&lt;/span&gt;h-text_h&lt;span class="o"&gt;)&lt;/span&gt;/2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That displays "Intro" only during the first five seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Text Overlays via FFmpeg Micro API
&lt;/h2&gt;

&lt;p&gt;If you don't want to manage FFmpeg binaries, FFmpeg Micro runs these commands for you over HTTP.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;@text-overlay&lt;/code&gt; virtual option handles font loading, line wrapping, and positioning for you:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"inputs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://example.com/video.mp4"&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"outputFormat"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"mp4"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"options"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"option"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"@text-overlay"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"argument"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Your text here"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"style"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"charsPerLine"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"fontSize"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"lineSpacing"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"x"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0.15*w"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"y"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"(h-text_h)/2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"boxBorderW"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For full control, pass a raw drawtext filter string using the &lt;code&gt;-vf&lt;/code&gt; option:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"inputs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://example.com/video.mp4"&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"outputFormat"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"mp4"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"options"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"option"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"-vf"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"argument"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"drawtext=text='Hello World':fontsize=48:fontcolor=white:x=(w-text_w)/2:y=(h-text_h)/2"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both go to &lt;code&gt;POST https://api.ffmpeg-micro.com/v1/transcodes&lt;/code&gt; with an &lt;code&gt;Authorization: Bearer YOUR_API_KEY&lt;/code&gt; header.&lt;/p&gt;

&lt;p&gt;FFmpeg Micro is a cloud API that runs FFmpeg commands over HTTP. You send a POST request with your inputs and options, and get back processed video. No binary to install, no server to manage. &lt;a href="https://ffmpeg-micro.com" rel="noopener noreferrer"&gt;Get a free API key at ffmpeg-micro.com&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  How do I center text in FFmpeg drawtext?
&lt;/h3&gt;

&lt;p&gt;Set &lt;code&gt;x=(w-text_w)/2&lt;/code&gt; to center horizontally and &lt;code&gt;y=(h-text_h)/2&lt;/code&gt; to center vertically. The variables &lt;code&gt;w&lt;/code&gt; and &lt;code&gt;h&lt;/code&gt; are the video dimensions, while &lt;code&gt;text_w&lt;/code&gt; and &lt;code&gt;text_h&lt;/code&gt; are the rendered size of the text string.&lt;/p&gt;

&lt;h3&gt;
  
  
  What's the difference between fontfile and fontname in drawtext?
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;fontfile&lt;/code&gt; takes a direct file path to a &lt;code&gt;.ttf&lt;/code&gt; or &lt;code&gt;.otf&lt;/code&gt; font. &lt;code&gt;fontname&lt;/code&gt; references a font by its installed name and requires FFmpeg to be compiled with fontconfig. Use &lt;code&gt;fontfile&lt;/code&gt; if you need consistent results across environments.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I add a background box behind drawtext text?
&lt;/h3&gt;

&lt;p&gt;Add &lt;code&gt;box=1:boxcolor=black@0.5:boxborderw=10&lt;/code&gt; to your drawtext parameters. The &lt;code&gt;@0.5&lt;/code&gt; controls opacity (0 is fully transparent, 1 is fully opaque), and &lt;code&gt;boxborderw&lt;/code&gt; sets the padding in pixels around the text.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I show a timestamp on video with drawtext?
&lt;/h3&gt;

&lt;p&gt;Yes. Use &lt;code&gt;text='%{pts\:hms}'&lt;/code&gt; with &lt;code&gt;expansion=normal&lt;/code&gt; (the default). This renders the video's presentation timestamp in &lt;code&gt;HH:MM:SS&lt;/code&gt; format on every frame. Escape the colon with a backslash because drawtext uses colons as delimiters.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I make drawtext appear only for part of the video?
&lt;/h3&gt;

&lt;p&gt;Use the &lt;code&gt;enable&lt;/code&gt; parameter with a &lt;code&gt;between()&lt;/code&gt; expression. For example, &lt;code&gt;enable='between(t,2,8)'&lt;/code&gt; shows the text only from the 2-second mark to the 8-second mark. The variable &lt;code&gt;t&lt;/code&gt; is the current time in seconds.&lt;/p&gt;

</description>
      <category>ffmpeg</category>
      <category>video</category>
      <category>devops</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
