<?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.clauneck.workers.dev/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.clauneck.workers.dev/javidjamae</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.clauneck.workers.dev/feed/javidjamae"/>
    <language>en</language>
    <item>
      <title>How to Post 100 Videos a Week Without Editing Software</title>
      <dc:creator>Javid Jamae</dc:creator>
      <pubDate>Thu, 25 Jun 2026 10:17:09 +0000</pubDate>
      <link>https://dev.clauneck.workers.dev/javidjamae/how-to-post-100-videos-a-week-without-editing-software-2nhn</link>
      <guid>https://dev.clauneck.workers.dev/javidjamae/how-to-post-100-videos-a-week-without-editing-software-2nhn</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.ffmpeg-micro.com/blog/post-100-videos-week-without-editing-software" rel="noopener noreferrer"&gt;ffmpeg-micro.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;You run a one-person operation. You know you should be posting video content. But you're also the one building the product, handling support, and managing the business. The idea of opening editing software 100 times a week sounds like a joke.&lt;/p&gt;

&lt;p&gt;It's not a joke if you take the editor out of the equation.&lt;/p&gt;

&lt;p&gt;This post walks through a real pipeline that lets a solo operator publish 100+ videos a week across YouTube Shorts, TikTok, and Instagram Reels without touching editing software once. The whole thing runs on an automation tool, a video processing API, and about 30 minutes of initial setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why 100 Videos Isn't as Crazy as It Sounds
&lt;/h2&gt;

&lt;p&gt;Most people think "100 videos" means 100 shoots. It doesn't. It means having source footage (or templates) and a system that turns them into platform-ready clips automatically.&lt;/p&gt;

&lt;p&gt;A 10-minute podcast episode can become 15-20 short clips. A single product demo can be reformatted for three platforms. Five blog posts can turn into five text-on-video pieces per post. The math adds up fast when the system handles the repetitive work.&lt;/p&gt;

&lt;p&gt;The bottleneck was never ideas or footage. It was the manual step of opening Premiere, DaVinci, or CapCut, trimming each clip, adding captions, exporting at the right resolution, and doing that over and over.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Pipeline
&lt;/h2&gt;

&lt;p&gt;The full system has four stages:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Source content goes in.&lt;/strong&gt; This could be long-form video you've already recorded, podcast episodes, screen recordings, or even just text content that gets turned into video via templates.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;An automation tool orchestrates everything.&lt;/strong&gt; n8n, Make.com, or Zapier watches for new content (a new file in Google Drive, a new row in Airtable, a webhook from your CMS) and kicks off the processing jobs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;FFmpeg Micro handles the video processing.&lt;/strong&gt; Each job is an API call. Trim to 60 seconds for Shorts. Scale to 9:16 for TikTok. Add a text overlay. Burn in captions. Compress for upload. No FFmpeg install. No server to manage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The outputs get distributed.&lt;/strong&gt; The automation tool takes the processed videos and uploads them to each platform, or drops them in a folder for scheduled posting.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Real Example: Blog Posts to Video Clips
&lt;/h2&gt;

&lt;p&gt;Say you publish 5 blog posts a week. Each post has a headline and 3-4 key points. You want a short video for each key point with text-on-screen overlay. That's 15-20 videos per week from content you already wrote.&lt;/p&gt;

&lt;p&gt;The n8n workflow breaks down into three steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A webhook fires when a new blog post is published&lt;/li&gt;
&lt;li&gt;n8n pulls the title and key points from the post&lt;/li&gt;
&lt;li&gt;For each key point, n8n calls FFmpeg Micro to overlay the text on a background video template&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The FFmpeg Micro API call for each clip:&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-bucket.storage.com/background-template.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;"You don't need editing software to post 100 videos a week"&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;20&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;54&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;12&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;"(w-text_w)/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;"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;10&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;That's it. One API call per clip. n8n loops through every key point and fires these in parallel.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;n8n downloads the finished videos and uploads to YouTube Shorts, TikTok, or a Google Drive folder for batch scheduling&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;No editing software opened. No manual export. No resizing one clip at a time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scaling to 100 a Week
&lt;/h2&gt;

&lt;p&gt;To hit triple digits, combine multiple content sources:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Podcast clips (40-50/week).&lt;/strong&gt; Record 2 podcast episodes. Each one produces 20-25 clips when you trim at interesting timestamps. FFmpeg Micro handles the trim, the 9:16 crop, and the caption overlay in a single job.&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-bucket.storage.com/podcast-ep-42.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="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;"-ss"&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;"00:04:32"&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;"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;"-t"&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;"58"&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;"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;"crop=ih*9/16:ih,scale=1080:1920"&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;&lt;strong&gt;Blog-to-video (15-20/week).&lt;/strong&gt; The text overlay workflow above. Five posts, 3-4 clips each.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Product demos (10-15/week).&lt;/strong&gt; Record one 20-minute walkthrough and slice it. Each feature gets its own clip.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reposts across platforms (20-30/week).&lt;/strong&gt; Take your best-performing clips and reformat them for platforms you haven't posted to yet. A vertical TikTok video becomes a square Instagram post with padding:&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-bucket.storage.com/tiktok-clip.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="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;"scale=1080:1080:force_original_aspect_ratio=decrease,pad=1080:1080:(ow-iw)/2:(oh-ih)/2:black"&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;Each of these is a single API call. Your automation tool fires them in batch, downloads the results, and either auto-publishes or queues them for review.&lt;/p&gt;

&lt;h2&gt;
  
  
  What This Actually Costs
&lt;/h2&gt;

&lt;p&gt;FFmpeg Micro charges per minute of video processed. A 60-second short costs about one minute of processing time. At 100 clips per week, you're looking at roughly 100 minutes of processing. The free tier covers small-scale testing. A paid plan handles the full volume.&lt;/p&gt;

