<?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 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>
    <item>
      <title>ffmpeg.wasm vs a Hosted FFmpeg API: Where Browser-Side Breaks</title>
      <dc:creator>Javid Jamae</dc:creator>
      <pubDate>Mon, 20 Jul 2026 16:45:42 +0000</pubDate>
      <link>https://dev.to/javidjamae/ffmpegwasm-vs-a-hosted-ffmpeg-api-where-browser-side-breaks-21k9</link>
      <guid>https://dev.to/javidjamae/ffmpegwasm-vs-a-hosted-ffmpeg-api-where-browser-side-breaks-21k9</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.ffmpeg-micro.com/blog/ffmpeg-wasm-vs-hosted-api" rel="noopener noreferrer"&gt;ffmpeg-micro.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;ffmpeg.wasm looks like magic the first time you see it. Run FFmpeg right in the browser. No server, no install, no infrastructure. Your user picks a file, you process it client-side, and nobody's video ever leaves their machine.&lt;/p&gt;

&lt;p&gt;Then someone uploads a 200MB file and the tab crashes.&lt;/p&gt;

&lt;h2&gt;
  
  
  What ffmpeg.wasm actually is
&lt;/h2&gt;

&lt;p&gt;ffmpeg.wasm is a WebAssembly port of FFmpeg that runs entirely in the browser. Your JavaScript can do this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;FFmpeg&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;@ffmpeg/ffmpeg&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&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;FFmpeg&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;ffmpeg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&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;ffmpeg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;input.mp4&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;videoData&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;ffmpeg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exec&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-i&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;input.mp4&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;-vf&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;scale=640:360&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;output.mp4&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;data&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;ffmpeg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;output.mp4&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No server call. No API key. The entire operation happens in the user's browser tab.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where ffmpeg.wasm works well
&lt;/h2&gt;

&lt;p&gt;For small, quick operations on small files:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Thumbnail extraction&lt;/strong&gt; from short clips (under 50MB)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Format detection&lt;/strong&gt; and metadata inspection&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Quick trims&lt;/strong&gt; of short videos&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Privacy-sensitive workflows&lt;/strong&gt; where video can't leave the device&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The five walls you'll hit
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Memory ceiling
&lt;/h3&gt;

&lt;p&gt;A 500MB video needs at least 500MB of RAM just for the input. The practical limit is roughly 100-200MB per file on desktop, less on mobile.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Single-threaded performance
&lt;/h3&gt;

&lt;p&gt;A transcode that takes 30 seconds on a server can take 5-10 minutes in the browser. Multi-threaded builds require &lt;code&gt;SharedArrayBuffer&lt;/code&gt; and specific CORS headers that break third-party embeds.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Codec limitations
&lt;/h3&gt;

&lt;p&gt;The Wasm build doesn't include every codec. Licensing and binary size constraints mean you get a subset.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Mobile is unreliable
&lt;/h3&gt;

&lt;p&gt;Mobile browsers have tighter memory limits and more aggressive tab killing. A transcode that works on your MacBook will crash Safari on an iPhone 12.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. No batch processing
&lt;/h3&gt;

&lt;p&gt;Processing 50 files means the user keeps the tab open. Any browser crash kills the entire batch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Comparison table
&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;ffmpeg.wasm&lt;/th&gt;
&lt;th&gt;Hosted FFmpeg API&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Max file size&lt;/td&gt;
&lt;td&gt;~100-200MB&lt;/td&gt;
&lt;td&gt;No practical limit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Processing speed&lt;/td&gt;
&lt;td&gt;5-10x slower&lt;/td&gt;
&lt;td&gt;Server-grade&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mobile support&lt;/td&gt;
&lt;td&gt;Unreliable&lt;/td&gt;
&lt;td&gt;Works everywhere&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Batch processing&lt;/td&gt;
&lt;td&gt;Manual, fragile&lt;/td&gt;
&lt;td&gt;Built-in queuing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Privacy&lt;/td&gt;
&lt;td&gt;Video stays on device&lt;/td&gt;
&lt;td&gt;Video uploaded to server&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost&lt;/td&gt;
&lt;td&gt;Free (client CPU)&lt;/td&gt;
&lt;td&gt;Pay per minute processed&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The hybrid approach
&lt;/h2&gt;

&lt;p&gt;Use ffmpeg.wasm for lightweight client-side ops and route heavier work to an API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;FILE_SIZE_THRESHOLD&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// 50MB&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;processVideo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&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;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;size&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;FILE_SIZE_THRESHOLD&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="nf"&gt;processClientSide&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// ffmpeg.wasm&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;processServerSide&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// FFmpeg Micro API&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read the &lt;a href="https://www.ffmpeg-micro.com/blog/ffmpeg-wasm-vs-hosted-api" rel="noopener noreferrer"&gt;full post with detailed analysis&lt;/a&gt; on ffmpeg-micro.com.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webassembly</category>
      <category>ffmpeg</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How to Remove Audio from Video with FFmpeg</title>
      <dc:creator>Javid Jamae</dc:creator>
      <pubDate>Mon, 20 Jul 2026 16:45:15 +0000</pubDate>
      <link>https://dev.to/javidjamae/how-to-remove-audio-from-video-with-ffmpeg-2a30</link>
      <guid>https://dev.to/javidjamae/how-to-remove-audio-from-video-with-ffmpeg-2a30</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.ffmpeg-micro.com/blog/ffmpeg-remove-audio-from-video" rel="noopener noreferrer"&gt;ffmpeg-micro.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;You have a product demo with terrible microphone hum, a social clip that needs a music swap, or a background loop that should just be silent. The fix in FFmpeg is one flag: &lt;code&gt;-an&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This post covers three ways to strip audio from video with FFmpeg, from the simplest one-liner to batch processing hundreds of files.&lt;/p&gt;

&lt;h2&gt;
  
  
  The &lt;code&gt;-an&lt;/code&gt; flag: remove all audio in one shot
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;-an&lt;/code&gt; flag tells FFmpeg to drop every audio stream from the output. No re-encoding of the audio track, no codec selection. It just disappears.&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;-an&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt;:v copy output-silent.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is it. The video stream gets copied byte-for-byte (&lt;code&gt;-c:v copy&lt;/code&gt;), so the operation finishes in seconds regardless of file length.&lt;/p&gt;

&lt;p&gt;Use &lt;code&gt;-c:v copy&lt;/code&gt; whenever you can. It avoids re-encoding video, which means no quality loss and fast execution.&lt;/p&gt;

&lt;h3&gt;
  
  
  When &lt;code&gt;-c:v copy&lt;/code&gt; won't work
&lt;/h3&gt;

&lt;p&gt;If you are also applying video filters (scaling, cropping, watermarking), you need to re-encode:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffmpeg &lt;span class="nt"&gt;-i&lt;/span&gt; input.mp4 &lt;span class="nt"&gt;-an&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;-pix_fmt&lt;/span&gt; yuv420p output-silent.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Replace audio with silence instead of removing it
&lt;/h2&gt;

&lt;p&gt;Some platforms reject videos without an audio track. Instagram, TikTok, and certain ad networks expect audio to exist, even if it is empty:&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;-f&lt;/span&gt; lavfi &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="nv"&gt;anullsrc&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;r&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;44100:cl&lt;span class="o"&gt;=&lt;/span&gt;stereo &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-silent-track.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output file has a valid audio track that happens to be completely silent.&lt;/p&gt;

&lt;h2&gt;
  
  
  Batch-process: mute all videos in a folder
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; muted
&lt;span class="k"&gt;for &lt;/span&gt;f &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt;.mp4&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;ffmpeg &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-an&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt;:v copy &lt;span class="s2"&gt;"muted/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;f&lt;/span&gt;&lt;span class="p"&gt;%.mp4&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-silent.mp4"&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;"The output still has audio."&lt;/strong&gt; Use &lt;code&gt;-c:v copy&lt;/code&gt; not &lt;code&gt;-c copy&lt;/code&gt;. The shorthand &lt;code&gt;-c copy&lt;/code&gt; copies ALL streams including audio.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Instagram/TikTok rejects the video."&lt;/strong&gt; Use the silent audio track approach instead of &lt;code&gt;-an&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"My player shows no duration."&lt;/strong&gt; Add &lt;code&gt;-movflags +faststart&lt;/code&gt; to the command.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;&lt;strong&gt;Does &lt;code&gt;-an&lt;/code&gt; remove audio without re-encoding?&lt;/strong&gt; Yes. With &lt;code&gt;-c:v copy&lt;/code&gt;, the video is copied byte-for-byte. Only audio is dropped.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is the difference between &lt;code&gt;-an&lt;/code&gt; and &lt;code&gt;-vn&lt;/code&gt;?&lt;/strong&gt; &lt;code&gt;-an&lt;/code&gt; removes audio, keeps video. &lt;code&gt;-vn&lt;/code&gt; does the opposite.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Will removing audio reduce file size?&lt;/strong&gt; Yes, by the size of the audio stream (typically 5-15% of total).&lt;/p&gt;