&lt;p&gt;Compare that to hiring a video editor ($500-2000/month) or spending 20 hours a week in editing software yourself. The math isn't close.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Don't process videos sequentially.&lt;/strong&gt; Fire API calls in parallel from your automation tool. n8n's "split in batches" node lets you process 10-20 clips at once. Sequential processing turns a 15-minute pipeline into a 3-hour one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Template backgrounds matter.&lt;/strong&gt; If your text-on-video clips look generic, it's because the background template is generic. Invest an hour recording 5-10 branded background loops (abstract motion, your office, your product's UI) and rotate through them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Platform specs change.&lt;/strong&gt; TikTok updated their preferred aspect ratio in 2025. YouTube Shorts now accepts up to 3 minutes. Build your automation with configurable dimensions and durations so a spec change is a variable update, not a pipeline rebuild.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Don't skip the review step at first.&lt;/strong&gt; Run the pipeline into a "review" folder for the first week. Spot-check 10% of the output. Once you trust the system, switch to auto-publish.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Do I need to know FFmpeg commands to use this?&lt;/strong&gt; No. FFmpeg Micro's preset system handles common operations (trim, scale, compress) with simple parameters. You only need raw FFmpeg options for custom work like specific filter chains.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What automation tools work with this?&lt;/strong&gt; Any tool that can make HTTP requests. n8n, Make.com, and Zapier all work. So does a Python script, a Node.js cron job, or a Shortcuts workflow on macOS.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Will the videos look professional?&lt;/strong&gt; That depends on your source material and templates, not the processing tool. FFmpeg Micro processes video at the same quality as FFmpeg running locally. If your source footage is 1080p, your output is 1080p.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I add captions/subtitles automatically?&lt;/strong&gt; Yes. Pair a transcription API (Whisper, Deepgram, AssemblyAI) with FFmpeg Micro. The transcription API generates the subtitle file, and FFmpeg Micro burns it into the video.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What if a job fails?&lt;/strong&gt; FFmpeg Micro returns clear error messages on failure. Your automation tool can catch the error and retry or alert you. Failed jobs don't charge processing time.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Last verified: June 2026. API examples use FFmpeg Micro v1.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>automation</category>
      <category>video</category>
      <category>productivity</category>
      <category>api</category>
    </item>
    <item>
      <title>How to Use FFmpeg with Kotlin (No Installation Required)</title>
      <dc:creator>Javid Jamae</dc:creator>
      <pubDate>Thu, 25 Jun 2026 10:16:37 +0000</pubDate>
      <link>https://dev.clauneck.workers.dev/javidjamae/how-to-use-ffmpeg-with-kotlin-no-installation-required-37nn</link>
      <guid>https://dev.clauneck.workers.dev/javidjamae/how-to-use-ffmpeg-with-kotlin-no-installation-required-37nn</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.ffmpeg-micro.com/blog/how-to-use-ffmpeg-with-kotlin" rel="noopener noreferrer"&gt;ffmpeg-micro.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;You need video processing in your Kotlin app. Maybe you're building a Spring Boot backend that transcodes user uploads, an Android app that generates social clips, or a Ktor service that watermarks content before delivery. You search "ffmpeg kotlin" and find scattered examples, most from 2019.&lt;/p&gt;

&lt;p&gt;FFmpeg handles the video processing. But running it from Kotlin means picking between ProcessBuilder (which works but requires FFmpeg on every machine), a JVM wrapper library, or a cloud API that skips the install entirely. This post covers all three with working Kotlin code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running FFmpeg from Kotlin with ProcessBuilder
&lt;/h2&gt;

&lt;p&gt;Kotlin runs on the JVM, so you get Java's ProcessBuilder for free. Install FFmpeg on the machine, then shell out to it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;process&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ProcessBuilder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"ffmpeg"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"-i"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"input.mp4"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"-c:v"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"libx264"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"-crf"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"23"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"-preset"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"medium"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"-c:a"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"aac"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"-b:a"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"128k"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"output.mp4"&lt;/span&gt;
&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;redirectErrorStream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;inputStream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;bufferedReader&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;forEachLine&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;exitCode&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waitFor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nf"&gt;check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exitCode&lt;/span&gt; &lt;span class="p"&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="s"&gt;"FFmpeg failed with exit code $exitCode"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the simplest path. But you own the FFmpeg install on every server, every CI runner, every Docker image. On Docker, FFmpeg adds 80-200MB. On serverless (Lambda, Cloud Functions), you can't install system binaries at all and the 250MB package limit is tight.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using Jaffree for a Fluent API
&lt;/h2&gt;

&lt;p&gt;Jaffree is the best JVM wrapper for FFmpeg. It works perfectly from Kotlin and gives you a builder API instead of string arrays:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="c1"&gt;// build.gradle.kts&lt;/span&gt;
&lt;span class="nf"&gt;dependencies&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;implementation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"com.github.kokorin.jaffree:jaffree:2023.09.10"&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 kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;com.github.kokorin.jaffree.ffmpeg.FFmpeg&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;com.github.kokorin.jaffree.ffmpeg.UrlInput&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;com.github.kokorin.jaffree.ffmpeg.UrlOutput&lt;/span&gt;

&lt;span class="nc"&gt;FFmpeg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;atPath&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addInput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;UrlInput&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"input.mp4"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addOutput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nc"&gt;UrlOutput&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"output.mp4"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addArguments&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"-c:v"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"libx264"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addArguments&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"-crf"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"23"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addArguments&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"-preset"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"medium"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addArguments&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"-c:a"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"aac"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addArguments&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"-b:a"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"128k"&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;execute&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Jaffree also supports progress tracking, which is useful for showing users a progress bar:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nc"&gt;FFmpeg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;atPath&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addInput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;UrlInput&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"input.mp4"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addOutput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nc"&gt;UrlOutput&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"output.mp4"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addArguments&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"-c:v"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"libx264"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addArguments&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"-crf"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"23"&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;setProgressListener&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;progress&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
        &lt;span class="nf"&gt;println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Frame: ${progress.frame}, Time: ${progress.timeMark}"&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;execute&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Better than raw ProcessBuilder. But Jaffree still needs FFmpeg on the host. It's a wrapper, not a replacement. You still manage the binary across environments.&lt;/p&gt;

&lt;h2&gt;
  
  
  Processing Video via Cloud API (No FFmpeg Install)
&lt;/h2&gt;

&lt;p&gt;If you don't want FFmpeg on your machines, offload the work to a cloud API. FFmpeg Micro gives you full FFmpeg capabilities through HTTP. Here's a minimal example using OkHttp, the most common HTTP client in the Kotlin ecosystem:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="c1"&gt;// build.gradle.kts&lt;/span&gt;
&lt;span class="nf"&gt;dependencies&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;implementation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"com.squareup.okhttp3:okhttp:4.12.0"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;implementation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"org.json:json:20240303"&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 kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;okhttp3.*&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;okhttp3.MediaType.Companion.toMediaType&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;okhttp3.RequestBody.Companion.toRequestBody&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.json.JSONObject&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.json.JSONArray&lt;/span&gt;

&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;client&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OkHttpClient&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;apiKey&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"FFMPEG_MICRO_API_KEY"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;body&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;JSONObject&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;apply&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"inputs"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;JSONArray&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;JSONObject&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"https://example.com/input.mp4"&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
    &lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"outputFormat"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"mp4"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"preset"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;JSONObject&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;apply&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"quality"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"high"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"resolution"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"1080p"&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;toString&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;request&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Builder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"https://api.ffmpeg-micro.com/v1/transcodes"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Authorization"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Bearer $apiKey"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Content-Type"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"application/json"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toRequestBody&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"application/json"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toMediaType&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;response&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;newCall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;JSONObject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="o"&gt;!!&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;jobId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Job created: $jobId"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No binary to install. No Docker image bloat. No codec management. Send a video URL, pick your settings, get results back.&lt;/p&gt;

&lt;p&gt;For advanced operations, pass raw FFmpeg options instead of presets:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;advancedBody&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;JSONObject&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;apply&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"inputs"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;JSONArray&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;JSONObject&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"https://example.com/input.mp4"&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
    &lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"outputFormat"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"webm"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"options"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;JSONArray&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;apply&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;JSONObject&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"option"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"-c:v"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"argument"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"libvpx-vp9"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;JSONObject&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"option"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"-crf"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"argument"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"30"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;JSONObject&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"option"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"-b:v"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"argument"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"0"&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;toString&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;Transcode jobs are async. Poll until the job finishes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="py"&gt;status&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"queued"&lt;/span&gt;
&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"queued"&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"processing"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;Thread&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="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;statusRequest&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Builder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"https://api.ffmpeg-micro.com/v1/transcodes/$jobId"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Authorization"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Bearer $apiKey"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;statusResponse&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;newCall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;statusRequest&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;JSONObject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;statusResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="o"&gt;!!&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;getString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Status: $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;Once the status is &lt;code&gt;completed&lt;/code&gt;, grab the download URL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;downloadRequest&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Builder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"https://api.ffmpeg-micro.com/v1/transcodes/$jobId/download"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Authorization"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Bearer $apiKey"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;downloadResponse&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;newCall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;downloadRequest&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;downloadUrl&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;JSONObject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;downloadResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="o"&gt;!!&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;getString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;// Signed URL valid for 10 minutes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Using Ktor Client Instead
&lt;/h3&gt;

&lt;p&gt;If you're already using Ktor, use its built-in HTTP client instead of adding OkHttp:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="c1"&gt;// build.gradle.kts&lt;/span&gt;
&lt;span class="nf"&gt;dependencies&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;implementation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"io.ktor:ktor-client-core:2.3.12"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;implementation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"io.ktor:ktor-client-cio:2.3.12"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;implementation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"io.ktor:ktor-client-content-negotiation:2.3.12"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;implementation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"io.ktor:ktor-serialization-kotlinx-json:2.3.12"&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 kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;io.ktor.client.*&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;io.ktor.client.engine.cio.*&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;io.ktor.client.request.*&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;io.ktor.client.statement.*&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;io.ktor.http.*&lt;/span&gt;

&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;client&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;HttpClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;CIO&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;response&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"https://api.ffmpeg-micro.com/v1/transcodes"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Authorization"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Bearer $apiKey"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;contentType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ContentType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Json&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;setBody&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"""
        {
            "inputs": [{"url": "https://example.com/input.mp4"}],
            "outputFormat": "mp4",
            "preset": {"quality": "high", "resolution": "1080p"}
        }
    """&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trimIndent&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;bodyAsText&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ktor's client is lighter than OkHttp and plays well with coroutines if your backend is already async.&lt;/p&gt;

&lt;h2&gt;
  
  
  CLI vs. API: Side-by-Side
&lt;/h2&gt;

&lt;p&gt;Same operation (converting MP4 to 720p WebM) done both ways:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;FFmpeg CLI (requires local install):&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;-c&lt;/span&gt;:v libvpx-vp9 &lt;span class="nt"&gt;-crf&lt;/span&gt; 30 &lt;span class="nt"&gt;-b&lt;/span&gt;:v 0 &lt;span class="nt"&gt;-vf&lt;/span&gt; &lt;span class="nv"&gt;scale&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nt"&gt;-2&lt;/span&gt;:720 output.webm
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;FFmpeg Micro API (no install):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;body&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;JSONObject&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;apply&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"inputs"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;JSONArray&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;JSONObject&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"https://example.com/input.mp4"&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
    &lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"outputFormat"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"webm"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"options"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;JSONArray&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;apply&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;JSONObject&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"option"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"-c:v"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"argument"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"libvpx-vp9"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;JSONObject&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"option"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"-crf"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"argument"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"30"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;JSONObject&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"option"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"-b:v"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"argument"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"0"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;JSONObject&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"option"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"-vf"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"argument"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"scale=-2:720"&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;toString&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The CLI version needs FFmpeg installed. The API version runs from any Kotlin app with an HTTP client.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;ProcessBuilder path issues on macOS.&lt;/strong&gt; Kotlin scripts and Gradle tasks don't always inherit your shell PATH. If FFmpeg isn't found, pass the full path (&lt;code&gt;/usr/local/bin/ffmpeg&lt;/code&gt; or &lt;code&gt;/opt/homebrew/bin/ffmpeg&lt;/code&gt; on Apple Silicon).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Blocking the main thread in Android.&lt;/strong&gt; Never call ProcessBuilder or make HTTP requests on the main thread. Wrap everything in a coroutine with &lt;code&gt;Dispatchers.IO&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nf"&gt;withContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Dispatchers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;IO&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;response&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;newCall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="c1"&gt;// handle response&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Jaffree version compatibility.&lt;/strong&gt; Some older Jaffree versions have issues with FFmpeg 6.x. Use &lt;code&gt;2023.09.10&lt;/code&gt; or newer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Large file uploads.&lt;/strong&gt; For files over 100MB, use FFmpeg Micro's presigned upload flow instead of passing a public URL. Generate a presigned URL, PUT the file directly to cloud storage, confirm the upload, then reference the GCS URL in your transcode request.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Can I use FFmpeg on Android directly?&lt;/strong&gt; Not easily. Android doesn't include FFmpeg, and cross-compiling it for ARM is painful. Mobile FFmpeg (now FFmpeg Kit) works but adds 20-50MB to your APK. A cloud API avoids all of this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does Kotlin Multiplatform work with FFmpeg?&lt;/strong&gt; On JVM targets, yes (ProcessBuilder or Jaffree). On Native or JS targets, no. A cloud API works from any Kotlin target since it's just HTTP.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's the cost of using a cloud API vs self-hosting?&lt;/strong&gt; FFmpeg Micro has a free tier for testing and small workloads. For production, pricing is per-minute of video processed. Self-hosting costs server time, maintenance, and your attention when codecs break after an OS update.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is the API fast enough for real-time processing?&lt;/strong&gt; FFmpeg Micro is designed for batch and async workflows, not real-time streaming. Jobs typically complete in seconds for short videos. For live streaming, you need a different tool.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I handle errors from the API?&lt;/strong&gt; Check the HTTP status code. 400 means bad request (wrong parameters). 402 means you've hit your quota. 401 means your API key is invalid or expired. The response body always includes an error message.&lt;/p&gt;

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

</description>
      <category>kotlin</category>
      <category>ffmpeg</category>
      <category>api</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Build an Automated YouTube Channel That Publishes Itself</title>
      <dc:creator>Javid Jamae</dc:creator>
      <pubDate>Wed, 24 Jun 2026 10:15:15 +0000</pubDate>
      <link>https://dev.clauneck.workers.dev/javidjamae/build-an-automated-youtube-channel-that-publishes-itself-55o2</link>
      <guid>https://dev.clauneck.workers.dev/javidjamae/build-an-automated-youtube-channel-that-publishes-itself-55o2</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.ffmpeg-micro.com/blog/automated-youtube-channel-pipeline-ffmpeg" rel="noopener noreferrer"&gt;ffmpeg-micro.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Faceless YouTube channels make money. That's not the interesting part. The interesting part is that most of the work can be automated, and the people doing it well are publishing 30+ videos per week without touching editing software.&lt;/p&gt;

&lt;p&gt;The pipeline looks like this: pick a niche, generate scripts, create visuals, compose the video with FFmpeg, and schedule uploads. Each step can be automated. This post walks through the full stack.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pick a niche that works for automation
&lt;/h2&gt;

&lt;p&gt;Not every YouTube niche is automatable. You need content where the visuals don't require custom footage. That means niches built on stock clips, screen recordings, text overlays, or generated images.&lt;/p&gt;

&lt;p&gt;Niches that work well:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Motivational/quote compilations&lt;/li&gt;
&lt;li&gt;Reddit story narration&lt;/li&gt;
&lt;li&gt;News roundups and "top 10" lists&lt;/li&gt;
&lt;li&gt;Educational explainers with slides or diagrams&lt;/li&gt;
&lt;li&gt;Product reviews using B-roll and screenshots&lt;/li&gt;
&lt;li&gt;Coding tutorials with screen capture&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Niches that don't automate well: vlogs, travel content, cooking channels, anything where the camera work IS the content.&lt;/p&gt;

&lt;p&gt;The revenue math is simple. YouTube pays roughly $2-8 CPM (cost per 1,000 views) depending on the niche. Finance and tech niches pay closer to $8. Entertainment and gaming hover around $2-3. At 100,000 monthly views with a $5 CPM, that's $500/month. Scale to 300,000 views and you're at $1,500/month.&lt;/p&gt;

&lt;h2&gt;
  
  
  Script generation with AI
&lt;/h2&gt;

&lt;p&gt;Each video needs a script. For a 60-second Short, that's about 150 words. For a 5-minute explainer, roughly 750 words.&lt;/p&gt;

&lt;p&gt;Use Claude, GPT-4, or any capable LLM with a system prompt tailored to your niche:&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.anthropic.com/v1/messages &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"x-api-key: YOUR_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;'{
    "model": "claude-sonnet-4-6-20250514",
    "max_tokens": 1024,
    "messages": [{"role": "user", "content": "Write a 150-word script for a YouTube Short about 5 underrated productivity apps. Punchy, direct, no filler."}]
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For batch production, store your topic list in a spreadsheet or database and loop through it. One API call per script. At scale, you're generating 30+ scripts per week for under $1 in API costs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Turn scripts into audio with TTS
&lt;/h2&gt;

&lt;p&gt;Text-to-speech has gotten good enough that viewers can't always tell. ElevenLabs and OpenAI's TTS are the top options.&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.openai.com/v1/audio/speech &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_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;'{
    "model": "tts-1",
    "input": "Your script text goes here...",
    "voice": "onyx"
  }'&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; narration.mp3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives you an MP3 file for each video. Batch it the same way: loop through scripts, generate audio, save to a folder.&lt;/p&gt;

&lt;h2&gt;
  
  
  Compose the video with FFmpeg
&lt;/h2&gt;

&lt;p&gt;This is where it all comes together. You need to combine background footage, text overlays, and narration into a final video. FFmpeg handles all of it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Basic composition: background + audio
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffmpeg &lt;span class="nt"&gt;-i&lt;/span&gt; background.mp4 &lt;span class="nt"&gt;-i&lt;/span&gt; narration.mp3 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-c&lt;/span&gt;:v copy &lt;span class="nt"&gt;-c&lt;/span&gt;:a aac &lt;span class="nt"&gt;-shortest&lt;/span&gt; output.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Add text overlays for key points
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffmpeg &lt;span class="nt"&gt;-i&lt;/span&gt; background.mp4 &lt;span class="nt"&gt;-i&lt;/span&gt; narration.mp3 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-vf&lt;/span&gt; &lt;span class="s2"&gt;"drawtext=text='5 Apps You Need':fontsize=48:fontcolor=white:x=(w-text_w)/2:y=100:enable='between(t,0,3)'"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-c&lt;/span&gt;:a aac &lt;span class="nt"&gt;-shortest&lt;/span&gt; output.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  At scale: use the FFmpeg Micro API
&lt;/h3&gt;

&lt;p&gt;When you're processing 30+ videos per week, running FFmpeg locally becomes a bottleneck. Server resources, queue management, error handling. The FFmpeg Micro API handles all of that.&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/background.mp4"},
      {"url": "https://storage.example.com/narration.mp3"}
    ],
    "outputFormat": "mp4",
    "options": [
      {"option": "-c:v", "argument": "libx264"},
      {"option": "-c:a", "argument": "aac"},
      {"option": "-shortest", "argument": ""}
    ]
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;FFmpeg Micro processes the video in the cloud, scales automatically, and returns a download URL when it's done. No server to manage. No FFmpeg to install. You pay per minute of video processed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Automate the full pipeline with n8n or Make
&lt;/h2&gt;