&lt;p&gt;Read the &lt;a href="https://www.ffmpeg-micro.com/blog/ffmpeg-remove-audio-from-video" rel="noopener noreferrer"&gt;full post with API examples&lt;/a&gt; on ffmpeg-micro.com.&lt;/p&gt;

</description>
      <category>ffmpeg</category>
      <category>video</category>
      <category>tutorial</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Running FFmpeg on Heroku: The Ephemeral-Filesystem Trap (and the Fix)</title>
      <dc:creator>Javid Jamae</dc:creator>
      <pubDate>Sun, 19 Jul 2026 17:41:51 +0000</pubDate>
      <link>https://dev.to/javidjamae/running-ffmpeg-on-heroku-the-ephemeral-filesystem-trap-and-the-fix-4i39</link>
      <guid>https://dev.to/javidjamae/running-ffmpeg-on-heroku-the-ephemeral-filesystem-trap-and-the-fix-4i39</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.ffmpeg-micro.com/blog/ffmpeg-heroku-ephemeral-filesystem-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 push to Heroku, trigger a transcode, and the job vanishes. No output file. No error you can Google. Just a request that hangs and then dies.&lt;/p&gt;

&lt;p&gt;This isn't a config mistake. Heroku's architecture is fundamentally hostile to long-running FFmpeg jobs, and the buildpack README won't tell you that.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three ways Heroku kills your FFmpeg jobs
&lt;/h2&gt;

&lt;p&gt;Most developers hit one of these. Some hit all three in the same week.&lt;/p&gt;

&lt;h3&gt;
  
  
  The ephemeral filesystem eats your output
&lt;/h3&gt;

&lt;p&gt;Heroku dynos don't have persistent storage. Every file you write goes to &lt;code&gt;/tmp&lt;/code&gt;, and &lt;code&gt;/tmp&lt;/code&gt; gets wiped on every dyno restart. Deploys, config changes, Heroku's own dyno cycling (which happens at least once every 24 hours). All of it triggers a restart.&lt;/p&gt;

&lt;p&gt;If your FFmpeg job takes 4 minutes to transcode a video and Heroku cycles the dyno at minute 3, the output file is gone. No partial recovery. No resume. The work just disappears.&lt;/p&gt;

&lt;p&gt;This is the one that burns people the hardest because it works fine during testing. Short test files finish before the cycle hits. Production files with real-world durations don't.&lt;/p&gt;

&lt;h3&gt;
  
  
  The buildpack bloats your slug
&lt;/h3&gt;

&lt;p&gt;The standard &lt;code&gt;heroku-buildpack-ffmpeg-latest&lt;/code&gt; adds roughly 80MB to your slug size. Heroku caps slugs at 500MB. That sounds like plenty until you account for your Node modules, your Python dependencies, your static assets, and whatever else your app ships.&lt;/p&gt;

&lt;p&gt;A mid-sized Rails or Next.js app can easily sit at 350-400MB before adding FFmpeg. Drop in the buildpack and you're flirting with the limit. Add a second binary or a few large dependencies and the deploy fails outright.&lt;/p&gt;

&lt;p&gt;You can strip FFmpeg down to only the codecs you need, but that means maintaining a custom buildpack fork. Now you're debugging FFmpeg compilation flags on top of everything else.&lt;/p&gt;

&lt;h3&gt;
  
  
  The 30-second timeout kills long encodes
&lt;/h3&gt;

&lt;p&gt;Heroku web dynos enforce a hard 30-second request timeout at the router level. This isn't configurable. If your HTTP request doesn't return a response within 30 seconds, Heroku's router drops the connection and returns an H12 error.&lt;/p&gt;

&lt;p&gt;FFmpeg transcoding a 5-minute video at reasonable quality takes longer than 30 seconds. Every time.&lt;/p&gt;

&lt;p&gt;The standard workaround is moving FFmpeg to a worker dyno with a job queue. That means adding Redis, adding a queue library (Sidekiq for Ruby, Bull for Node), writing the queue/worker plumbing, and handling job status polling from your frontend. Your "just run FFmpeg" feature now requires three additional infrastructure components and a new failure surface.&lt;/p&gt;

&lt;p&gt;Worker dynos don't have the 30-second timeout, but they still have the ephemeral filesystem problem. You've solved one issue and kept another.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest fork: harden it or offload it
&lt;/h2&gt;

&lt;p&gt;You have two real options. Neither is wrong, but they lead to very different maintenance burdens.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option A: Harden the Heroku setup
&lt;/h3&gt;

&lt;p&gt;This means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Streaming FFmpeg output directly to S3 instead of writing to &lt;code&gt;/tmp&lt;/code&gt; (using pipes or chunked uploads)&lt;/li&gt;
&lt;li&gt;Running FFmpeg on worker dynos with Redis and a job queue&lt;/li&gt;
&lt;li&gt;Pinning your buildpack to a specific FFmpeg version so deploys don't break when upstream changes&lt;/li&gt;
&lt;li&gt;Building health checks and retry logic for jobs that die mid-transcode&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This works. Teams do run FFmpeg on Heroku in production. But you're maintaining a distributed video processing pipeline on top of an app platform that wasn't designed for it. Every piece (S3 streaming, queue infrastructure, retry logic, buildpack maintenance) is another thing that can break at 2am.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option B: Keep the dyno stateless
&lt;/h3&gt;

&lt;p&gt;Instead of fighting Heroku's architecture, work with it. Keep your dyno doing what dynos are good at (serving requests, running application logic) and send the FFmpeg work to an API that handles the filesystem, the timeouts, and the binary management for you.&lt;/p&gt;

&lt;p&gt;Your Heroku app sends a POST request with the input file URL and the FFmpeg options. The transcode runs on infrastructure built for it. Your dyno stays stateless, your slug stays small, and you don't need Redis.&lt;/p&gt;