&lt;p&gt;The glue that connects everything is a workflow automation tool. n8n and Make.com both work. The workflow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Trigger:&lt;/strong&gt; New row in a Google Sheet (or cron schedule)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Script generation:&lt;/strong&gt; Call Claude/GPT API with the topic&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TTS:&lt;/strong&gt; Send script to ElevenLabs or OpenAI TTS, save the audio file&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Upload assets:&lt;/strong&gt; Push background video and audio to cloud storage&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compose:&lt;/strong&gt; Call FFmpeg Micro API to combine assets&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Poll for completion:&lt;/strong&gt; Check job status until done&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Download:&lt;/strong&gt; Grab the output video URL&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Upload to YouTube:&lt;/strong&gt; Use the YouTube Data API to publish&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each step is an HTTP request. No custom code required if you're using n8n or Make. The entire pipeline runs on a schedule.&lt;/p&gt;

&lt;h2&gt;
  
  
  The math at 30 videos per week
&lt;/h2&gt;

&lt;p&gt;Running this pipeline at 30 videos per week costs roughly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Script generation (LLM):&lt;/strong&gt; ~$3/month (150-word scripts are cheap)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TTS:&lt;/strong&gt; ~$15/month (ElevenLabs starter plan or OpenAI TTS at $15/1M characters)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Video processing (FFmpeg Micro):&lt;/strong&gt; ~$10-20/month depending on video length&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automation (n8n Cloud or Make):&lt;/strong&gt; ~$20/month&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Total:&lt;/strong&gt; ~$50-60/month&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Against potential YouTube ad revenue of $500-1,500/month at 100k-300k views, the margins are strong. The timeline to 100k monthly views varies wildly by niche, but channels publishing daily content consistently tend to hit it within 6-12 months.&lt;/p&gt;

&lt;h2&gt;
  
  
  What separates channels that make money from channels that don't
&lt;/h2&gt;

&lt;p&gt;Volume alone isn't enough. The channels that actually monetize share a few patterns:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;They pick high-CPM niches.&lt;/strong&gt; A finance channel earning $8 CPM needs 125,000 views for $1,000/month. An entertainment channel at $2 CPM needs 500,000 views for the same revenue. Niche selection is the highest-leverage decision in the whole pipeline.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;They optimize thumbnails and titles.&lt;/strong&gt; Automated content production means nothing if nobody clicks. Spend time on thumbnails (Canva templates work), and test titles. This is the one step that benefits from human judgment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;They batch process.&lt;/strong&gt; Instead of making one video at a time, they generate a week's worth of scripts, produce all the audio, compose all the videos, and schedule everything in one session. The pipeline described above does exactly this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;They reinvest early revenue.&lt;/strong&gt; First $500/month goes into better stock footage subscriptions, higher-quality TTS voices, or a second channel in a different niche.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  How many videos per week should I publish?
&lt;/h3&gt;

&lt;p&gt;Start with 5-7. Increase to daily (7/week) once your pipeline is stable. Channels doing 30+ per week usually have multiple series or formats running in parallel.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do faceless channels get monetized by YouTube?
&lt;/h3&gt;

&lt;p&gt;Yes. YouTube's Partner Program requires 1,000 subscribers and 4,000 watch hours (or 10M Shorts views). Content type doesn't matter as long as it's original and provides value. Compilations of other people's content get flagged. Original scripts with stock footage do not.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I use this for YouTube Shorts specifically?
&lt;/h3&gt;

&lt;p&gt;Absolutely. Shorts are actually easier to automate because they're 60 seconds max. Shorter scripts, simpler compositions, faster processing. And Shorts can drive subscribers fast, which helps you hit the monetization threshold.&lt;/p&gt;

&lt;h3&gt;
  
  
  What's the best automation tool for this?
&lt;/h3&gt;

&lt;p&gt;n8n if you want self-hosted control and unlimited workflows. Make.com if you want a visual builder and don't mind the per-operation pricing. Both integrate with FFmpeg Micro, YouTube, and LLM APIs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is this against YouTube's terms of service?
&lt;/h3&gt;

&lt;p&gt;No. YouTube's policies target spam, misleading content, and reused content without added value. Original scripts, original compositions, and genuine informational content are fine. Channels that just re-upload other people's videos get taken down. Channels that automate original content production don't.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Last verified: June 2026. API examples use FFmpeg Micro v1, Claude API, and OpenAI TTS.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>youtube</category>
      <category>automation</category>
      <category>ffmpeg</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>FFmpeg -c copy Explained: How to Remux Video Without Re-encoding</title>
      <dc:creator>Javid Jamae</dc:creator>
      <pubDate>Wed, 24 Jun 2026 10:14:49 +0000</pubDate>
      <link>https://dev.clauneck.workers.dev/javidjamae/ffmpeg-c-copy-explained-how-to-remux-video-without-re-encoding-3k79</link>
      <guid>https://dev.clauneck.workers.dev/javidjamae/ffmpeg-c-copy-explained-how-to-remux-video-without-re-encoding-3k79</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.ffmpeg-micro.com/blog/ffmpeg-c-copy-remux-without-reencoding" rel="noopener noreferrer"&gt;ffmpeg-micro.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;You have an MKV file. Your player needs MP4. The video and audio codecs inside are already perfect. Re-encoding the whole thing would take minutes, burn CPU, and quietly degrade quality.&lt;/p&gt;

&lt;p&gt;The fix is one flag: &lt;code&gt;-c copy&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What -c copy actually does
&lt;/h2&gt;

&lt;p&gt;FFmpeg's &lt;code&gt;-c copy&lt;/code&gt; tells the encoder to skip encoding entirely. It copies the raw video and audio bitstreams from the input container into the output container without touching a single frame. This process is called &lt;strong&gt;remuxing&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The difference is massive:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Operation&lt;/th&gt;
&lt;th&gt;Time (1 min video)&lt;/th&gt;
&lt;th&gt;CPU usage&lt;/th&gt;
&lt;th&gt;Quality loss&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Re-encode (libx264, CRF 23)&lt;/td&gt;
&lt;td&gt;15-45 seconds&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;Yes (generational)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stream copy (-c copy)&lt;/td&gt;
&lt;td&gt;Under 1 second&lt;/td&gt;
&lt;td&gt;Near zero&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Remuxing is instant because there's no decode-encode cycle. FFmpeg reads packets from the source container and writes them to the target container. That's it.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to use -c copy (and when not to)
&lt;/h2&gt;

&lt;p&gt;Use &lt;code&gt;-c copy&lt;/code&gt; when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You're changing containers (MKV to MP4, MP4 to MKV, AVI to MP4)&lt;/li&gt;
&lt;li&gt;You need to extract a single stream (pull audio out of a video, pull video without audio)&lt;/li&gt;
&lt;li&gt;You're adding &lt;code&gt;faststart&lt;/code&gt; for web delivery without re-encoding&lt;/li&gt;
&lt;li&gt;You want to strip metadata or chapters while keeping the media intact&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Don't use &lt;code&gt;-c copy&lt;/code&gt; when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The source codec isn't compatible with the target container (VP9 in an MP4 container will fail in most players)&lt;/li&gt;
&lt;li&gt;You need to change resolution, bitrate, or codec&lt;/li&gt;
&lt;li&gt;You're trimming and need frame-accurate cuts (stream copy can only cut on keyframes)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Common remux operations
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Change container: MKV to MP4
&lt;/h3&gt;

&lt;p&gt;The most common remux. MKV files with H.264 video and AAC audio drop into MP4 containers without issues.&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;h3&gt;
  
  
  Add faststart for web streaming
&lt;/h3&gt;

&lt;p&gt;MP4 files store their metadata (the "moov atom") at the end by default. Browsers need that metadata before they can start playing. The &lt;code&gt;-movflags +faststart&lt;/code&gt; flag moves it to the front so the video starts playing immediately instead of downloading the whole file first.&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;h3&gt;
  
  
  Extract audio from video
&lt;/h3&gt;

&lt;p&gt;Pull the audio track out without re-encoding. The output format should match the audio codec. If the source has AAC audio, output to &lt;code&gt;.aac&lt;/code&gt; or &lt;code&gt;.m4a&lt;/code&gt;. If it's Opus, output to &lt;code&gt;.ogg&lt;/code&gt; or &lt;code&gt;.opus&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffmpeg &lt;span class="nt"&gt;-i&lt;/span&gt; input.mp4 &lt;span class="nt"&gt;-c&lt;/span&gt;:a copy &lt;span class="nt"&gt;-vn&lt;/span&gt; audio-only.aac
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;-vn&lt;/code&gt; flag tells FFmpeg to drop the video stream entirely. The &lt;code&gt;-c:a copy&lt;/code&gt; copies the audio bitstream as-is.&lt;/p&gt;

&lt;h3&gt;
  
  
  Extract video without audio
&lt;/h3&gt;



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

&lt;/div&gt;



&lt;p&gt;Same idea, reversed. &lt;code&gt;-an&lt;/code&gt; drops audio, &lt;code&gt;-c:v copy&lt;/code&gt; keeps video untouched.&lt;/p&gt;

&lt;h3&gt;
  
  
  Remux multiple files in a batch
&lt;/h3&gt;

&lt;p&gt;When you have a folder of MKV files that need to be MP4:&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="k"&gt;for &lt;/span&gt;f &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt;.mkv&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;-c&lt;/span&gt; copy &lt;span class="s2"&gt;"&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;%.mkv&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.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;
  
  
  Remuxing with the FFmpeg Micro API
&lt;/h2&gt;

&lt;p&gt;If you're processing videos in an application or automation workflow, you can remux through the FFmpeg Micro API. No FFmpeg installation needed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Container change via API
&lt;/h3&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.mkv"}],
    "outputFormat": "mp4",
    "options": [
      {"option": "-c", "argument": "copy"},
      {"option": "-movflags", "argument": "+faststart"}
    ]
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This queues a remux job that copies all streams into an MP4 container with faststart enabled. The response includes a job ID you can poll:&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;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"abc-123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"queued"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"billableMinutes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.5&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;h3&gt;
  
  
  Extract audio via API
&lt;/h3&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": "aac",
    "options": [
      {"option": "-c:a", "argument": "copy"},
      {"option": "-vn", "argument": ""}
    ]
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both operations complete in seconds because there's no re-encoding involved. You only pay for the input duration, and the processing time is minimal.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Codec/container mismatch.&lt;/strong&gt; Not every codec fits in every container. VP9 video works in WebM and MKV but most players choke on VP9 inside MP4. If your remux produces a file that won't play, check whether the codec is compatible with the target container.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Keyframe-only cuts.&lt;/strong&gt; When you combine &lt;code&gt;-c copy&lt;/code&gt; with &lt;code&gt;-ss&lt;/code&gt; (seek) and &lt;code&gt;-t&lt;/code&gt; (duration) for trimming, FFmpeg can only cut at keyframe boundaries. Your trim points might be off by a few frames. If you need frame-exact cuts, you'll need to re-encode at least the video stream.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Missing moov atom on interrupted encodes.&lt;/strong&gt; If an MP4 encode was interrupted, the moov atom never got written. You can't fix this with &lt;code&gt;-c copy&lt;/code&gt; because the container metadata is corrupt. Tools like &lt;code&gt;untrunc&lt;/code&gt; or &lt;code&gt;recover_mp4&lt;/code&gt; can sometimes reconstruct it, but that's outside FFmpeg's scope.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Audio codec mismatch in MKV to MP4.&lt;/strong&gt; MKV files sometimes contain audio in formats that MP4 doesn't support well (like Vorbis or FLAC). The remux will succeed, but playback might fail. In that case, copy the video but re-encode just the audio: &lt;code&gt;-c:v copy -c:a aac&lt;/code&gt;.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Is -c copy the same as -codec copy?
&lt;/h3&gt;

&lt;p&gt;Yes. &lt;code&gt;-c copy&lt;/code&gt; is shorthand for &lt;code&gt;-codec copy&lt;/code&gt;. Both tell FFmpeg to copy all streams without re-encoding. You can also be specific per stream: &lt;code&gt;-c:v copy&lt;/code&gt; for video only, &lt;code&gt;-c:a copy&lt;/code&gt; for audio only.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does -c copy reduce file size?
&lt;/h3&gt;

&lt;p&gt;Not directly. Since no re-encoding happens, the bitstream stays the same size. The file might be slightly smaller or larger depending on container overhead differences between MKV and MP4, but we're talking kilobytes, not megabytes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I remux a WebM file to MP4?
&lt;/h3&gt;

&lt;p&gt;It depends on the codecs. If the WebM contains VP8 or VP9 video, putting it in an MP4 container is technically possible but playback support is inconsistent. If it contains H.264 (rare in WebM), the remux to MP4 works fine. For VP9 to MP4 conversion that actually plays everywhere, you'd need to re-encode to H.264.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I check what codecs are in my file?
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffmpeg &lt;span class="nt"&gt;-i&lt;/span&gt; input.mkv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look for the "Stream" lines. They'll show something like &lt;code&gt;Stream #0:0: Video: h264&lt;/code&gt; and &lt;code&gt;Stream #0:1: Audio: aac&lt;/code&gt;. If both are compatible with your target container, &lt;code&gt;-c copy&lt;/code&gt; will work.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why is my remuxed file slightly different in size?
&lt;/h3&gt;

&lt;p&gt;Container formats have different overhead. MKV and MP4 store metadata differently, use different packet framing, and have different alignment requirements. A few KB difference is normal and expected.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Last verified: June 2026. CLI examples tested with FFmpeg 7.x, API examples use FFmpeg Micro v1.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ffmpeg</category>
      <category>video</category>
      <category>api</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Repurpose a Podcast Episode into 10 Social Clips Automatically</title>
      <dc:creator>Javid Jamae</dc:creator>
      <pubDate>Tue, 23 Jun 2026 10:20:25 +0000</pubDate>
      <link>https://dev.clauneck.workers.dev/javidjamae/repurpose-a-podcast-episode-into-10-social-clips-automatically-4mpk</link>
      <guid>https://dev.clauneck.workers.dev/javidjamae/repurpose-a-podcast-episode-into-10-social-clips-automatically-4mpk</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.ffmpeg-micro.com/blog/repurpose-podcast-social-clips-ffmpeg" rel="noopener noreferrer"&gt;ffmpeg-micro.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Podcast clipping SaaS tools like Opus Clip, Riverside, Podcastle, and Descript charge between $19 and $500 per month. If you're a solo creator, an agency, or an automation builder, there's a cheaper way: FFmpeg.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Pipeline
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Start with one full podcast episode&lt;/li&gt;
&lt;li&gt;Trim clips with &lt;code&gt;-ss&lt;/code&gt; and &lt;code&gt;-t&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Resize for each platform (9:16 vertical, 1:1 square, 16:9 landscape)&lt;/li&gt;
&lt;li&gt;Add text overlays for muted autoplay&lt;/li&gt;
&lt;li&gt;Batch all 10 clips through a script or API&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Trim a Clip
&lt;/h2&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; podcast.mp4 &lt;span class="nt"&gt;-ss&lt;/span&gt; 00:12:30 &lt;span class="nt"&gt;-t&lt;/span&gt; 00:00:45 &lt;span class="nt"&gt;-c&lt;/span&gt; copy clip1.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or via the FFmpeg Micro API:&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://storage.example.com/podcast-ep42.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="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;"-ss"&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;"00:12:30"&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;"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;"-t"&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;"00:00:45"&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;"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;"-c:v"&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;"libx264"&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;"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;"-crf"&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;"23"&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;h2&gt;
  
  
  Resize for Vertical (9:16)
&lt;/h2&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; clip1.mp4 &lt;span class="nt"&gt;-vf&lt;/span&gt; &lt;span class="s2"&gt;"scale=1080:1920:force_original_aspect_ratio=decrease,pad=1080:1920:(ow-iw)/2:(oh-ih)/2"&lt;/span&gt; vertical-clip1.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Cost Comparison
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Monthly Cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Opus Clip&lt;/td&gt;
&lt;td&gt;$19-$39/mo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Riverside&lt;/td&gt;
&lt;td&gt;$24/mo+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Descript&lt;/td&gt;
&lt;td&gt;$24-$33/mo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FFmpeg Micro&lt;/td&gt;
&lt;td&gt;Pay-per-minute, free tier&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A 1-hour podcast split into 10 clips costs pennies with FFmpeg Micro. Not $24. Pennies.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;-c copy&lt;/code&gt; cuts on keyframes only (may start early)&lt;/li&gt;
&lt;li&gt;Re-encode audio too (&lt;code&gt;-c:a aac&lt;/code&gt;) to avoid drift&lt;/li&gt;
&lt;li&gt;Add &lt;code&gt;-pix_fmt yuv420p&lt;/code&gt; for social platform compatibility&lt;/li&gt;
&lt;li&gt;Keep CRF at 23 or lower for mobile viewing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Read the full guide at &lt;a href="https://www.ffmpeg-micro.com/blog/repurpose-podcast-social-clips-ffmpeg" rel="noopener noreferrer"&gt;ffmpeg-micro.com/blog/repurpose-podcast-social-clips-ffmpeg&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ffmpeg</category>
      <category>podcast</category>
      <category>automation</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How to Encode VP9 Video with FFmpeg: libvpx-vp9 CRF, Bitrate, and Quality Guide</title>
      <dc:creator>Javid Jamae</dc:creator>
      <pubDate>Tue, 23 Jun 2026 10:19:13 +0000</pubDate>
      <link>https://dev.clauneck.workers.dev/javidjamae/how-to-encode-vp9-video-with-ffmpeg-libvpx-vp9-crf-bitrate-and-quality-guide-2ljo</link>
      <guid>https://dev.clauneck.workers.dev/javidjamae/how-to-encode-vp9-video-with-ffmpeg-libvpx-vp9-crf-bitrate-and-quality-guide-2ljo</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.ffmpeg-micro.com/blog/ffmpeg-vp9-encoding-libvpx-guide" rel="noopener noreferrer"&gt;ffmpeg-micro.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;VP9 is Google's open, royalty-free video codec. It powers most of YouTube's streaming library, and every modern browser supports it through the WebM container. If you need high-quality web video without licensing headaches, VP9 with FFmpeg's libvpx-vp9 encoder is the standard choice.&lt;/p&gt;

&lt;p&gt;This guide covers the practical settings: CRF mode, 2-pass encoding, speed tuning, and multithreading.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR: The Default VP9 Command
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffmpeg &lt;span class="nt"&gt;-i&lt;/span&gt; input.mp4 &lt;span class="nt"&gt;-c&lt;/span&gt;:v libvpx-vp9 &lt;span class="nt"&gt;-crf&lt;/span&gt; 31 &lt;span class="nt"&gt;-b&lt;/span&gt;:v 0 &lt;span class="nt"&gt;-c&lt;/span&gt;:a libopus &lt;span class="nt"&gt;-b&lt;/span&gt;:a 128k output.webm
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;CRF 31 with &lt;code&gt;-b:v 0&lt;/code&gt; gives you true constant-quality mode. Opus audio at 128k is the natural pairing for WebM.&lt;/p&gt;

&lt;h2&gt;
  
  
  What CRF Does in VP9
&lt;/h2&gt;

&lt;p&gt;VP9's CRF scale runs from 0 to 63. Lower means better quality and bigger files. This is different from H.264's libx264, which uses a 0-51 range.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You must set &lt;code&gt;-b:v 0&lt;/code&gt; for true CRF mode.&lt;/strong&gt; If you leave out &lt;code&gt;-b:v 0&lt;/code&gt;, FFmpeg treats the CRF value as a constrained quality floor. This is the single most common VP9 encoding mistake.&lt;/p&gt;

&lt;h2&gt;
  
  
  VP9 CRF Values by Use Case
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;CRF&lt;/th&gt;
&lt;th&gt;Use Case&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;15-20&lt;/td&gt;
&lt;td&gt;Archival / master copies&lt;/td&gt;
&lt;td&gt;Near-lossless. Large files.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;24-28&lt;/td&gt;
&lt;td&gt;High-quality streaming&lt;/td&gt;
&lt;td&gt;Visually transparent for most content&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;30-33&lt;/td&gt;
&lt;td&gt;General web video&lt;/td&gt;
&lt;td&gt;Good quality at reasonable file sizes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;36-42&lt;/td&gt;
&lt;td&gt;Low-bandwidth delivery&lt;/td&gt;
&lt;td&gt;Noticeable softness, but watchable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;45+&lt;/td&gt;
&lt;td&gt;Previews / thumbnails&lt;/td&gt;
&lt;td&gt;Significant quality loss&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Speed Settings
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;-cpu-used&lt;/code&gt; is a number from 0 to 8. Zero is the slowest and highest quality. For most encodes, &lt;code&gt;-deadline good -cpu-used 2&lt;/code&gt; is the sweet spot.&lt;/p&gt;

&lt;h2&gt;
  
  
  2-Pass Encoding
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffmpeg &lt;span class="nt"&gt;-i&lt;/span&gt; input.mp4 &lt;span class="nt"&gt;-c&lt;/span&gt;:v libvpx-vp9 &lt;span class="nt"&gt;-b&lt;/span&gt;:v 2M &lt;span class="nt"&gt;-deadline&lt;/span&gt; good &lt;span class="nt"&gt;-cpu-used&lt;/span&gt; 2 &lt;span class="nt"&gt;-row-mt&lt;/span&gt; 1 &lt;span class="nt"&gt;-pass&lt;/span&gt; 1 &lt;span class="nt"&gt;-an&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; null /dev/null
ffmpeg &lt;span class="nt"&gt;-i&lt;/span&gt; input.mp4 &lt;span class="nt"&gt;-c&lt;/span&gt;:v libvpx-vp9 &lt;span class="nt"&gt;-b&lt;/span&gt;:v 2M &lt;span class="nt"&gt;-deadline&lt;/span&gt; good &lt;span class="nt"&gt;-cpu-used&lt;/span&gt; 2 &lt;span class="nt"&gt;-row-mt&lt;/span&gt; 1 &lt;span class="nt"&gt;-pass&lt;/span&gt; 2 &lt;span class="nt"&gt;-c&lt;/span&gt;:a libopus &lt;span class="nt"&gt;-b&lt;/span&gt;:a 128k output.webm
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Multithreading
&lt;/h2&gt;

&lt;p&gt;Always enable &lt;code&gt;-row-mt 1&lt;/code&gt;. It can cut encode time by 50% or more with no quality penalty.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Forgetting &lt;code&gt;-b:v 0&lt;/code&gt; in CRF mode&lt;/li&gt;
&lt;li&gt;Using &lt;code&gt;-cpu-used 0&lt;/code&gt; (extremely slow)&lt;/li&gt;
&lt;li&gt;Skipping &lt;code&gt;-row-mt 1&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Using .mp4 container (use .webm instead)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Read the full guide with API examples at &lt;a href="https://www.ffmpeg-micro.com/blog/ffmpeg-vp9-encoding-libvpx-guide" rel="noopener noreferrer"&gt;ffmpeg-micro.com/blog/ffmpeg-vp9-encoding-libvpx-guide&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ffmpeg</category>
      <category>webdev</category>
      <category>video</category>
      <category>programming</category>
    </item>
    <item>
      <title>Remotion + FFmpeg Micro: Programmatic Video Without the Lambda Bill</title>
      <dc:creator>Javid Jamae</dc:creator>
      <pubDate>Mon, 22 Jun 2026 18:57:53 +0000</pubDate>
      <link>https://dev.clauneck.workers.dev/javidjamae/remotion-ffmpeg-micro-programmatic-video-without-the-lambda-bill-3knf</link>
      <guid>https://dev.clauneck.workers.dev/javidjamae/remotion-ffmpeg-micro-programmatic-video-without-the-lambda-bill-3knf</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.ffmpeg-micro.com/blog/remotion-ffmpeg-micro-serverless-video" rel="noopener noreferrer"&gt;ffmpeg-micro.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Remotion lets you build videos the same way you build React components. But moving those compositions from your local dev server to a production pipeline usually means setting up AWS Lambda, managing cold starts, and watching your bill climb with every render. The post-processing step, compressing, converting, and combining your rendered output, doesn't need any of that infrastructure. FFmpeg Micro handles it with a single API call.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Use Remotion to compose and render your videos. Use FFmpeg Micro's API to compress, convert, add audio, and batch-process the output. No Lambda configuration, no self-hosted FFmpeg binaries, no DevOps overhead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Remotion Ends and FFmpeg Starts
&lt;/h2&gt;

&lt;p&gt;Remotion is a composition and rendering tool. You define scenes in React, animate them with code, and export frames to video. It's excellent at that job.&lt;/p&gt;

&lt;p&gt;But rendering is only half the pipeline. After Remotion hands you an MP4, you still need to compress it for delivery, convert it to WebM for browser compatibility, mix in a background music track, or pull a thumbnail frame for your Open Graph tags. That's FFmpeg territory.&lt;/p&gt;