&lt;p&gt;A single API call from your Heroku app replaces the entire worker dyno + queue + S3 pipeline:&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-s3-bucket.amazonaws.com/input.mp4"}],
    "outputFormat": "mp4",
    "options": [
      {"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 input file gets pulled from your S3 bucket (or any public URL). The transcode runs on FFmpeg Micro's infrastructure. You poll for completion or set up a webhook. No buildpack, no worker dyno, no ephemeral filesystem.&lt;/p&gt;

&lt;p&gt;Your Heroku app stays a Heroku app. The video processing happens somewhere that won't wipe your files mid-job.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real cost isn't the first deploy
&lt;/h2&gt;

&lt;p&gt;Getting FFmpeg running on Heroku isn't the hard part. Keeping it running is. The dyno cycling, the slug creep as your app grows, the queue jobs that silently fail, the buildpack that pulls a new FFmpeg version and breaks your encoding flags.&lt;/p&gt;

&lt;p&gt;Every hour you spend debugging Heroku-specific FFmpeg failures is an hour you're not building the feature your users actually asked for.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Why does FFmpeg work locally but fail on Heroku?
&lt;/h3&gt;

&lt;p&gt;Local machines have persistent filesystems and no request timeouts. Heroku dynos have ephemeral storage that wipes on restart and a 30-second router timeout on web dynos. FFmpeg jobs that finish fine on your laptop will fail in production if they write to disk or take longer than 30 seconds.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I increase the Heroku dyno timeout for video processing?
&lt;/h3&gt;

&lt;p&gt;No. The 30-second timeout on web dynos is enforced at the Heroku router and is not configurable. Your options are moving FFmpeg to a background worker dyno with a job queue (Redis + Sidekiq or Bull) or offloading the transcode to an external API.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is the best FFmpeg buildpack for Heroku?
&lt;/h3&gt;

&lt;p&gt;The most common is &lt;code&gt;heroku-buildpack-ffmpeg-latest&lt;/code&gt;, which pulls the current FFmpeg build. It adds roughly 80MB to your slug. For production use, pin it to a specific FFmpeg version so your deploys don't break when upstream releases a new build with different codec defaults.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I handle large video files on Heroku's ephemeral filesystem?
&lt;/h3&gt;

&lt;p&gt;Don't write to the local filesystem for anything you need to keep. Stream FFmpeg output directly to S3 using pipes, or use a hosted FFmpeg API that handles storage independently. Any file written to &lt;code&gt;/tmp&lt;/code&gt; on a Heroku dyno will be lost on the next restart, which happens at least once every 24 hours.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is Heroku a good platform for video processing?
&lt;/h3&gt;

&lt;p&gt;Heroku is built for stateless web applications, not compute-heavy media processing. You can make it work with worker dynos, job queues, and external storage, but you're adding significant infrastructure complexity. If video processing is a core feature of your app, purpose-built infrastructure (or a hosted API) will save you maintenance time.&lt;/p&gt;

</description>
      <category>ffmpeg</category>
      <category>heroku</category>
      <category>devops</category>
      <category>video</category>
    </item>
    <item>
      <title>How to Flip a Video with FFmpeg (hflip and vflip Filters)</title>
      <dc:creator>Javid Jamae</dc:creator>
      <pubDate>Sun, 19 Jul 2026 17:41:47 +0000</pubDate>
      <link>https://dev.to/javidjamae/how-to-flip-a-video-with-ffmpeg-hflip-and-vflip-filters-4d29</link>
      <guid>https://dev.to/javidjamae/how-to-flip-a-video-with-ffmpeg-hflip-and-vflip-filters-4d29</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.ffmpeg-micro.com/blog/ffmpeg-flip-video-hflip-vflip" rel="noopener noreferrer"&gt;ffmpeg-micro.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Front-facing camera footage is mirrored. Text reads backwards, logos are reversed, and the left-right orientation is wrong. If you're processing selfie video, screen recordings from mobile devices, or any source that captured a mirror image, you need to flip it before the output goes anywhere useful.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;To flip a video horizontally with FFmpeg, use the &lt;code&gt;hflip&lt;/code&gt; video filter:&lt;/strong&gt; &lt;code&gt;ffmpeg -i input.mp4 -vf hflip -c:a copy output.mp4&lt;/code&gt;. For a vertical flip, replace &lt;code&gt;hflip&lt;/code&gt; with &lt;code&gt;vflip&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Flip vs. Rotate
&lt;/h2&gt;

&lt;p&gt;This trips people up constantly. A flip mirrors the frame along an axis. A rotation spins it. Horizontal flip (&lt;code&gt;hflip&lt;/code&gt;) mirrors left to right, like looking in a mirror. A 90-degree rotation turns the entire frame sideways, changing the orientation and usually the resolution dimensions too.&lt;/p&gt;

&lt;p&gt;If someone tells you their video is "upside down," they probably want &lt;code&gt;vflip&lt;/code&gt; or a 180-degree rotation. A 180-degree rotation is actually identical to combining &lt;code&gt;hflip&lt;/code&gt; and &lt;code&gt;vflip&lt;/code&gt; together, which we'll cover below.&lt;/p&gt;

&lt;h2&gt;
  
  
  Horizontal Flip with &lt;code&gt;hflip&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;hflip&lt;/code&gt; filter mirrors the video along the vertical axis. Every frame gets its pixels reversed left-to-right. This is the filter you want for selfie correction, fixing mirrored webcam footage, or creating a mirror effect for social media content.&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; hflip &lt;span class="nt"&gt;-c&lt;/span&gt;:a copy output.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;-vf hflip&lt;/code&gt; flag applies the horizontal flip filter. The &lt;code&gt;-c:a copy&lt;/code&gt; flag passes the audio stream through without re-encoding, which saves time and avoids any quality loss on the audio side.&lt;/p&gt;

&lt;p&gt;The video stream does get re-encoded. FFmpeg has to decode each frame, flip the pixel data, and encode it again. You can't flip a video with stream copy on the video track because the actual pixel data needs to change.&lt;/p&gt;

&lt;h2&gt;
  
  
  Vertical Flip with &lt;code&gt;vflip&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;vflip&lt;/code&gt; filter mirrors the video along the horizontal axis, flipping it upside down. Every frame gets its rows of pixels reversed top-to-bottom.&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; vflip &lt;span class="nt"&gt;-c&lt;/span&gt;:a copy output.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll run into vertical flip less often than horizontal. The most common use case is correcting footage from cameras mounted upside down, like security cameras or action cameras with inverted mounts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Combining &lt;code&gt;hflip&lt;/code&gt; and &lt;code&gt;vflip&lt;/code&gt; for 180-Degree Rotation
&lt;/h2&gt;

&lt;p&gt;Chaining both filters together gives you the equivalent of a 180-degree rotation. The frame gets mirrored horizontally and vertically, which produces the same result as spinning it halfway around.&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;"hflip,vflip"&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt;:a copy output.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The filter chain runs left to right. FFmpeg applies &lt;code&gt;hflip&lt;/code&gt; first, then &lt;code&gt;vflip&lt;/code&gt; to the already-flipped output. The order doesn't matter for this specific combination since the end result is the same either way. It does matter when you start mixing flips with other filters like scaling or cropping.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preserving Audio
&lt;/h2&gt;

&lt;p&gt;Both examples above use &lt;code&gt;-c:a copy&lt;/code&gt; to pass the audio through untouched. This is almost always what you want. Flipping video has nothing to do with the audio stream, so re-encoding it just wastes time and adds a generation of lossy compression for no reason.&lt;/p&gt;

&lt;p&gt;If you leave out &lt;code&gt;-c:a copy&lt;/code&gt;, FFmpeg will re-encode the audio using its default codec for the output container. For MP4, that's usually AAC. The quality difference on a single re-encode is minimal, but there's no upside when stream copy is available.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Forgetting that video gets re-encoded
&lt;/h3&gt;

&lt;p&gt;You can't use &lt;code&gt;-c:v copy&lt;/code&gt; with flip filters. The pixel data is changing, so FFmpeg needs to decode and re-encode the video stream. If you try &lt;code&gt;-c:v copy -vf hflip&lt;/code&gt;, FFmpeg will throw an error. This means your output file size and quality depend on your encoder settings. If you need to match the original quality closely, set your bitrate or CRF value explicitly:&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; hflip &lt;span class="nt"&gt;-c&lt;/span&gt;:v libx264 &lt;span class="nt"&gt;-crf&lt;/span&gt; 18 &lt;span class="nt"&gt;-c&lt;/span&gt;:a copy output.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Confusing flip with transpose
&lt;/h3&gt;

&lt;p&gt;FFmpeg also has a &lt;code&gt;transpose&lt;/code&gt; filter that combines rotation with an optional flip. If you use &lt;code&gt;transpose=1&lt;/code&gt; expecting a simple mirror, you'll get a 90-degree clockwise rotation instead. Stick with &lt;code&gt;hflip&lt;/code&gt; and &lt;code&gt;vflip&lt;/code&gt; when you just need mirroring. Use &lt;code&gt;transpose&lt;/code&gt; only when you actually need to rotate by 90 or 270 degrees.&lt;/p&gt;

&lt;h3&gt;
  
  
  Not accounting for metadata rotation
&lt;/h3&gt;

&lt;p&gt;Some mobile video files store rotation in metadata rather than in the actual pixel data. FFmpeg auto-rotates based on metadata by default. If your input has a rotation flag in its metadata and you apply &lt;code&gt;hflip&lt;/code&gt;, you get unexpected results because FFmpeg applies the auto-rotation before your filter. To disable auto-rotation and work with the raw pixel data, add &lt;code&gt;-noautorotate&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;-noautorotate&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; input.mp4 &lt;span class="nt"&gt;-vf&lt;/span&gt; hflip &lt;span class="nt"&gt;-c&lt;/span&gt;:a copy output.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Flipping Video at Scale with the FFmpeg Micro API
&lt;/h2&gt;

&lt;p&gt;Running these commands locally works fine for one-off jobs. When you need to flip hundreds or thousands of videos programmatically, shelling out to FFmpeg from your application code gets messy fast. You're managing processes, handling failures, provisioning compute, and babysitting encoding queues.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://ffmpeg-micro.com" rel="noopener noreferrer"&gt;FFmpeg Micro&lt;/a&gt; gives you a REST API that runs FFmpeg filters in the cloud, including &lt;code&gt;hflip&lt;/code&gt; and &lt;code&gt;vflip&lt;/code&gt;. Send a POST request with your filter chain and get back the processed video:&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/input.mp4"}],
    "outputFormat": "mp4",
    "options": [
      {"option": "-vf", "argument": "hflip"},
      {"option": "-c:a", "argument": "copy"}
    ]
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No infrastructure to manage. No FFmpeg installation to maintain. Just an API call with the same filter syntax you already know.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  How do I mirror a video in FFmpeg?
&lt;/h3&gt;

&lt;p&gt;Use the &lt;code&gt;hflip&lt;/code&gt; video filter to mirror a video horizontally in FFmpeg: &lt;code&gt;ffmpeg -i input.mp4 -vf hflip -c:a copy output.mp4&lt;/code&gt;. This reverses the frame left-to-right, producing a mirror image of the original footage.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is the difference between hflip and vflip in FFmpeg?
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;hflip&lt;/code&gt; filter mirrors the video along the vertical axis, flipping it left-to-right like a mirror reflection. The &lt;code&gt;vflip&lt;/code&gt; filter mirrors the video along the horizontal axis, flipping it upside down. You can combine both with &lt;code&gt;-vf "hflip,vflip"&lt;/code&gt; to get a 180-degree rotation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I flip a video without re-encoding the audio?
&lt;/h3&gt;

&lt;p&gt;Yes. Add &lt;code&gt;-c:a copy&lt;/code&gt; to your FFmpeg command to pass the audio stream through without re-encoding. The video stream will still be re-encoded because the pixel data changes during a flip, but the audio stays untouched.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I rotate a video 180 degrees with FFmpeg?
&lt;/h3&gt;

&lt;p&gt;Combine &lt;code&gt;hflip&lt;/code&gt; and &lt;code&gt;vflip&lt;/code&gt; filters: &lt;code&gt;ffmpeg -i input.mp4 -vf "hflip,vflip" -c:a copy output.mp4&lt;/code&gt;. This mirrors the frame both horizontally and vertically, which produces the same result as a 180-degree rotation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does flipping a video affect quality?
&lt;/h3&gt;

&lt;p&gt;Flipping requires FFmpeg to re-encode the video stream, which can reduce quality depending on your encoder settings. To minimize quality loss, use a low CRF value (like 18) with libx264. The audio stream isn't affected if you use &lt;code&gt;-c:a copy&lt;/code&gt;.&lt;/p&gt;

</description>
      <category>ffmpeg</category>
      <category>video</category>
      <category>tutorial</category>
      <category>api</category>
    </item>
    <item>
      <title>Optimize Videos for Framer with FFmpeg Micro</title>
      <dc:creator>Javid Jamae</dc:creator>
      <pubDate>Sat, 18 Jul 2026 15:59:34 +0000</pubDate>
      <link>https://dev.to/javidjamae/optimize-videos-for-framer-with-ffmpeg-micro-4p6d</link>
      <guid>https://dev.to/javidjamae/optimize-videos-for-framer-with-ffmpeg-micro-4p6d</guid>
      <description>&lt;p&gt;Originally published at &lt;a href="https://www.ffmpeg-micro.com/blog/optimize-videos-framer-ffmpeg-micro" rel="noopener noreferrer"&gt;ffmpeg-micro.com&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Framer doesn't process video. Drop a 200MB ProRes file into a Framer project and that's exactly what visitors download. No compression, no format conversion, no codec negotiation. Your marketing site loads like it's 2008.&lt;/p&gt;

&lt;p&gt;Framer's own documentation tells you to run FFmpeg commands before uploading. That's the right advice. But most Framer users are designers and founders, not people who want to install a command-line tool and memorize codec flags.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Convert your video to H.264 MP4, compress it with CRF, and apply the &lt;code&gt;+faststart&lt;/code&gt; flag before uploading to Framer. FFmpeg Micro's REST API does all three in one call. No FFmpeg installation required.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Why Framer Can't Handle This for You
&lt;/h2&gt;

&lt;p&gt;Framer is a design tool, not a media pipeline. When you upload a video, Framer stores it on its CDN without any transcoding. That means whatever codec, resolution, and file size you upload is what every visitor gets.&lt;/p&gt;

&lt;p&gt;This creates three problems.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;File size.&lt;/strong&gt; Framer caches assets in the browser, but most browsers cap that cache at around 5 MB per resource. A 50 MB hero video won't stay cached. Every repeat visit triggers a fresh download.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Codec compatibility.&lt;/strong&gt; Safari won't play WebM. Older browsers choke on HEVC. If you shot on an iPhone (HEVC by default) or exported from Premiere as WebM, a chunk of your audience sees a broken player.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Progressive download.&lt;/strong&gt; Without the &lt;code&gt;faststart&lt;/code&gt; flag, MP4 files store their metadata (the moov atom) at the end of the file. The browser has to download the entire file before it can start playback. On a landing page, that delay kills conversions.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Three Things Every Framer Video Needs
&lt;/h2&gt;

&lt;p&gt;Before uploading any video to Framer, you need to do three things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Compress it.&lt;/strong&gt; Reduce the file size using CRF (Constant Rate Factor) so the visual quality stays high while the bitrate drops. CRF 23 is a good starting point for web video.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Convert to H.264 MP4.&lt;/strong&gt; This is the one format that plays everywhere. Safari, Chrome, Firefox, Edge, iOS, Android. No exceptions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add faststart.&lt;/strong&gt; The &lt;code&gt;-movflags +faststart&lt;/code&gt; flag moves the moov atom to the beginning of the file so browsers can start playback immediately, even before the full download finishes.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The FFmpeg CLI Way
&lt;/h2&gt;

&lt;p&gt;If you have FFmpeg installed locally, this single command handles all three:&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.mov &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;-c&lt;/span&gt;:a aac &lt;span class="nt"&gt;-movflags&lt;/span&gt; +faststart output.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That works. But it requires installing FFmpeg, knowing the right flags, and running it manually every time you want to update a video on your Framer site. If you're processing multiple hero sections, testimonial clips, or product demos, it gets old fast.&lt;/p&gt;

&lt;h2&gt;
  
  
  The API Way (FFmpeg Micro)
&lt;/h2&gt;

&lt;p&gt;FFmpeg Micro is a hosted REST API that runs FFmpeg in the cloud. You send it a video URL, tell it what you want, and get back a processed file. No binary to install. No server to manage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Simple mode&lt;/strong&gt; uses presets:&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 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/raw-hero.mov"}],
    "outputFormat": "mp4",
    "preset": {"quality": "high", "resolution": "1080p"}
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Advanced mode&lt;/strong&gt; gives you full control over codec, CRF, and faststart:&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 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/raw-hero.mov"}],
    "outputFormat": "mp4",
    "options": [
      {"option": "-c:v", "argument": "libx264"},
      {"option": "-crf", "argument": "23"},
      {"option": "-movflags", "argument": "+faststart"},
      {"option": "-c:a", "argument": "aac"}
    ]
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The API returns a job ID. Poll the status endpoint until it completes, then grab the download URL.&lt;/p&gt;

&lt;p&gt;Here's the full flow in JavaScript:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;API_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;your-api-key&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// 1. Start the transcode&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="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;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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://example.com/raw-hero.mov&lt;/span&gt;&lt;span class="dl"&gt;"&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;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="s2"&gt;-movflags&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;+faststart&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;-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="s2"&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="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;json&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

&lt;span class="c1"&gt;// 2. Poll until complete&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="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="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;5000&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;check&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;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="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;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;check&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;// 3. Get the download URL&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;download&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;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="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;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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Download your optimized video:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;download&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Upload the resulting file to Framer and you're done. Compressed H.264 MP4 with faststart, ready for every browser.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common Framer Video Problems (and Fixes)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Video won't play in Safari.&lt;/strong&gt; Safari doesn't support WebM or VP9. Convert to H.264 MP4 using the options above. This is the single most common Framer video complaint.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hero video loads slowly.&lt;/strong&gt; Two culprits: the file is too large, or it's missing the faststart flag. Compress with CRF 23 and make sure &lt;code&gt;-movflags +faststart&lt;/code&gt; is in your options. For above-the-fold hero videos, aim for under 5 MB.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Video looks blurry after upload.&lt;/strong&gt; Framer didn't compress it. You did, probably too aggressively. Lower the CRF number (18-20 gives near-lossless quality at a larger file size) or bump the resolution preset to 1080p.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;File is still too large after compression.&lt;/strong&gt; Drop the resolution. A 720p video at CRF 23 is often under 3 MB for a 15-second clip. On a marketing site, most visitors won't notice the resolution difference on a background video.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Does Framer compress video automatically?
&lt;/h3&gt;

&lt;p&gt;No. Framer serves videos exactly as uploaded. There is no server-side transcoding, compression, or format conversion. You need to optimize video files before uploading them to your Framer project.&lt;/p&gt;

&lt;h3&gt;
  
  
  What video format works best for Framer sites?
&lt;/h3&gt;

&lt;p&gt;H.264 MP4 with the faststart flag is the best format for Framer. It plays in every major browser including Safari on iOS, supports progressive download for fast playback, and compresses well for web delivery.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why won't my video play in Safari on a Framer site?
&lt;/h3&gt;

&lt;p&gt;Safari does not support WebM or VP9 video codecs. If your video was exported as WebM or uses HEVC without a fallback, Safari will show a blank player or an error. Converting to H.264 MP4 fixes this across all browsers.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I optimize video for Framer without installing FFmpeg?
&lt;/h3&gt;

&lt;p&gt;Yes. FFmpeg Micro is a cloud API that runs FFmpeg remotely. You send a video URL via HTTP request and receive a compressed, converted file back. No installation, no command line, no server. A &lt;a href="https://www.ffmpeg-micro.com/pricing" rel="noopener noreferrer"&gt;free tier&lt;/a&gt; is available to get started.&lt;/p&gt;

&lt;h3&gt;
  
  
  What CRF value should I use for Framer hero videos?
&lt;/h3&gt;

&lt;p&gt;CRF 23 is a good default for web video. It produces a significant file size reduction with minimal visible quality loss. For hero sections where quality matters more, try CRF 18-20. For background videos where some softness is acceptable, CRF 28 keeps files very small.&lt;/p&gt;

</description>
      <category>ffmpeg</category>
      <category>framer</category>
      <category>webdev</category>
      <category>video</category>
    </item>
    <item>
      <title>How to Use FFmpeg with Bun</title>
      <dc:creator>Javid Jamae</dc:creator>
      <pubDate>Sat, 18 Jul 2026 15:59:17 +0000</pubDate>
      <link>https://dev.to/javidjamae/how-to-use-ffmpeg-with-bun-44og</link>
      <guid>https://dev.to/javidjamae/how-to-use-ffmpeg-with-bun-44og</guid>
      <description>&lt;p&gt;Originally published at &lt;a href="https://www.ffmpeg-micro.com/blog/how-to-use-ffmpeg-with-bun" rel="noopener noreferrer"&gt;ffmpeg-micro.com&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;You're building something in Bun and you need video processing. Thumbnail generation, format conversion, transcoding user uploads. You search for "ffmpeg bun" and find a bunch of Node.js tutorials that mostly work but don't use any of Bun's native APIs. You're stuck shimming &lt;code&gt;child_process&lt;/code&gt; when Bun has its own subprocess interface built in.&lt;/p&gt;

&lt;p&gt;Bun's selling points are speed and developer experience. Native TypeScript, fast startup, a built-in bundler. But when it comes to calling FFmpeg, you still need to figure out how to bridge your code to a native binary. Or avoid the binary entirely.&lt;/p&gt;

&lt;p&gt;There are three realistic paths. Each works, each has real tradeoffs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using Bun.spawn with a Local FFmpeg Binary
&lt;/h2&gt;

&lt;p&gt;If FFmpeg is installed on the machine, you can call it directly using &lt;code&gt;Bun.spawn&lt;/code&gt;. This is Bun's native subprocess API. It's similar in spirit to Node's &lt;code&gt;child_process.spawn&lt;/code&gt;, but the interface is cleaner and returns a &lt;code&gt;Subprocess&lt;/code&gt; object you can &lt;code&gt;await&lt;/code&gt; directly.&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;proc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Bun&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;spawn&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;ffmpeg&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;-i&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;input.mp4&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;-c:v&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;libx264&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;-crf&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;23&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;-preset&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;medium&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;-c:a&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;aac&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;-b:a&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;192k&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;output.mp4&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="na"&gt;stderr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pipe&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;exitCode&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;proc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exited&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;exitCode&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="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stderr&lt;/span&gt; &lt;span class="o"&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;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;proc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stderr&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="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&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;FFmpeg 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;stderr&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Transcoding complete&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No permission flags needed. Unlike Deno, Bun doesn't sandbox subprocess execution. You call &lt;code&gt;Bun.spawn&lt;/code&gt; and it runs. That makes local development faster but also means your code has full system access by default.&lt;/p&gt;

&lt;p&gt;This approach works well for local scripts, CLI tools, and servers where you control the environment. But it breaks down in production. You need FFmpeg installed on every machine. Most serverless and edge runtimes don't support spawning native binaries. And you're responsible for managing FFmpeg versions, codec libraries, and OS-level dependencies yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using FFmpeg WASM in Bun
&lt;/h2&gt;

&lt;p&gt;ffmpeg.wasm compiles FFmpeg to WebAssembly, so it runs without a native binary. Bun's Node.js compatibility means it just works:&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;FFmpeg&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;@ffmpeg/ffmpeg&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;fetchFile&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;@ffmpeg/util&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&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;FFmpeg&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;ffmpeg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&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;ffmpeg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;input.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="nf"&gt;fetchFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./input.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;ffmpeg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exec&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;-i&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;input.mp4&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;-c:v&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;libx264&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;output.mp4&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;data&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;ffmpeg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;output.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;Bun&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;output.mp4&lt;/span&gt;&lt;span class="dl"&gt;"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No FFmpeg binary needed. No system dependencies. But the tradeoffs are significant. Processing is 10-20x slower than native FFmpeg because WebAssembly can't match native CPU performance for heavy compute work. Memory is constrained. Not all codecs are available in the WASM build. And large files (anything over a few hundred MB) will likely crash.&lt;/p&gt;

&lt;p&gt;ffmpeg.wasm is fine for lightweight operations like extracting a single frame or trimming a short clip. Don't try to transcode a full-length video with it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using the FFmpeg Micro Cloud API
&lt;/h2&gt;

&lt;p&gt;If you don't want to manage FFmpeg binaries or accept WASM performance penalties, a cloud API is the cleanest path. FFmpeg Micro processes video through standard HTTP requests, which Bun handles natively with &lt;code&gt;fetch()&lt;/code&gt;. No npm packages. No binary dependencies. Just HTTP.&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;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="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;Bun&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://example.com/video.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;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="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;`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; 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="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One &lt;code&gt;fetch&lt;/code&gt; call. FFmpeg Micro handles the transcoding infrastructure, codec management, and scaling. Your Bun app just sends an HTTP request.&lt;/p&gt;

&lt;p&gt;For advanced use cases where you need specific FFmpeg flags, use the &lt;code&gt;options&lt;/code&gt; array instead of presets:&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;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="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;Bun&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://example.com/video.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;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;webm&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;libvpx-vp9&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;30&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;-b: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;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="s2"&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="s2"&gt;libopus&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Polling for Completion
&lt;/h3&gt;

&lt;p&gt;Transcodes are async. Poll the status endpoint until the job 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="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;waitForTranscode&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="kr"&gt;string&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="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&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;unknown&lt;/span&gt;&lt;span class="o"&gt;&amp;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;apiKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Bun&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;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&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;res&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="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;apiKey&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;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;res&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="s2"&gt;completed&lt;/span&gt;&lt;span class="dl"&gt;"&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="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="nx"&gt;job&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="nx"&gt;Bun&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&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="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;Once completed, grab the download URL:&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;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;apiKey&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;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="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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Download:&lt;/span&gt;&lt;span class="dl"&gt;"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works identically on your local machine, in a Docker container, or on any hosting platform that runs Bun. No environment-specific setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which Approach Should You Use
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Bun.spawn&lt;/strong&gt; if you're writing a local script or CLI tool where you control the machine and FFmpeg is already installed. Bun's fast startup makes it great for quick scripts that shell out to FFmpeg.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ffmpeg.wasm&lt;/strong&gt; if you need in-process video handling for small files and can tolerate much slower performance.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A cloud API like FFmpeg Micro&lt;/strong&gt; if you're building a production app, deploying to serverless infrastructure, or want to avoid managing FFmpeg binaries entirely. The API approach is the only option on platforms that don't support native binaries, and it keeps your Bun app lightweight.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Using &lt;code&gt;child_process&lt;/code&gt; instead of &lt;code&gt;Bun.spawn&lt;/code&gt;.&lt;/strong&gt; Bun supports Node's &lt;code&gt;child_process&lt;/code&gt; for compatibility, but &lt;code&gt;Bun.spawn&lt;/code&gt; is faster and returns a cleaner API. The &lt;code&gt;proc.exited&lt;/code&gt; promise is simpler than wiring up event listeners on a &lt;code&gt;ChildProcess&lt;/code&gt; object.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Forgetting to handle stderr.&lt;/strong&gt; FFmpeg writes all its logging (including progress) to stderr, not stdout. If you don't set &lt;code&gt;stderr: "pipe"&lt;/code&gt; in &lt;code&gt;Bun.spawn&lt;/code&gt;, you won't see error messages when a transcode fails. Bun defaults to inheriting the parent's stderr, which works for debugging but swallows output if you need to capture it programmatically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Using &lt;code&gt;Bun.file&lt;/code&gt; for reading FFmpeg output before it's flushed.&lt;/strong&gt; &lt;code&gt;Bun.file&lt;/code&gt; is fast, but if you read an output file before FFmpeg has fully flushed and closed it, you'll get a partial file. Always &lt;code&gt;await proc.exited&lt;/code&gt; before reading the output.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Assuming Bun.spawn works everywhere.&lt;/strong&gt; Bun's subprocess API needs a real OS process. On platforms like Cloudflare Workers or other V8 isolate runtimes, subprocess spawning isn't available. If your deployment target restricts system calls, use a cloud API instead.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Can I use fluent-ffmpeg with Bun?
&lt;/h3&gt;

&lt;p&gt;Yes. Bun's Node.js compatibility layer supports &lt;code&gt;fluent-ffmpeg&lt;/code&gt; out of the box. Install it with &lt;code&gt;bun add fluent-ffmpeg&lt;/code&gt; and it works the same as in Node. But you still need a local FFmpeg binary installed. If you want Bun-native code without the Node compatibility layer, use &lt;code&gt;Bun.spawn&lt;/code&gt; directly or the &lt;a href="https://dev.to/blog/ffmpeg-api-getting-started-5-examples"&gt;FFmpeg Micro API&lt;/a&gt; for cloud processing.&lt;/p&gt;

&lt;h3&gt;
  
  
  How does Bun compare to Node.js for FFmpeg video processing?
&lt;/h3&gt;

&lt;p&gt;The FFmpeg CLI itself is identical. The difference is in how you call it. Node.js uses &lt;code&gt;child_process.spawn&lt;/code&gt;, Bun uses &lt;code&gt;Bun.spawn&lt;/code&gt;. Both need the binary installed. Bun's faster startup makes short-lived scripts noticeably quicker, and native TypeScript means no build step. The cloud API approach is the same across both runtimes since it's just HTTP &lt;code&gt;fetch&lt;/code&gt;. For a Node.js comparison, check the &lt;a href="https://dev.to/blog/how-to-use-ffmpeg-with-nodejs"&gt;Node.js FFmpeg guide&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does Bun.spawn work on serverless platforms?
&lt;/h3&gt;

&lt;p&gt;It depends on the platform. Bun can run on traditional servers and in Docker, where &lt;code&gt;Bun.spawn&lt;/code&gt; works fine. But serverless environments like Cloudflare Workers or edge functions typically run in V8 isolates that don't support subprocess execution. If you're deploying to those platforms, the cloud API approach is your best option.&lt;/p&gt;

&lt;h3&gt;
  
  
  What video formats does the FFmpeg Micro API support?
&lt;/h3&gt;

&lt;p&gt;FFmpeg Micro accepts any format FFmpeg can read as input. For output, you can specify mp4, webm, or mov in the &lt;code&gt;outputFormat&lt;/code&gt; field. The preset mode supports quality levels (high, medium, low) and resolution options (480p, 720p, 1080p, 4k). Check the &lt;a href="https://www.ffmpeg-micro.com/docs" rel="noopener noreferrer"&gt;full API docs&lt;/a&gt; for the complete list.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do I need to pay to use FFmpeg Micro with Bun?
&lt;/h3&gt;

&lt;p&gt;FFmpeg Micro has a free tier with enough processing time to build and test your integration. You can &lt;a href="https://www.ffmpeg-micro.com/pricing" rel="noopener noreferrer"&gt;sign up for a free API key&lt;/a&gt; and start processing video in minutes. For a hands-on walkthrough of FFmpeg concepts, the &lt;a href="https://www.ffmpeg-micro.com/training/learn-ffmpeg" rel="noopener noreferrer"&gt;Learn FFmpeg training&lt;/a&gt; covers everything from basics to automation.&lt;/p&gt;

</description>
      <category>ffmpeg</category>
      <category>bunjs</category>
      <category>typescript</category>
      <category>api</category>
    </item>
    <item>
      <title>How to Use FFmpeg with Trigger.dev (No Binary Setup Required)</title>
      <dc:creator>Javid Jamae</dc:creator>
      <pubDate>Fri, 17 Jul 2026 10:21:29 +0000</pubDate>
      <link>https://dev.to/javidjamae/how-to-use-ffmpeg-with-triggerdev-no-binary-setup-required-4gjb</link>
      <guid>https://dev.to/javidjamae/how-to-use-ffmpeg-with-triggerdev-no-binary-setup-required-4gjb</guid>
      <description>&lt;p&gt;Originally published at &lt;a href="https://www.ffmpeg-micro.com/blog/ffmpeg-trigger-dev-video-processing" rel="noopener noreferrer"&gt;ffmpeg-micro.com&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Trigger.dev's official FFmpeg guide tells you to install the &lt;code&gt;@trigger.dev/build&lt;/code&gt; extension, bundle a static FFmpeg binary into your task, and call it with &lt;code&gt;ffmpegPath()&lt;/code&gt;. It works. Until your build artifact balloons past 100 MB, your cold starts crawl, and you're debugging codec availability across deployment environments.&lt;/p&gt;

&lt;p&gt;You don't need to ship FFmpeg with your Trigger.dev task. Call an FFmpeg API over HTTP instead, and keep your tasks lean.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Quick answer:&lt;/strong&gt; Replace the &lt;code&gt;@trigger.dev/build&lt;/code&gt; FFmpeg extension with HTTP calls to &lt;a href="https://www.ffmpeg-micro.com" rel="noopener noreferrer"&gt;FFmpeg Micro&lt;/a&gt;. POST your video URL to &lt;code&gt;/v1/transcodes&lt;/code&gt;, poll for completion, and grab the download link. No binary in your build, no cold-start penalty, and it works on any Trigger.dev deployment target.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What breaks when you bundle FFmpeg as a binary
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;@trigger.dev/build&lt;/code&gt; approach uses &lt;code&gt;ffmpegExtension()&lt;/code&gt; to include a static FFmpeg binary in your task's build output. Trigger.dev's own docs walk through this pattern. For simple cases, it's fine. But production workloads expose several problems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Build size bloat.&lt;/strong&gt; A static FFmpeg binary adds 80-120 MB to your task artifact. Every deploy ships that weight, and every cold start loads it into memory.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cold-start latency.&lt;/strong&gt; Larger artifacts mean slower cold starts. If your task scales to zero between runs, each invocation pays the binary loading tax.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Codec availability.&lt;/strong&gt; Static builds may lack specific codecs you need. Want &lt;code&gt;libx265&lt;/code&gt; or &lt;code&gt;libvpx-vp9&lt;/code&gt;? You're recompiling or hoping the bundled binary includes it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Version management.&lt;/strong&gt; Pinning FFmpeg versions across environments is your problem. Updates mean rebuilding and redeploying.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these issues exist when FFmpeg runs on someone else's infrastructure.&lt;/p&gt;

&lt;h2&gt;
  
  
  The binary approach (before)
&lt;/h2&gt;

&lt;p&gt;This is what Trigger.dev's guide recommends. You install the extension, configure your build, and call FFmpeg from the task:&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;// trigger.config.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;ffmpegExtension&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;@trigger.dev/build/extensions&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;default&lt;/span&gt; &lt;span class="nf"&gt;defineConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;extensions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;ffmpegExtension&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// tasks/process-video.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;task&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;@trigger.dev/sdk/v3&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;ffmpegPath&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;@trigger.dev/build/extensions&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;execSync&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;child_process&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;processVideoTask&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;task&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;process-video&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;run&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="na"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;inputPath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/tmp/input.mp4&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;outputPath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/tmp/output.mp4&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// Download the file first&lt;/span&gt;
    &lt;span class="nf"&gt;execSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`curl -o &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;inputPath&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;payload&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="s2"&gt;"`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Run FFmpeg binary&lt;/span&gt;
    &lt;span class="nf"&gt;execSync&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="nf"&gt;ffmpegPath&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt; -i &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;inputPath&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; -vf scale=1280:720 -crf 23 &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;outputPath&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Upload output somewhere...&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;You're managing file I/O, disk space in &lt;code&gt;/tmp&lt;/code&gt;, the binary path, and cleanup. The task does too much.&lt;/p&gt;

&lt;h2&gt;
  
  
  Drop the binary: FFmpeg over HTTP in Trigger.dev
&lt;/h2&gt;

&lt;p&gt;Replace all of that with three HTTP calls. No extension, no binary, no file system management.&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;task&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;@trigger.dev/sdk/v3&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;processVideoTask&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;task&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;process-video&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;run&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="na"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;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="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;BASE_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&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="c1"&gt;// 1. Start the transcode job&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;createRes&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_URL&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="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;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;payload&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;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="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;createRes&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;// 2. Poll until complete&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="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="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;5000&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;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;BASE_URL&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;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="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;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;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;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;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="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;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Transcode failed for 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;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// 3. Get the download URL&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_URL&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;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="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;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="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="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="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the entire task. No &lt;code&gt;trigger.config.ts&lt;/code&gt; changes. No build extensions. No &lt;code&gt;/tmp&lt;/code&gt; disk management. Your task artifact stays small, cold starts stay fast, and FFmpeg version management is someone else's problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using custom FFmpeg options
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;preset&lt;/code&gt; field handles common operations, but you can pass raw FFmpeg flags through the &lt;code&gt;options&lt;/code&gt; array when you need fine-grained control. Resize to 720p, set a specific CRF, or choose a codec:&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="nx"&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;payload&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;-vf&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;scale=1280:720&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;-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;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="s2"&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="s2"&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="s2"&gt;-b: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="s2"&gt;128k&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;outputFormat&lt;/code&gt; accepts &lt;code&gt;mp4&lt;/code&gt;, &lt;code&gt;webm&lt;/code&gt;, or &lt;code&gt;mov&lt;/code&gt;. Use &lt;code&gt;-vf&lt;/code&gt; for video filters. Note that &lt;code&gt;-filter_complex&lt;/code&gt; is not supported. Use &lt;code&gt;-vf&lt;/code&gt; for all filter operations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Handling file uploads in your task
&lt;/h2&gt;

&lt;p&gt;If your Trigger.dev task receives a file that isn't publicly accessible by URL, upload it to FFmpeg Micro first using the presigned upload flow:&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;// 1. Get a presigned upload URL&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;presignRes&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_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/v1/upload/presigned-url`&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;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;filename&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;input.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;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="na"&gt;fileSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;fileBuffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;byteLength&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;result&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;presignRes&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;// 2. PUT the file to the presigned URL&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;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;uploadUrl&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;PUT&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;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;video/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;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;fileBuffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// 3. Confirm the upload&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;confirmRes&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_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/v1/upload/confirm`&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;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;filename&lt;/span&gt;&lt;span class="p"&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;filename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;fileSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;fileBuffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;byteLength&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;confirmed&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;confirmRes&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;// Now use confirmed.result.gcsUrl as your input URL for transcoding&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The upload goes directly to cloud storage, not through the API server. Fast even for large files.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Checking only for "completed" in your poll loop.&lt;/strong&gt; If a job fails and you're not checking for &lt;code&gt;"failed"&lt;/code&gt;, your loop runs until the Trigger.dev task times out. Always check both terminal states.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mixing &lt;code&gt;options&lt;/code&gt; and &lt;code&gt;preset&lt;/code&gt; in the same request.&lt;/strong&gt; If you include both, &lt;code&gt;options&lt;/code&gt; takes priority and &lt;code&gt;preset&lt;/code&gt; is ignored. Pick one per request.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Storing the signed download URL for later use.&lt;/strong&gt; Signed URLs expire after 10 minutes. Always call the &lt;code&gt;/download&lt;/code&gt; endpoint fresh when you actually need the file.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Using &lt;code&gt;-filter_complex&lt;/code&gt; instead of &lt;code&gt;-vf&lt;/code&gt;.&lt;/strong&gt; The FFmpeg Micro API doesn't support &lt;code&gt;-filter_complex&lt;/code&gt;. Use &lt;code&gt;-vf&lt;/code&gt; for video filter operations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Polling too aggressively.&lt;/strong&gt; A 1-second interval wastes API calls. Five seconds works well for most video lengths. A 10-minute 1080p file typically processes in 1-3 minutes.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Do I still need &lt;a class="mentioned-user" href="https://dev.to/trigger"&gt;@trigger&lt;/a&gt;.dev/build for this approach?
&lt;/h3&gt;

&lt;p&gt;No. The API approach uses standard &lt;code&gt;fetch&lt;/code&gt; calls. Remove &lt;code&gt;ffmpegExtension()&lt;/code&gt; from your &lt;code&gt;trigger.config.ts&lt;/code&gt; entirely. No build extensions are needed.&lt;/p&gt;

&lt;h3&gt;
  
  
  What if my Trigger.dev task times out before the transcode finishes?
&lt;/h3&gt;

&lt;p&gt;Increase the &lt;code&gt;maxDuration&lt;/code&gt; on your task definition. Most transcodes finish within a few minutes. For very long videos, consider splitting the polling into a separate task that the first task triggers, or use Trigger.dev's &lt;code&gt;wait&lt;/code&gt; utilities.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I process videos that require authentication to download?
&lt;/h3&gt;

&lt;p&gt;Yes. Use a pre-signed URL from your storage provider (S3 pre-signed URL, GCS signed URL) as the input URL. FFmpeg Micro downloads the video from whatever URL you provide. For more details, see our guide on &lt;a href="https://dev.to/blog/ffmpeg-auth-headers"&gt;FFmpeg authorization headers&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  How does pricing compare to running my own FFmpeg binary?
&lt;/h3&gt;

&lt;p&gt;The binary approach costs you compute time on your Trigger.dev infrastructure, plus the cold-start overhead on every invocation. FFmpeg Micro has a free tier with 10 minutes of processing per month, enough to build and test your workflow. Paid plans scale based on usage. For a deeper comparison, see &lt;a href="https://dev.to/blog/self-hosting-ffmpeg-vs-api"&gt;self-hosting FFmpeg vs. using an API&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  What video formats are supported?
&lt;/h3&gt;

&lt;p&gt;Input: any format FFmpeg can read, including MP4, WebM, MOV, AVI, and MKV. Output: &lt;code&gt;mp4&lt;/code&gt;, &lt;code&gt;webm&lt;/code&gt;, or &lt;code&gt;mov&lt;/code&gt;. Audio formats (MP3, WAV, AAC) are supported for audio extraction workflows.&lt;/p&gt;

&lt;p&gt;If you're already using Trigger.dev for background jobs and want video processing without the binary overhead, &lt;a href="https://www.ffmpeg-micro.com" rel="noopener noreferrer"&gt;grab a free FFmpeg Micro API key&lt;/a&gt; and swap out the extension. The TypeScript examples above work as-is. For more on using FFmpeg with TypeScript in general, check out our &lt;a href="https://dev.to/blog/how-to-use-ffmpeg-with-typescript"&gt;FFmpeg with TypeScript guide&lt;/a&gt;.&lt;/p&gt;

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

</description>
      <category>ffmpeg</category>
      <category>typescript</category>
      <category>triggerdev</category>
      <category>serverless</category>
    </item>
    <item>
      <title>FFmpeg eq Filter: Adjust Brightness, Contrast, and Saturation</title>
      <dc:creator>Javid Jamae</dc:creator>
      <pubDate>Fri, 17 Jul 2026 10:21:17 +0000</pubDate>
      <link>https://dev.to/javidjamae/ffmpeg-eq-filter-adjust-brightness-contrast-and-saturation-32h5</link>
      <guid>https://dev.to/javidjamae/ffmpeg-eq-filter-adjust-brightness-contrast-and-saturation-32h5</guid>
      <description>&lt;p&gt;Originally published at &lt;a href="https://www.ffmpeg-micro.com/blog/ffmpeg-eq-filter-guide" rel="noopener noreferrer"&gt;ffmpeg-micro.com&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Video shot in different conditions looks different. A batch of user-uploaded clips won't match each other. Camera auto-exposure shifts between takes. Screen recordings come out dim. The FFmpeg &lt;code&gt;eq&lt;/code&gt; filter lets you fix brightness, contrast, saturation, and gamma in a single filter pass, right from the command line or an API call, without opening a GUI editor.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Quick answer:&lt;/strong&gt; The FFmpeg &lt;code&gt;eq&lt;/code&gt; filter adjusts brightness, contrast, saturation, and gamma of a video stream. Apply it with &lt;code&gt;-vf "eq=brightness=0.06:contrast=1.2:saturation=1.3"&lt;/code&gt; to brighten, boost contrast, and enrich colors in one pass. All parameters are optional and default to neutral values (brightness 0, contrast 1.0, saturation 1.0, gamma 1.0).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  How the FFmpeg eq Filter Works
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;eq&lt;/code&gt; filter modifies pixel values in the YUV color space. Brightness shifts the luma channel up or down. Contrast scales luma around a midpoint. Saturation scales the chroma channels. Gamma applies a nonlinear curve to luma (or individual RGB channels after internal conversion).&lt;/p&gt;

&lt;p&gt;The basic syntax:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;eq=brightness=&amp;lt;val&amp;gt;:contrast=&amp;lt;val&amp;gt;:saturation=&amp;lt;val&amp;gt;:gamma=&amp;lt;val&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All parameters are keyword arguments separated by colons. You only need to specify the ones you want to change.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Parameter&lt;/th&gt;
&lt;th&gt;Range&lt;/th&gt;
&lt;th&gt;Default&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;brightness&lt;/td&gt;
&lt;td&gt;-1.0 to 1.0&lt;/td&gt;
&lt;td&gt;0.0&lt;/td&gt;
&lt;td&gt;Shifts luma up (brighter) or down (darker)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;contrast&lt;/td&gt;
&lt;td&gt;-1000.0 to 1000.0&lt;/td&gt;
&lt;td&gt;1.0&lt;/td&gt;
&lt;td&gt;Scales luma around the midpoint. Below 1.0 flattens, above 1.0 sharpens tonal separation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;saturation&lt;/td&gt;
&lt;td&gt;0.0 to 3.0&lt;/td&gt;
&lt;td&gt;1.0&lt;/td&gt;
&lt;td&gt;Scales color intensity. 0 is grayscale, above 1.0 makes colors more vivid&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;gamma&lt;/td&gt;
&lt;td&gt;0.1 to 10.0&lt;/td&gt;
&lt;td&gt;1.0&lt;/td&gt;
&lt;td&gt;Nonlinear luma curve. Below 1.0 brightens midtones, above 1.0 darkens them&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;gamma_r&lt;/td&gt;
&lt;td&gt;0.1 to 10.0&lt;/td&gt;
&lt;td&gt;1.0&lt;/td&gt;
&lt;td&gt;Gamma correction for the red channel only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;gamma_g&lt;/td&gt;
&lt;td&gt;0.1 to 10.0&lt;/td&gt;
&lt;td&gt;1.0&lt;/td&gt;
&lt;td&gt;Gamma correction for the green channel only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;gamma_b&lt;/td&gt;
&lt;td&gt;0.1 to 10.0&lt;/td&gt;
&lt;td&gt;1.0&lt;/td&gt;
&lt;td&gt;Gamma correction for the blue channel only&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Adjusting Brightness and Contrast with FFmpeg
&lt;/h2&gt;

&lt;p&gt;The most common use case: a video came out too dark or too flat, and you need to correct it before publishing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CLI:&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.mp4 &lt;span class="nt"&gt;-vf&lt;/span&gt; &lt;span class="s2"&gt;"eq=brightness=0.1:contrast=1.3"&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt;:a copy output.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This bumps brightness by 0.1 (a subtle lift) and increases contrast to 1.3 (noticeable tonal punch). The &lt;code&gt;-c:a copy&lt;/code&gt; flag passes audio through without re-encoding.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;API (FFmpeg Micro):&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://storage.example.com/input.mp4"}],
    "outputFormat": "mp4",
    "options": [
      {"option": "-vf", "argument": "eq=brightness=0.1:contrast=1.3"},
      {"option": "-c:a", "argument": "copy"}
    ]
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A brightness value of 0.1 is a good starting point for underexposed footage. Going above 0.2 usually looks washed out. For contrast, stay between 1.0 and 1.5 for natural results. Anything above 2.0 starts to crush shadows and clip highlights.&lt;/p&gt;

&lt;h2&gt;
  
  
  Color Correction: Adjusting Saturation
&lt;/h2&gt;

&lt;p&gt;Desaturated footage is a common problem with screen recordings, webcam captures, and action cameras in overcast light. The &lt;code&gt;saturation&lt;/code&gt; parameter fixes it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CLI:&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.mp4 &lt;span class="nt"&gt;-vf&lt;/span&gt; &lt;span class="s2"&gt;"eq=saturation=1.5"&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt;:a copy output.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;API (FFmpeg Micro):&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://storage.example.com/input.mp4"}],
    "outputFormat": "mp4",
    "options": [
      {"option": "-vf", "argument": "eq=saturation=1.5"},
      {"option": "-c:a", "argument": "copy"}
    ]
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Setting saturation to 0 converts the video to grayscale. That's useful for stylistic effects or for feeding into computer vision pipelines where color is irrelevant. A value of 1.5 adds noticeable vibrancy without looking artificial. Above 2.0, colors start to bleed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using Gamma for Midtone Correction
&lt;/h2&gt;

&lt;p&gt;Brightness is a linear shift. Gamma is a curve. That distinction matters when you're correcting footage that looks dim but you don't want to blow out the highlights.&lt;/p&gt;

&lt;p&gt;Gamma below 1.0 lifts the midtones and shadows while leaving the brightest parts mostly untouched. Gamma above 1.0 darkens midtones without crushing blacks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CLI:&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.mp4 &lt;span class="nt"&gt;-vf&lt;/span&gt; &lt;span class="s2"&gt;"eq=gamma=0.7:saturation=1.2"&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt;:a copy output.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This brightens midtones (gamma 0.7 gives a visible lift) and slightly boosts color. It's a better choice than the brightness parameter for footage that's underexposed but has intact highlights.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;API (FFmpeg Micro):&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://storage.example.com/input.mp4"}],
    "outputFormat": "mp4",
    "options": [
      {"option": "-vf", "argument": "eq=gamma=0.7:saturation=1.2"},
      {"option": "-c:a", "argument": "copy"}
    ]
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For per-channel control, use &lt;code&gt;gamma_r&lt;/code&gt;, &lt;code&gt;gamma_g&lt;/code&gt;, and &lt;code&gt;gamma_b&lt;/code&gt;. This is useful for correcting white balance problems. Footage shot under tungsten lighting looks orange. You can fix it by reducing the red gamma and boosting the blue gamma:&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;"eq=gamma_r=0.85:gamma_b=1.15"&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt;:a copy output.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Combining eq with Other Filters
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;eq&lt;/code&gt; filter works well chained with other video filters. A common pipeline is to scale, color-correct, then encode.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CLI:&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.mp4 &lt;span class="nt"&gt;-vf&lt;/span&gt; &lt;span class="s2"&gt;"scale=1920:1080,eq=brightness=0.06:contrast=1.2:saturation=1.3"&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 &lt;span class="nt"&gt;-c&lt;/span&gt;:a copy output.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This resizes to 1080p, applies color correction, and encodes with libx264 at CRF 23. Filters in a &lt;code&gt;-vf&lt;/code&gt; chain execute left to right, so scale runs before eq.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;API (FFmpeg Micro):&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://storage.example.com/input.mp4"}],
    "outputFormat": "mp4",
    "options": [
      {"option": "-vf", "argument": "scale=1920:1080,eq=brightness=0.06:contrast=1.2:saturation=1.3"},
      {"option": "-c:v", "argument": "libx264"},
      {"option": "-crf", "argument": "23"},
      {"option": "-c:a", "argument": "copy"}
    ]
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also combine eq with the &lt;a href="https://dev.to/blog/ffmpeg-fade-in-fade-out-video"&gt;FFmpeg fade filter&lt;/a&gt; for polished intros, or chain it after the &lt;a href="https://dev.to/blog/ffmpeg-scale-filter-preserve-aspect-ratio"&gt;scale filter&lt;/a&gt; when resizing user uploads to a consistent resolution.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Confusing brightness with gamma.&lt;/strong&gt; Brightness adds a flat offset to every pixel. Highlights clip, shadows shift uniformly. Gamma applies a curve that affects midtones most. For exposure correction, gamma almost always produces more natural results than brightness.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Contrast values that destroy detail.&lt;/strong&gt; A contrast of 1.3 looks good. A contrast of 3.0 makes your video look like a threshold filter. The useful range for natural-looking output is 0.8 to 1.5. Use values outside that range only for deliberate stylistic effects.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Saturation above 2.0 on compressed video.&lt;/strong&gt; Highly compressed source video (low-bitrate H.264, heavily compressed WebM) already has chroma artifacts. Pushing saturation above 2.0 amplifies those artifacts and produces color banding. If the source is lossy, keep saturation under 1.8.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Not using &lt;code&gt;-c:a copy&lt;/code&gt;.&lt;/strong&gt; The eq filter only touches video. Without &lt;code&gt;-c:a copy&lt;/code&gt;, FFmpeg re-encodes your audio track for no reason. Always pass audio through when you're only doing video corrections. The FFmpeg Micro API handles this automatically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Expecting &lt;code&gt;-filter_complex&lt;/code&gt; support.&lt;/strong&gt; The eq filter is a simple single-input, single-output filter. Use it with &lt;code&gt;-vf&lt;/code&gt;, not &lt;code&gt;-filter_complex&lt;/code&gt;. In the FFmpeg Micro API, use the &lt;code&gt;-vf&lt;/code&gt; option field. The API doesn't support &lt;code&gt;-filter_complex&lt;/code&gt;.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  How do I make a video brighter with FFmpeg?
&lt;/h3&gt;

&lt;p&gt;Use the eq filter with a positive brightness value: &lt;code&gt;ffmpeg -i input.mp4 -vf "eq=brightness=0.1" -c:a copy output.mp4&lt;/code&gt;. The brightness parameter ranges from -1.0 (black) to 1.0 (white), with 0 being no change. For a more natural lift that preserves highlights, use gamma instead: &lt;code&gt;eq=gamma=0.8&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I convert a video to grayscale with FFmpeg?
&lt;/h3&gt;

&lt;p&gt;Yes. Set the eq filter's saturation parameter to 0: &lt;code&gt;ffmpeg -i input.mp4 -vf "eq=saturation=0" -c:a copy output.mp4&lt;/code&gt;. This removes all color information while preserving the luma channel. The output is a true grayscale video.&lt;/p&gt;

&lt;h3&gt;
  
  
  What's the difference between the eq filter and the curves filter in FFmpeg?
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;eq&lt;/code&gt; filter adjusts brightness, contrast, saturation, and gamma with simple numeric parameters. The &lt;code&gt;curves&lt;/code&gt; filter provides fine-grained control with custom tone curves (similar to Photoshop curves). For quick corrections, eq is faster to use. For precise color grading with specific control points, curves gives you more flexibility at the cost of a more complex syntax.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I fix white balance with FFmpeg?
&lt;/h3&gt;

&lt;p&gt;Use the eq filter's per-channel gamma controls. For warm (orange-tinted) footage, reduce red and boost blue: &lt;code&gt;eq=gamma_r=0.85:gamma_b=1.15&lt;/code&gt;. For cool (blue-tinted) footage, do the opposite: &lt;code&gt;eq=gamma_r=1.15:gamma_b=0.85&lt;/code&gt;. These corrections are approximate. For precise white balance, consider the &lt;code&gt;colorbalance&lt;/code&gt; or &lt;code&gt;colortemperature&lt;/code&gt; filters.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does the eq filter re-encode the video?
&lt;/h3&gt;

&lt;p&gt;Yes. Any video filter in FFmpeg requires decoding and re-encoding the video stream. To control output quality, specify an encoder and quality setting like &lt;code&gt;-c:v libx264 -crf 20&lt;/code&gt;. If you want to adjust brightness or contrast without re-encoding, that's not possible with any filter-based approach.&lt;/p&gt;

&lt;h2&gt;
  
  
  Color-Correct Video Without Managing FFmpeg
&lt;/h2&gt;

&lt;p&gt;If you're building color correction into a product or processing user uploads at scale, you can skip the FFmpeg installation and server management. The &lt;a href="https://www.ffmpeg-micro.com" rel="noopener noreferrer"&gt;FFmpeg Micro API&lt;/a&gt; runs the eq filter (and every other FFmpeg filter) as a stateless HTTP endpoint. &lt;a href="https://www.ffmpeg-micro.com" rel="noopener noreferrer"&gt;Get a free API key&lt;/a&gt; and start correcting video in minutes.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Last verified: July 2026 against FFmpeg 7.x and the FFmpeg Micro API v1.&lt;/em&gt;&lt;/p&gt;

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