&lt;p&gt;The traditional approach is to install FFmpeg on a server, write shell scripts, and manage the infrastructure yourself. Or you bolt FFmpeg onto the same Lambda setup you're already using for Remotion rendering, adding complexity to a system that's already complex enough.&lt;/p&gt;

&lt;p&gt;FFmpeg Micro gives you a REST API instead. POST your video URL, specify what you want, and poll for the result. It runs on managed infrastructure, so you don't install anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  Compress Remotion Output
&lt;/h2&gt;

&lt;p&gt;Remotion renders tend to be large. A 60-second 1080p composition can easily produce a 200MB+ file because Remotion prioritizes quality over file size during rendering. That's fine for an intermediate artifact, but you can't serve a 200MB video to users.&lt;/p&gt;

&lt;p&gt;FFmpeg Micro's preset system handles compression in one call:&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-bucket.s3.amazonaws.com/remotion-render.mp4"}],
    "outputFormat": "mp4",
    "preset": {"quality": "high", "resolution": "1080p"}
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;quality&lt;/code&gt; preset maps to a sensible CRF value and encoding configuration, so you don't need to memorize FFmpeg flags. For most Remotion output, the "high" preset cuts file size by 60-80% with no visible quality loss.&lt;/p&gt;

&lt;p&gt;For more control over compression settings, see the full guide on &lt;a href="https://dev.clauneck.workers.dev/blog/ffmpeg-compress-video-api"&gt;compressing video with the FFmpeg Micro API&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Convert to WebM or MOV
&lt;/h2&gt;

&lt;p&gt;Remotion outputs MP4 by default. If you need WebM for better browser-native playback or MOV for Apple ecosystem delivery, change the &lt;code&gt;outputFormat&lt;/code&gt; and specify your codec:&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-bucket.s3.amazonaws.com/remotion-render.mp4"}],
    "outputFormat": "webm",
    "options": [
      {"option": "-c:v", "argument": "libvpx-vp9"},
      {"option": "-crf", "argument": "30"},
      {"option": "-b:v", "argument": "0"}
    ]
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Setting &lt;code&gt;-b:v&lt;/code&gt; to &lt;code&gt;0&lt;/code&gt; tells VP9 to use constant quality mode, letting the CRF value drive the quality-to-size tradeoff. This is the recommended approach for WebM encoding when you care more about consistent quality than hitting a specific bitrate target.&lt;/p&gt;

&lt;p&gt;Need to support multiple formats from a single Remotion render? Fire off parallel API calls for each format. The &lt;a href="https://dev.clauneck.workers.dev/blog/ffmpeg-convert-video-format"&gt;format conversion guide&lt;/a&gt; covers the full set of supported output types.&lt;/p&gt;

&lt;h2&gt;
  
  
  Add Background Audio to a Rendered Video
&lt;/h2&gt;

&lt;p&gt;Remotion supports audio in compositions, but sometimes you want to add music or voiceover after rendering. Maybe your audio asset wasn't ready during the render step. Maybe you're A/B testing different background tracks against the same video.&lt;/p&gt;

&lt;p&gt;FFmpeg Micro accepts multiple inputs, so you can merge a video and audio file in a single request:&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-bucket.s3.amazonaws.com/remotion-render.mp4"},
      {"url": "https://your-bucket.s3.amazonaws.com/background-music.mp3"}
    ],
    "outputFormat": "mp4",
    "options": [
      {"option": "-shortest", "argument": ""}
    ]
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;-shortest&lt;/code&gt; flag trims the output to the length of the shorter input, so a 3-minute music track won't extend a 45-second video with silence. The API supports up to 10 inputs per request, which also opens the door to concatenating separately rendered Remotion segments (intros, outros, chapters) into a single deliverable.&lt;/p&gt;

&lt;p&gt;More audio mixing patterns are covered in the &lt;a href="https://dev.clauneck.workers.dev/blog/ffmpeg-merge-audio-video"&gt;audio merging guide&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Batch Processing Personalized Videos
&lt;/h2&gt;

&lt;p&gt;This is where the Remotion + FFmpeg Micro combination really shines. Say you're rendering 500 personalized onboarding videos, each with a different user's name and data. Remotion handles the personalized rendering. FFmpeg Micro handles the post-processing at scale.&lt;/p&gt;

&lt;p&gt;A simple TypeScript helper to kick off batch compression:&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;processRemotionBatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;videoUrls&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="nx"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;jobs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;videoUrls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&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;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="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="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="nx"&gt;url&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
          &lt;span class="na"&gt;outputFormat&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mp4&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;preset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;quality&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;high&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;resolution&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;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="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="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="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;jobs&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;Each call returns a job ID. Poll &lt;code&gt;GET /v1/transcodes/{id}&lt;/code&gt; until the status flips from &lt;code&gt;processing&lt;/code&gt; to &lt;code&gt;completed&lt;/code&gt;, then grab the download URL from &lt;code&gt;GET /v1/transcodes/{id}/download&lt;/code&gt;. For large batches, add a concurrency limiter so you don't fire 500 requests simultaneously.&lt;/p&gt;

&lt;p&gt;You can also extract thumbnail frames from each rendered video for email previews or social cards. The &lt;a href="https://dev.clauneck.workers.dev/blog/ffmpeg-generate-video-thumbnails"&gt;thumbnail generation guide&lt;/a&gt; walks through that workflow.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Remotion's default codec may surprise you.&lt;/strong&gt; Remotion typically outputs H.264 in an MP4 container, but the encoding settings vary based on your Remotion config. If you're passing the output to FFmpeg Micro with custom options (not presets), make sure your flags are compatible with the input codec. When in doubt, use the preset system and let FFmpeg Micro pick the right encoding chain.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Large renders need the presigned upload flow.&lt;/strong&gt; If your Remotion output lives on a public URL (S3, GCS, R2), pass it directly as an input URL. But if you're rendering locally and the file is only on disk, you'll need to upload it first. FFmpeg Micro provides a three-step flow: &lt;code&gt;POST /v1/upload/presigned-url&lt;/code&gt; to get a signed GCS URL, &lt;code&gt;PUT&lt;/code&gt; the file to that URL, then &lt;code&gt;POST /v1/upload/confirm&lt;/code&gt; to register it. Use this instead of trying to serve files from localhost.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Poll, don't webhook.&lt;/strong&gt; FFmpeg Micro uses a polling model for job status. Hit &lt;code&gt;GET /v1/transcodes/{id}&lt;/code&gt; on an interval. A job moves through &lt;code&gt;queued&lt;/code&gt;, &lt;code&gt;processing&lt;/code&gt;, and then settles on &lt;code&gt;completed&lt;/code&gt; or &lt;code&gt;failed&lt;/code&gt;. A 2-3 second polling interval works well for most video lengths. Don't assume the job is done just because the POST returned 201. That 201 means the job was accepted, not finished.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Can FFmpeg Micro fully replace Remotion Lambda for rendering?&lt;/strong&gt;&lt;br&gt;
No. FFmpeg Micro is a post-processing API, not a rendering engine. You still need Remotion (locally, via Lambda, or via Remotion Cloud) to turn your React compositions into video files. FFmpeg Micro picks up after the render step for compression, conversion, and audio mixing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's the maximum file size FFmpeg Micro can process?&lt;/strong&gt;&lt;br&gt;
The presigned upload flow supports files up to 2GB through Google Cloud Storage. For URL-based inputs, there's no strict limit on the API side. Most Remotion renders, even long-form content at 1080p, won't hit a size constraint.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is it cheaper than running FFmpeg on Lambda?&lt;/strong&gt;&lt;br&gt;
For post-processing tasks, yes. AWS Lambda bills by duration and memory. A 10-minute 1080p transcode on Lambda with 3GB memory costs roughly $0.30 per run, and you still need to manage the deployment, layers, and timeout configuration. FFmpeg Micro charges per job with predictable pricing and zero infrastructure management.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I chain multiple FFmpeg operations on the same video?&lt;/strong&gt;&lt;br&gt;
Each API call runs one operation. If you need to compress and then convert to WebM, that's two sequential calls. Use the download URL from the first job as the input URL for the second. In practice, you can often combine operations into a single call by passing the right combination of options and output format.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does FFmpeg Micro support Remotion's transparent video (ProRes) output?&lt;/strong&gt;&lt;br&gt;
If you configure Remotion to output ProRes in a MOV container, you can pass that URL to FFmpeg Micro and convert it to any supported format. This is useful when you need an alpha channel during composition but want to deliver H.264 MP4 to end users.&lt;/p&gt;




&lt;p&gt;Ready to skip the Lambda configuration? Grab a free API key at &lt;a href="https://ffmpeg-micro.com" rel="noopener noreferrer"&gt;ffmpeg-micro.com&lt;/a&gt; and start processing your Remotion renders in minutes.&lt;/p&gt;

</description>
      <category>remotion</category>
      <category>react</category>
      <category>ffmpeg</category>
      <category>serverless</category>
    </item>
    <item>
      <title>How to Change Video Frame Rate with FFmpeg</title>
      <dc:creator>Javid Jamae</dc:creator>
      <pubDate>Mon, 22 Jun 2026 18:57:38 +0000</pubDate>
      <link>https://dev.clauneck.workers.dev/javidjamae/how-to-change-video-frame-rate-with-ffmpeg-24h8</link>
      <guid>https://dev.clauneck.workers.dev/javidjamae/how-to-change-video-frame-rate-with-ffmpeg-24h8</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.ffmpeg-micro.com/blog/ffmpeg-change-video-frame-rate" rel="noopener noreferrer"&gt;ffmpeg-micro.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Different platforms demand different frame rates. A 60fps screen recording won't fly on a broadcast system expecting 29.97fps, and uploading 24fps cinema footage to a platform that expects 30fps can cause playback stutters. FFmpeg gives you several ways to handle frame rate conversion, but the options can be confusing. This guide covers the practical approaches that actually work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Quick answer:&lt;/strong&gt; To change a video's frame rate to 30fps with FFmpeg, run &lt;code&gt;ffmpeg -i input.mp4 -r 30 output.mp4&lt;/code&gt;. This re-encodes the video and adjusts the frame timing to match the new rate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check Your Current Frame Rate
&lt;/h2&gt;

&lt;p&gt;Before converting anything, check what you're working with. Use &lt;code&gt;ffprobe&lt;/code&gt; to inspect the video:&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; error &lt;span class="nt"&gt;-select_streams&lt;/span&gt; v:0 &lt;span class="nt"&gt;-show_entries&lt;/span&gt; &lt;span class="nv"&gt;stream&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;r_frame_rate,avg_frame_rate &lt;span class="nt"&gt;-of&lt;/span&gt; &lt;span class="nv"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;noprint_wrappers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 input.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This prints both the stream's base frame rate (&lt;code&gt;r_frame_rate&lt;/code&gt;) and the average frame rate (&lt;code&gt;avg_frame_rate&lt;/code&gt;). You'll see output like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="py"&gt;r_frame_rate&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;30/1&lt;/span&gt;
&lt;span class="py"&gt;avg_frame_rate&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;30/1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A value of &lt;code&gt;30/1&lt;/code&gt; means exactly 30fps. You might also see &lt;code&gt;30000/1001&lt;/code&gt;, which is the NTSC standard 29.97fps. Knowing your source frame rate helps you pick the right conversion approach.&lt;/p&gt;

&lt;h2&gt;
  
  
  Change Frame Rate with the -r Flag
&lt;/h2&gt;

&lt;p&gt;The simplest way to change frame rate is the &lt;code&gt;-r&lt;/code&gt; output flag. Place it after your input file:&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;-r&lt;/span&gt; 24 output.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This re-encodes the entire video and sets the output to exactly 24fps. FFmpeg automatically drops or duplicates frames to hit the target rate. If your source is 60fps and you target 24fps, FFmpeg drops roughly 60% of the frames. If you go the other direction, from 24fps up to 60fps, it duplicates frames to fill the gaps.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;-r&lt;/code&gt; flag is the go-to method for most frame rate conversions. It handles the math for you and keeps audio in sync automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Change Frame Rate with the fps Filter
&lt;/h2&gt;

&lt;p&gt;For more control over how frames are selected during conversion, use the &lt;code&gt;fps&lt;/code&gt; video filter:&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;"fps=60"&lt;/span&gt; output.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;fps&lt;/code&gt; filter lets you specify rounding behavior, which matters when the source and target frame rates don't divide evenly. You can control how FFmpeg rounds timestamps:&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;"fps=fps=30:round=near"&lt;/span&gt; output.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;round&lt;/code&gt; parameter accepts &lt;code&gt;near&lt;/code&gt; (nearest, the default), &lt;code&gt;up&lt;/code&gt;, &lt;code&gt;down&lt;/code&gt;, and &lt;code&gt;zero&lt;/code&gt; (round toward zero). This level of control is useful when you need frame-accurate output for editing workflows or broadcast delivery.&lt;/p&gt;

&lt;p&gt;Both the &lt;code&gt;-r&lt;/code&gt; flag and the &lt;code&gt;fps&lt;/code&gt; filter re-encode the video. There's no way to change frame rate without re-encoding, since the frame timing data is baked into the compressed stream.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common Frame Rates and When to Use Them
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Frame Rate&lt;/th&gt;
&lt;th&gt;Common Name&lt;/th&gt;
&lt;th&gt;Typical Use&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;24fps&lt;/td&gt;
&lt;td&gt;Film&lt;/td&gt;
&lt;td&gt;Cinema, narrative content, film-look videos&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;25fps&lt;/td&gt;
&lt;td&gt;PAL&lt;/td&gt;
&lt;td&gt;European broadcast television&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;29.97fps&lt;/td&gt;
&lt;td&gt;NTSC&lt;/td&gt;
&lt;td&gt;North American broadcast television&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;30fps&lt;/td&gt;
&lt;td&gt;Web&lt;/td&gt;
&lt;td&gt;YouTube, social media, web video&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;60fps&lt;/td&gt;
&lt;td&gt;High frame rate&lt;/td&gt;
&lt;td&gt;Gaming footage, sports, smooth motion&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Most web video does fine at 30fps. If you're targeting YouTube or social platforms, 30fps is a safe default. For cinematic content, 24fps gives you that film-look cadence. And if you're working with broadcast delivery, match the regional standard: 29.97fps for NTSC territories, 25fps for PAL.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frame Rate Conversion via API
&lt;/h2&gt;

&lt;p&gt;If you're building frame rate conversion into an application or pipeline, the &lt;a href="https://ffmpeg-micro.com" rel="noopener noreferrer"&gt;FFmpeg Micro&lt;/a&gt; API lets you run FFmpeg operations over HTTP without managing servers or FFmpeg installations.&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": "-r", "argument": "30"}
    ]
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The API accepts the same FFmpeg flags you'd use on the command line. The &lt;code&gt;options&lt;/code&gt; array takes objects with &lt;code&gt;option&lt;/code&gt; and &lt;code&gt;argument&lt;/code&gt; fields. You point it at a source URL, specify your output format, and get back a processed file.&lt;/p&gt;

&lt;p&gt;This is especially useful when you need to normalize frame rates across user-uploaded content, or when your backend needs to convert video without installing FFmpeg on every server.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Try frame rate conversion via API.&lt;/strong&gt; You can get a free API key at &lt;a href="https://ffmpeg-micro.com" rel="noopener noreferrer"&gt;ffmpeg-micro.com&lt;/a&gt;.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Input -r vs. output -r.&lt;/strong&gt; Placing &lt;code&gt;-r&lt;/code&gt; before the input file changes how FFmpeg interprets the source timing, not the actual output frame rate:&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;# This changes input interpretation, NOT output frame rate&lt;/span&gt;
ffmpeg &lt;span class="nt"&gt;-r&lt;/span&gt; 15 &lt;span class="nt"&gt;-i&lt;/span&gt; input.mp4 &lt;span class="nt"&gt;-c&lt;/span&gt; copy output.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is mainly useful for raw video or image sequences where there's no embedded timing information. For normal video files, always place &lt;code&gt;-r&lt;/code&gt; after the input (&lt;code&gt;-i&lt;/code&gt;) to set the output frame rate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Frame duplication is not interpolation.&lt;/strong&gt; When you upscale from 24fps to 60fps, FFmpeg duplicates existing frames. It doesn't generate new in-between frames. The result looks identical to the 24fps source, just with repeated frames. If you need actual motion interpolation (generating new frames between existing ones), you'd need the &lt;code&gt;minterpolate&lt;/code&gt; filter, which is significantly slower and sometimes produces artifacts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Audio stays synced.&lt;/strong&gt; Both the &lt;code&gt;-r&lt;/code&gt; flag and the &lt;code&gt;fps&lt;/code&gt; filter keep audio in sync automatically. FFmpeg adjusts the video timeline without touching the audio stream. You don't need to add any extra flags for audio synchronization during frame rate conversion.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Does changing frame rate affect video quality?&lt;/strong&gt;&lt;br&gt;
Changing frame rate requires re-encoding, which means a generation loss in quality. You can minimize this by using a high-quality codec like H.264 or H.265 with a reasonable CRF value (e.g., &lt;code&gt;-crf 18&lt;/code&gt;). But going from 60fps down to 24fps means permanently discarding 60% of your frames.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I change frame rate without re-encoding?&lt;/strong&gt;&lt;br&gt;
No. Frame rate is embedded in the video stream's timing data. You can't change it with &lt;code&gt;-c copy&lt;/code&gt; (stream copy) in any meaningful way. The video must be decoded and re-encoded with the new frame timing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What happens to variable frame rate (VFR) video?&lt;/strong&gt;&lt;br&gt;
Screen recordings and phone videos often use variable frame rate. Running them through FFmpeg with &lt;code&gt;-r 30&lt;/code&gt; or &lt;code&gt;-vf "fps=30"&lt;/code&gt; converts them to constant frame rate (CFR), which fixes many editing and playback issues. This is actually one of the most common reasons to do a frame rate conversion.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I convert 29.97fps to exactly 30fps?&lt;/strong&gt;&lt;br&gt;
Use the fractional notation: &lt;code&gt;ffmpeg -i input.mp4 -r 30 output.mp4&lt;/code&gt;. If your source is 29.97fps (30000/1001), FFmpeg handles the slight timing adjustment. For the reverse, targeting NTSC: &lt;code&gt;ffmpeg -i input.mp4 -r 30000/1001 output.mp4&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Should I change frame rate before or after other edits?&lt;/strong&gt;&lt;br&gt;
Do your frame rate conversion early in the pipeline, especially if you're also &lt;a href="https://dev.clauneck.workers.dev/blog/ffmpeg-speed-up-slow-down-video"&gt;changing video speed&lt;/a&gt; or &lt;a href="https://dev.clauneck.workers.dev/blog/ffmpeg-crop-resize-video"&gt;cropping and resizing&lt;/a&gt;. Converting frame rate first gives your other filters a consistent time base to work with. If you're &lt;a href="https://dev.clauneck.workers.dev/blog/extract-frames-from-video-ffmpeg"&gt;extracting frames&lt;/a&gt; from the video, convert the frame rate first so you get the right number of output images.&lt;/p&gt;

</description>
      <category>ffmpeg</category>
      <category>video</category>
      <category>tutorial</category>
      <category>api</category>
    </item>
    <item>
      <title>Build an AI Video Editing Agent with Claude and FFmpeg Micro MCP</title>
      <dc:creator>Javid Jamae</dc:creator>
      <pubDate>Fri, 19 Jun 2026 10:15:23 +0000</pubDate>
      <link>https://dev.clauneck.workers.dev/javidjamae/build-an-ai-video-editing-agent-with-claude-and-ffmpeg-micro-mcp-4915</link>
      <guid>https://dev.clauneck.workers.dev/javidjamae/build-an-ai-video-editing-agent-with-claude-and-ffmpeg-micro-mcp-4915</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.ffmpeg-micro.com/blog/build-ai-video-editing-agent-claude-mcp" rel="noopener noreferrer"&gt;ffmpeg-micro.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Most developers using AI for video editing are still doing it manually. They prompt Claude or ChatGPT to generate an FFmpeg command, copy it, paste it into a terminal, debug the errors, and repeat. That works for one-off jobs. But if you're building a product or workflow where users describe video edits in plain English and get results back automatically, you need something different.&lt;/p&gt;

&lt;p&gt;You need an agent. An AI agent that understands video operations, calls the right API, and returns the finished video without anyone touching a terminal.&lt;/p&gt;

&lt;p&gt;This tutorial builds exactly that using Claude and the FFmpeg Micro MCP server. By the end, you'll have a working agent that takes natural language instructions like "trim this video to the first 30 seconds and convert it to 720p" and executes the entire job.&lt;/p&gt;

&lt;h2&gt;
  
  
  What You're Building
&lt;/h2&gt;

&lt;p&gt;The flow looks like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;User describes a video edit in plain English&lt;/li&gt;
&lt;li&gt;Claude interprets the request and picks the right FFmpeg operations&lt;/li&gt;
&lt;li&gt;The MCP server calls the FFmpeg Micro cloud API&lt;/li&gt;
&lt;li&gt;The API processes the video and returns a download URL&lt;/li&gt;
&lt;li&gt;The agent delivers the result&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;No local FFmpeg installation. No shell commands. No manual intervention after the initial prompt.&lt;/p&gt;

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

&lt;p&gt;You need three things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;An FFmpeg Micro account.&lt;/strong&gt; Sign up at &lt;a href="https://www.ffmpeg-micro.com" rel="noopener noreferrer"&gt;ffmpeg-micro.com&lt;/a&gt; (free tier works for testing).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Claude Desktop, Claude Code, or Cursor.&lt;/strong&gt; Any MCP-compatible AI tool works.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;2 minutes for setup.&lt;/strong&gt; The MCP server config is a single JSON block.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 1: Connect the FFmpeg Micro MCP Server
&lt;/h2&gt;

&lt;p&gt;Add this to your MCP configuration 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;"mcpServers"&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;"ffmpeg-micro"&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;"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;"http"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&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://mcp.ffmpeg-micro.com"&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;The first time you use it, your browser opens for OAuth sign-in. After that, the token is cached.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Config file locations:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Claude Desktop: &lt;code&gt;~/Library/Application Support/Claude/claude_desktop_config.json&lt;/code&gt; (Mac)&lt;/li&gt;
&lt;li&gt;Claude Code: project-level &lt;code&gt;.mcp.json&lt;/code&gt; or &lt;code&gt;~/.claude.json&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Cursor: Settings &amp;gt; Cursor Settings &amp;gt; MCP&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your tool doesn't support HTTP MCP yet, use the stdio transport instead:&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;"mcpServers"&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;"ffmpeg-micro"&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;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&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="s2"&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;"@ffmpeg-micro/mcp-server"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"env"&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;"FFMPEG_MICRO_API_KEY"&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_api_key_here"&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;h2&gt;
  
  
  Step 2: Understand the Available Tools
&lt;/h2&gt;

&lt;p&gt;Once connected, Claude can call six MCP tools:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&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;&lt;code&gt;transcode_video&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Creates a transcode job (returns immediately)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;transcode_and_wait&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Creates a job and polls until it finishes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;get_transcode&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Checks the status of a specific job&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;list_transcodes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Lists recent jobs with optional filters&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;get_download_url&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Generates a signed download link&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cancel_transcode&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Cancels a queued or in-progress job&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For most agent workflows, &lt;code&gt;transcode_and_wait&lt;/code&gt; is the one you want. It creates the job and blocks until the video is ready, so the agent can return the download link in a single turn.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Run Your First Agent Command
&lt;/h2&gt;

&lt;p&gt;Open Claude (or whichever tool you connected) and try this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Take this video and convert it to 720p MP4 with medium quality:
https://www.ffmpeg-micro.com/samples/quickstart-sample.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Claude reads your request, picks the right tool (&lt;code&gt;transcode_and_wait&lt;/code&gt;), constructs the API call with the correct parameters, and waits for the result. You get back a download URL for the processed video.&lt;/p&gt;

&lt;p&gt;Behind the scenes, the MCP call looks like this:&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="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;"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://www.ffmpeg-micro.com/samples/quickstart-sample.mp4"&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;"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="w"&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;"medium"&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;"720p"&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;You didn't write that JSON. Claude figured it out from your plain English instruction.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Try More Complex Operations
&lt;/h2&gt;

&lt;p&gt;The agent handles more than basic transcoding. Try these:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Extract audio from a video:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Extract the audio from this video as an MP3:
https://www.ffmpeg-micro.com/samples/quickstart-sample.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Compress for web delivery:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Compress this video for web. Target quality CRF 28, use H.265 encoding:
https://www.ffmpeg-micro.com/samples/quickstart-sample.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Claude translates that into the right FFmpeg options:&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="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;"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://www.ffmpeg-micro.com/samples/quickstart-sample.mp4"&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;"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;"-c:v"&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;"libx265"&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;"-crf"&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;"28"&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;"-preset"&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;"medium"&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;&lt;strong&gt;Trim a section:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Trim this video from 5 seconds to 15 seconds:
https://www.ffmpeg-micro.com/samples/quickstart-sample.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Stitch multiple videos together:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Concatenate these two videos into one MP4:
1. https://www.ffmpeg-micro.com/samples/quickstart-sample.mp4
2. https://www.ffmpeg-micro.com/samples/quickstart-sample.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every operation goes through the same flow: natural language in, processed video out.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Build a System Prompt for Your Agent
&lt;/h2&gt;

&lt;p&gt;If you're building this into a product, give Claude a system prompt that constrains the agent to video operations:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;You are a video editing assistant. You have access to the FFmpeg Micro
MCP server for video processing. When a user describes a video edit:

1. Identify the operation (transcode, trim, compress, extract audio, etc.)
2. Use the transcode_and_wait tool with the correct parameters
3. Return the download URL to the user
4. If the job fails, explain what went wrong

Always use preset mode for simple operations (quality + resolution).
Use options mode for specific codecs, filters, or advanced FFmpeg flags.
Never ask the user for technical parameters — infer them from the request.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This turns Claude from a general-purpose assistant into a focused video editing agent that handles the technical details automatically.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Using a non-public video URL.&lt;/strong&gt; The FFmpeg Micro API needs to fetch your video. If the URL requires authentication or is behind a firewall, the job will fail. For private files, use the upload flow first (presigned URL, PUT, confirm) to get a cloud storage URL.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Forgetting to check job status.&lt;/strong&gt; If you use &lt;code&gt;transcode_video&lt;/code&gt; instead of &lt;code&gt;transcode_and_wait&lt;/code&gt;, the job runs asynchronously. You'll need to call &lt;code&gt;get_transcode&lt;/code&gt; to check when it's done. For agent workflows, &lt;code&gt;transcode_and_wait&lt;/code&gt; avoids this entirely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Asking for unsupported output formats.&lt;/strong&gt; The API supports mp4, webm, avi, mov, mkv, gif, mp3, wav, and more. But if you ask Claude to output a format that FFmpeg doesn't support for your input, the job will fail with a clear error message.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Approach Beats Shell Scripts
&lt;/h2&gt;

&lt;p&gt;The traditional way to automate video editing with AI is prompting for FFmpeg commands, then running them locally. That works, but it has problems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You need FFmpeg installed on every machine&lt;/li&gt;
&lt;li&gt;Complex operations need multiple commands chained together&lt;/li&gt;
&lt;li&gt;Error handling is on you (wrong codecs, missing dependencies, file permission issues)&lt;/li&gt;
&lt;li&gt;Scaling means provisioning and managing servers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With the MCP agent approach, Claude handles the complexity and FFmpeg Micro handles the infrastructure. Your code (or your users) just describes what they want in plain English.&lt;/p&gt;

&lt;p&gt;FFmpeg Micro processes over 50,000 API calls per month. A 1-minute video transcode takes about 3 seconds. Pricing starts with a free tier, so you can prototype without spending anything.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Can I use this with GPT-4 or other LLMs instead of Claude?
&lt;/h3&gt;

&lt;p&gt;The MCP protocol is currently supported by Claude Desktop, Claude Code, Cursor, Windsurf, and other MCP-compatible tools. For OpenAI or other providers, you'd call the FFmpeg Micro REST API directly instead of going through MCP. The API itself works with any HTTP client.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do I need to know FFmpeg commands to use this?
&lt;/h3&gt;

&lt;p&gt;No. That's the point. The agent translates natural language to the right API calls. You say "compress this video" and Claude figures out the codec, CRF, and preset. If you do know FFmpeg, you can be more specific ("use libx265 with CRF 24"), and the agent will use those exact settings.&lt;/p&gt;

&lt;h3&gt;
  
  
  What happens if my video is too large?
&lt;/h3&gt;

&lt;p&gt;FFmpeg Micro supports files up to 2GB. For larger files, you'll need to split them first. The free tier has usage limits. Check &lt;a href="https://www.ffmpeg-micro.com/pricing" rel="noopener noreferrer"&gt;ffmpeg-micro.com/pricing&lt;/a&gt; for details on each plan.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I chain multiple operations in one request?
&lt;/h3&gt;

&lt;p&gt;Each API call handles one operation. But the agent can chain them automatically. Ask Claude "trim this video to 30 seconds, then convert to 720p WebM" and it will run two sequential transcode jobs, using the output of the first as the input for the second.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is the MCP server open source?
&lt;/h3&gt;

&lt;p&gt;Yes. The FFmpeg Micro MCP server is published as &lt;code&gt;@ffmpeg-micro/mcp-server&lt;/code&gt; on npm. You can inspect the source, fork it, or contribute.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>mcp</category>
      <category>claude</category>
      <category>ffmpeg</category>
    </item>
    <item>
      <title>How to Use FFmpeg with TypeScript (No Installation Required)</title>
      <dc:creator>Javid Jamae</dc:creator>
      <pubDate>Fri, 19 Jun 2026 10:14:49 +0000</pubDate>
      <link>https://dev.clauneck.workers.dev/javidjamae/how-to-use-ffmpeg-with-typescript-no-installation-required-2geb</link>
      <guid>https://dev.clauneck.workers.dev/javidjamae/how-to-use-ffmpeg-with-typescript-no-installation-required-2geb</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.ffmpeg-micro.com/blog/how-to-use-ffmpeg-with-typescript" rel="noopener noreferrer"&gt;ffmpeg-micro.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;You need to process video in your TypeScript project. Maybe you're building a Next.js app that generates thumbnails, a Bun server that transcodes user uploads, or an Express API that automates video workflows. You search for "ffmpeg typescript" and find a mess of untyped wrappers, broken binaries, and workarounds.&lt;/p&gt;

&lt;p&gt;TypeScript developers deserve type-safe video processing. But most approaches to FFmpeg in the Node.js ecosystem were built for JavaScript first, and the TypeScript experience is an afterthought. There are several paths, each with real tradeoffs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using child_process with TypeScript
&lt;/h2&gt;

&lt;p&gt;The lowest-level approach. Install FFmpeg on your system and call it via Node's &lt;code&gt;child_process&lt;/code&gt;:&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;spawn&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;child_process&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="nf"&gt;spawn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ffmpeg&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="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;-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="s1"&gt;libx264&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-crf&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;23&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&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="s1"&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="s1"&gt;-c:a&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;aac&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&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="s1"&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="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="nx"&gt;ffmpeg&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;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;data&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Buffer&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;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;`FFmpeg: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&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="nx"&gt;ffmpeg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;close&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&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="nx"&gt;code&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="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;`FFmpeg exited with code &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;code&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="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="s1"&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;TypeScript's type annotations give you &lt;code&gt;Buffer&lt;/code&gt; and &lt;code&gt;number | null&lt;/code&gt; on the callbacks. But that's where the type safety ends. The FFmpeg arguments are just strings. Typo in &lt;code&gt;-c:v&lt;/code&gt;? TypeScript won't catch it. Wrong codec name? You'll find out at runtime. And you still need FFmpeg installed on every machine that runs your code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using fluent-ffmpeg with TypeScript
&lt;/h2&gt;

&lt;p&gt;fluent-ffmpeg has community-maintained type definitions via &lt;code&gt;@types/fluent-ffmpeg&lt;/code&gt;:&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="nx"&gt;ffmpeg&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;fluent-ffmpeg&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;ffmpeg&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="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;videoCodec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;libx264&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;audioCodec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;aac&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;size&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1280x720&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;end&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="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="s1"&gt;Done&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Error&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;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="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&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;The types cover the chainable API, so you get autocomplete for methods like &lt;code&gt;.videoCodec()&lt;/code&gt; and &lt;code&gt;.size()&lt;/code&gt;. But the types are community-maintained and frequently lag behind the library. You'll hit &lt;code&gt;any&lt;/code&gt; types in edge cases, especially around filters and complex operations. And fluent-ffmpeg still requires a local FFmpeg binary. In Docker deployments, that adds 80-200MB to your image.&lt;/p&gt;

&lt;h2&gt;
  
  
  ffmpeg.wasm for Browser-Side TypeScript
&lt;/h2&gt;

&lt;p&gt;ffmpeg.wasm compiles FFmpeg to WebAssembly and ships with its own TypeScript types:&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="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;inputData&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;-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="s1"&gt;libx264&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="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 binary installation. Runs in the browser or Node.js. But processing is 10-20x slower than native FFmpeg, memory is limited to what the browser tab can allocate, and not all codecs are available in the WASM build. Fine for trimming a 30-second clip. Don't try transcoding a 1GB file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cloud API: Type-Safe FFmpeg Without Installing Anything
&lt;/h2&gt;

&lt;p&gt;If you don't want to manage FFmpeg binaries, deal with platform-specific builds, or add hundreds of megabytes to your Docker image, a cloud API is the cleanest path for TypeScript developers.&lt;/p&gt;

&lt;p&gt;FFmpeg Micro is a cloud API that lets you process video with a single HTTP call. No FFmpeg installation, no server management, no binary dependencies. And because it's just HTTP, it works with TypeScript's type system naturally.&lt;/p&gt;

&lt;p&gt;Here's a fully typed example:&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="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;TranscodeInput&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="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="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;TranscodeRequest&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TranscodeInput&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="nl"&gt;outputFormat&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;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;low&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;medium&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;high&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&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;480p&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;720p&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1080p&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;4k&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="nl"&gt;options&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="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;argument&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="p"&gt;}&lt;/span&gt;

&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;TranscodeResponse&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;queued&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;processing&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;completed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;failed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cancelled&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;outputUrl&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;billableMinutes&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;transcodeVideo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TranscodeRequest&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;TranscodeResponse&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;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.ffmpeg-micro.com/v1/transcodes&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Authorization&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;FFMPEG_MICRO_API_KEY&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="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;response&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="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: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;statusText&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="k"&gt;return&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you can call it with full type safety:&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;result&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;transcodeVideo&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="s1"&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="s1"&gt;mp4&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;preset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;quality&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;high&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;resolution&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;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="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;result&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;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="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;The TypeScript compiler catches bad property names, wrong types, and invalid enum values at build time. No runtime surprises from typo'd FFmpeg flags.&lt;/p&gt;

&lt;h2&gt;
  
  
  Polling for Job Completion
&lt;/h2&gt;

&lt;p&gt;Transcode jobs are async. Here's a type-safe polling function:&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="nx"&gt;maxWaitMs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;300000&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;TranscodeResponse&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;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;start&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;maxWaitMs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="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="s1"&gt;Authorization&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;FFMPEG_MICRO_API_KEY&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="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="na"&gt;job&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TranscodeResponse&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;completed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="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="s1"&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="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;2000&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;`Timed out waiting for job &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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use it after creating a transcode:&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;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;transcodeVideo&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="s1"&gt;https://example.com/raw-upload.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="s1"&gt;mp4&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;preset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;quality&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;medium&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;completed&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;waitForTranscode&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="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;`Output ready: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;completed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;outputUrl&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;h2&gt;
  
  
  Advanced: Custom FFmpeg Options with Type Safety
&lt;/h2&gt;

&lt;p&gt;For operations beyond presets (specific codecs, filters, bitrate control), use the &lt;code&gt;options&lt;/code&gt; array:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;transcodeVideo&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="s1"&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="s1"&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="s1"&gt;-c:v&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;argument&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;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="s1"&gt;-crf&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;argument&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;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="s1"&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="s1"&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="s1"&gt;-c:a&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;argument&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;options&lt;/code&gt; array accepts any valid FFmpeg flag. You get the full power of FFmpeg's CLI without managing the binary.&lt;/p&gt;

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

&lt;p&gt;It depends on your constraints:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Type Safety&lt;/th&gt;
&lt;th&gt;Requires FFmpeg Install&lt;/th&gt;
&lt;th&gt;Best For&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;child_process&lt;/td&gt;
&lt;td&gt;Minimal (callback types only)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Scripts, CI pipelines&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;fluent-ffmpeg&lt;/td&gt;
&lt;td&gt;Partial (@types lag behind)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Existing codebases already using it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ffmpeg.wasm&lt;/td&gt;
&lt;td&gt;Good (ships own types)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Small client-side edits&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloud API (FFmpeg Micro)&lt;/td&gt;
&lt;td&gt;Full (you define the interfaces)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Production apps, serverless, Docker&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For most TypeScript projects shipping to production, the cloud API approach gives you the best developer experience. No binary to install. No Docker image bloat. No platform-specific builds breaking in CI. Just typed HTTP calls that work the same everywhere your TypeScript runs.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Can I use FFmpeg with Deno or Bun in TypeScript?
&lt;/h3&gt;

&lt;p&gt;Yes. Since the cloud API approach uses standard &lt;code&gt;fetch&lt;/code&gt;, it works in any TypeScript runtime: Node.js, Deno, Bun, Cloudflare Workers, or even the browser. No runtime-specific dependencies.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does FFmpeg Micro have an official TypeScript SDK?
&lt;/h3&gt;

&lt;p&gt;Not yet. But since the API is simple REST (POST to create a job, GET to check status), the typed &lt;code&gt;fetch&lt;/code&gt; wrapper shown above covers every operation. The OpenAPI spec is available at &lt;a href="https://www.ffmpeg-micro.com/docs" rel="noopener noreferrer"&gt;ffmpeg-micro.com/docs&lt;/a&gt; if you want to generate types from it.&lt;/p&gt;

&lt;h3&gt;
  
  
  How much does cloud video processing cost vs. running FFmpeg locally?
&lt;/h3&gt;

&lt;p&gt;FFmpeg Micro charges per minute of video processed, starting with a free tier. For production workloads, this is typically cheaper than maintaining your own FFmpeg infrastructure when you factor in server costs, DevOps time, and scaling. A 1-minute video transcode takes about 3 seconds via the API.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I use FFmpeg Micro with Next.js API routes?
&lt;/h3&gt;

&lt;p&gt;Yes. API routes in Next.js run on the server. Use the typed &lt;code&gt;fetch&lt;/code&gt; wrapper from a route handler or server action. No binary installation needed since the processing happens in the cloud, not on your server.&lt;/p&gt;

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

&lt;p&gt;The API supports all formats FFmpeg supports as input. For output, specify the format in the &lt;code&gt;outputFormat&lt;/code&gt; field: mp4, webm, avi, mov, mkv, gif, mp3, wav, and more. See the full docs at &lt;a href="https://www.ffmpeg-micro.com/docs" rel="noopener noreferrer"&gt;ffmpeg-micro.com/docs&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ffmpeg</category>
      <category>typescript</category>
      <category>node</category>
      <category>api</category>
    </item>
    <item>
      <title>Auto-Publish 30 YouTube Shorts Per Week from One Long Video</title>
      <dc:creator>Javid Jamae</dc:creator>
      <pubDate>Thu, 18 Jun 2026 10:20:36 +0000</pubDate>
      <link>https://dev.clauneck.workers.dev/javidjamae/auto-publish-30-youtube-shorts-per-week-from-one-long-video-3gf7</link>
      <guid>https://dev.clauneck.workers.dev/javidjamae/auto-publish-30-youtube-shorts-per-week-from-one-long-video-3gf7</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.ffmpeg-micro.com/blog/auto-publish-30-youtube-shorts-per-week" rel="noopener noreferrer"&gt;ffmpeg-micro.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;One 30-minute video contains enough material for 30+ YouTube Shorts. Most creators leave that content on the table because splitting, captioning, and publishing 30 clips manually takes hours. With n8n and FFmpeg Micro, you can automate the entire pipeline: split the long video, add captions to each clip, and schedule them for auto-publish.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Pipeline at a Glance
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Split&lt;/strong&gt; a long video into 30-60 second clips using FFmpeg Micro's trim and crop operations&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Caption&lt;/strong&gt; each clip with burned-in subtitles using Whisper transcription&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Publish&lt;/strong&gt; each captioned clip to YouTube on a schedule via the YouTube Data API&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The whole thing runs in n8n, triggered by dropping a video URL into a webhook or a Google Drive folder.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Split the Long Video into Clips
&lt;/h2&gt;

&lt;p&gt;Use FFmpeg Micro's transcription endpoint to generate an SRT file and identify natural break points:&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/transcribe &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;'{"media_url": "https://storage.googleapis.com/your-bucket/long-video.mp4"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then trim each clip:&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.googleapis.com/your-bucket/long-video.mp4"}],
    "outputFormat": "mp4",
    "options": [
      {"option": "-ss", "argument": "00:02:15"},
      {"option": "-t", "argument": "45"},
      {"option": "-vf", "argument": "crop=ih*9/16:ih"},
      {"option": "-c:v", "argument": "libx264"},
      {"option": "-preset", "argument": "fast"}
    ]
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One API call per clip. 45 seconds, 9:16 vertical, H.264.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Add Captions
&lt;/h2&gt;

&lt;p&gt;Transcribe each clip individually, then burn subtitles back in with FFmpeg Micro's text overlay.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: The n8n Workflow
&lt;/h2&gt;

&lt;p&gt;Six nodes: Webhook trigger, HTTP Request (transcribe), Wait (poll), Code (parse SRT), Loop (trim+caption each clip), YouTube upload (scheduled publish).&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;totalDuration&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;$input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;first&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="nx"&gt;duration&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;CLIP_DURATION&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;45&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;segments&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;

&lt;span class="k"&gt;for &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;i&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="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;totalDuration&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;CLIP_DURATION&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="nx"&gt;i&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;startSeconds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;CLIP_DURATION&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;hours&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;startSeconds&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;padStart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0&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;mins&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;startSeconds&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;padStart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0&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;secs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;startSeconds&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="nf"&gt;padStart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;segments&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;clipNumber&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;startTime&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;hours&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;mins&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;secs&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="na"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CLIP_DURATION&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;segments&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;json&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Cost Breakdown
&lt;/h2&gt;

&lt;p&gt;For a 30-minute source video producing 30 clips: ~96 billable minutes total. One video per month fits in FFmpeg Micro's free tier (100 min/mo). Weekly production needs the Pro plan at $19/month.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;How many YouTube Shorts can I make from a 30-minute video?&lt;/strong&gt;&lt;br&gt;
30-40 Shorts at 45-60 seconds each.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do I need to install FFmpeg?&lt;/strong&gt;&lt;br&gt;
No. FFmpeg Micro is a cloud API. HTTP requests from n8n handle everything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I use Make.com instead of n8n?&lt;/strong&gt;&lt;br&gt;
Yes. FFmpeg Micro has an official Make.com app.&lt;/p&gt;

</description>
      <category>youtube</category>
      <category>automation</category>
      <category>n8n</category>
      <category>video</category>
    </item>
    <item>
      <title>FFmpeg Scale Filter: Resize Video and Keep Aspect Ratio</title>
      <dc:creator>Javid Jamae</dc:creator>
      <pubDate>Thu, 18 Jun 2026 10:19:51 +0000</pubDate>
      <link>https://dev.clauneck.workers.dev/javidjamae/ffmpeg-scale-filter-resize-video-and-keep-aspect-ratio-15j8</link>
      <guid>https://dev.clauneck.workers.dev/javidjamae/ffmpeg-scale-filter-resize-video-and-keep-aspect-ratio-15j8</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.ffmpeg-micro.com/blog/ffmpeg-scale-filter-preserve-aspect-ratio" rel="noopener noreferrer"&gt;ffmpeg-micro.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;You need to resize a video to 1080p, but the input is 4:3 and the output needs to be 16:9. You use &lt;code&gt;-s 1920x1080&lt;/code&gt; and get a stretched, distorted mess. The fix is FFmpeg's &lt;code&gt;scale&lt;/code&gt; filter, and specifically, the &lt;code&gt;-2&lt;/code&gt; trick and &lt;code&gt;force_original_aspect_ratio&lt;/code&gt; flag.&lt;/p&gt;

&lt;p&gt;This guide covers every common scaling scenario: auto-calculation with &lt;code&gt;-2&lt;/code&gt;, forced aspect ratios, letterboxing with &lt;code&gt;pad&lt;/code&gt;, and how to do it all through an API instead of wrestling with CLI flags.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick Answer: Scale Without Distortion
&lt;/h2&gt;

&lt;p&gt;The fastest way to resize a video while keeping its aspect ratio intact:&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=-2:1080"&lt;/span&gt; output.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;-2&lt;/code&gt; tells FFmpeg to calculate the width automatically based on the input's aspect ratio, rounding to the nearest even number (which video codecs require). Your 4:3 input becomes 1440x1080. Your 16:9 input becomes 1920x1080. No distortion.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the -2 Value Actually Does
&lt;/h2&gt;

&lt;p&gt;FFmpeg's scale filter takes width and height as arguments: &lt;code&gt;scale=width:height&lt;/code&gt;. When you set either dimension to &lt;code&gt;-1&lt;/code&gt; or &lt;code&gt;-2&lt;/code&gt;, FFmpeg calculates that dimension from the other one using the original aspect ratio.&lt;/p&gt;

&lt;p&gt;The difference between &lt;code&gt;-1&lt;/code&gt; and &lt;code&gt;-2&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;-1&lt;/code&gt; calculates the value, but it might produce an odd number (e.g., 1441)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-2&lt;/code&gt; does the same calculation but rounds to the nearest even number&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Odd pixel dimensions cause encoding failures with H.264 and H.265. Always use &lt;code&gt;-2&lt;/code&gt;, not &lt;code&gt;-1&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="c"&gt;# Scale to 720p height, auto-calculate width&lt;/span&gt;
ffmpeg &lt;span class="nt"&gt;-i&lt;/span&gt; input.mp4 &lt;span class="nt"&gt;-vf&lt;/span&gt; &lt;span class="s2"&gt;"scale=-2:720"&lt;/span&gt; output.mp4

&lt;span class="c"&gt;# Scale to 1280 width, auto-calculate height&lt;/span&gt;
ffmpeg &lt;span class="nt"&gt;-i&lt;/span&gt; input.mp4 &lt;span class="nt"&gt;-vf&lt;/span&gt; &lt;span class="s2"&gt;"scale=1280:-2"&lt;/span&gt; output.mp4

&lt;span class="c"&gt;# Scale to 4K height&lt;/span&gt;
ffmpeg &lt;span class="nt"&gt;-i&lt;/span&gt; input.mp4 &lt;span class="nt"&gt;-vf&lt;/span&gt; &lt;span class="s2"&gt;"scale=-2:2160"&lt;/span&gt; output.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  force_original_aspect_ratio: Fit or Fill
&lt;/h2&gt;

&lt;p&gt;Sometimes you need the output to be exactly 1920x1080, no exceptions. But your input is 4:3. The &lt;code&gt;force_original_aspect_ratio&lt;/code&gt; parameter handles this without stretching.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;decrease&lt;/strong&gt; shrinks the video to fit inside the target dimensions:&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:force_original_aspect_ratio=decrease"&lt;/span&gt; output.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;increase&lt;/strong&gt; enlarges the video to fill the target dimensions:&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:force_original_aspect_ratio=increase"&lt;/span&gt; output.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Letterboxing with pad
&lt;/h2&gt;

&lt;p&gt;If you need exactly 1920x1080 with black bars (letterboxing), chain the &lt;code&gt;pad&lt;/code&gt; filter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;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:force_original_aspect_ratio=decrease,pad=1920:1080:(ow-iw)/2:(oh-ih)/2:black"&lt;/span&gt; output.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Common Resolution Targets
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Target&lt;/th&gt;
&lt;th&gt;Command&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;480p (SD)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;scale=-2:480&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;720p (HD)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;scale=-2:720&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1080p (Full HD)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;scale=-2:1080&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2160p (4K)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;scale=-2:2160&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

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

&lt;p&gt;If you don't want to install FFmpeg or manage a server, you can resize videos through an API call:&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/video.mp4"}],
    "outputFormat": "mp4",
    "options": [
      {"option": "-vf", "argument": "scale=-2:1080"}
    ]
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;FFmpeg Micro is a cloud API that accepts the same FFmpeg filters as the CLI. No FFmpeg installation, no server management.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Does ffmpeg scale preserve aspect ratio by default?&lt;/strong&gt;&lt;br&gt;
No. The default &lt;code&gt;scale=width:height&lt;/code&gt; stretches the video to fit both dimensions. Use &lt;code&gt;-2&lt;/code&gt; for auto-calculation or &lt;code&gt;force_original_aspect_ratio&lt;/code&gt; to prevent distortion.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's the difference between -s and -vf scale in FFmpeg?&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;-s 1920x1080&lt;/code&gt; sets exact output dimensions and can distort. &lt;code&gt;-vf "scale=-2:1080"&lt;/code&gt; calculates one dimension from the other to keep the aspect ratio.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I resize video through an API without installing FFmpeg?&lt;/strong&gt;&lt;br&gt;
Yes. FFmpeg Micro is a cloud API that accepts the same FFmpeg filters. Send a POST to &lt;code&gt;/v1/transcodes&lt;/code&gt; with &lt;code&gt;options: [{"option": "-vf", "argument": "scale=-2:1080"}]&lt;/code&gt; and get back the resized video.&lt;/p&gt;

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