<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Javid Jamae</title>
    <description>The latest articles on DEV Community by Javid Jamae (@javidjamae).</description>
    <link>https://dev.to/javidjamae</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3873707%2F97b6ee49-1448-4e30-b41a-14c1f4eec9cc.jpg</url>
      <title>DEV Community: Javid Jamae</title>
      <link>https://dev.to/javidjamae</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/javidjamae"/>
    <language>en</language>
    <item>
      <title>RSS Feed to YouTube Short: Build the Full Pipeline</title>
      <dc:creator>Javid Jamae</dc:creator>
      <pubDate>Sun, 28 Jun 2026 10:20:22 +0000</pubDate>
      <link>https://dev.to/javidjamae/rss-feed-to-youtube-short-build-the-full-pipeline-nb1</link>
      <guid>https://dev.to/javidjamae/rss-feed-to-youtube-short-build-the-full-pipeline-nb1</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.ffmpeg-micro.com/blog/rss-feed-to-youtube-short-automated-pipeline" rel="noopener noreferrer"&gt;ffmpeg-micro.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Every time a new article drops in your RSS feed, you could turn it into a YouTube Short. Not manually. Fully automated: new post triggers the pipeline, and a finished Short lands in your upload queue.&lt;/p&gt;

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

&lt;p&gt;Five stages, each handled by a different tool:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;RSS trigger&lt;/strong&gt; catches new articles (n8n, Make.com, or a cron job)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LLM&lt;/strong&gt; rewrites the article into a 30-60 second script&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TTS&lt;/strong&gt; converts the script to audio (ElevenLabs, OpenAI TTS)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;FFmpeg Micro&lt;/strong&gt; composites the audio over background video with text overlay&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Upload&lt;/strong&gt; pushes the finished Short to YouTube&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Stage 1: RSS Trigger
&lt;/h2&gt;

&lt;p&gt;In n8n, drop an RSS Feed Trigger node pointing at your source feed. It fires every time a new item appears and outputs the article title, link, and content body.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stage 2: LLM Script Generation
&lt;/h2&gt;

&lt;p&gt;Feed the article body to an LLM with a prompt that outputs a tight script. YouTube Shorts max out at 60 seconds, so aim for 120-180 words.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stage 3: Text-to-Speech
&lt;/h2&gt;

&lt;p&gt;Send the script to a TTS API (ElevenLabs, OpenAI TTS, or Google Cloud TTS), then upload the audio to FFmpeg Micro for the composition step.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stage 4: Compose with FFmpeg Micro
&lt;/h2&gt;

&lt;p&gt;This is where it comes together. FFmpeg Micro merges background video with narration audio and adds text overlay:&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;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"https://api.ffmpeg-micro.com/v1/transcodes"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$FFMPEG_MICRO_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "inputs": [
      {"url": "https://your-bucket.com/background-9x16.mp4"},
      {"url": "gs://your-bucket/narration.mp3"}
    ],
    "outputFormat": "mp4",
    "options": [
      {"option": "@text-overlay", "argument": {
        "text": "Your headline here",
        "style": {"charsPerLine": 18, "fontSize": 60, "lineSpacing": 15}
      }}
    ]
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;@text-overlay&lt;/code&gt; virtual option handles font rendering, positioning, and line breaking automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stage 5: Upload to YouTube
&lt;/h2&gt;

&lt;p&gt;Download the finished video and push it to YouTube via the Data API. n8n has a built-in YouTube node that handles this.&lt;/p&gt;

&lt;p&gt;Read the &lt;a href="https://www.ffmpeg-micro.com/blog/rss-feed-to-youtube-short-automated-pipeline" rel="noopener noreferrer"&gt;full guide with all code examples and pitfalls&lt;/a&gt; on ffmpeg-micro.com.&lt;/p&gt;

</description>
      <category>automation</category>
      <category>youtube</category>
      <category>n8n</category>
      <category>ffmpeg</category>
    </item>
    <item>
      <title>How to Use FFmpeg with Deno (No Installation Required)</title>
      <dc:creator>Javid Jamae</dc:creator>
      <pubDate>Sun, 28 Jun 2026 10:19:42 +0000</pubDate>
      <link>https://dev.to/javidjamae/how-to-use-ffmpeg-with-deno-no-installation-required-54il</link>
      <guid>https://dev.to/javidjamae/how-to-use-ffmpeg-with-deno-no-installation-required-54il</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-deno" rel="noopener noreferrer"&gt;ffmpeg-micro.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;You're building something in Deno and you need video processing. Thumbnail generation, format conversion, transcoding user uploads. You search for "ffmpeg deno" and quickly realize the ecosystem isn't like Node.js. There's no &lt;code&gt;fluent-ffmpeg&lt;/code&gt; equivalent.&lt;/p&gt;

&lt;p&gt;Deno's security model makes this worse. By default, Deno sandboxes your code. No file system access, no subprocess execution, no network calls unless you explicitly grant permissions.&lt;/p&gt;

&lt;p&gt;There are three realistic paths.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using Deno.Command with a Local FFmpeg Binary
&lt;/h2&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;command&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;Deno&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Command&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ffmpeg&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;-i&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;input.mp4&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;-c:v&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;libx264&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;-crf&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;23&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;-preset&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;medium&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;-c:a&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;aac&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;-b:a&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;192k&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;output.mp4&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;stderr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;piped&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;process&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;command&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;spawn&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&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="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&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;status&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You must run this with &lt;code&gt;--allow-run=ffmpeg&lt;/code&gt; and &lt;code&gt;--allow-read&lt;/code&gt; / &lt;code&gt;--allow-write&lt;/code&gt; flags. Works for local scripts but breaks on Deno Deploy.&lt;/p&gt;

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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;FFmpeg&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;npm:@ffmpeg/ffmpeg&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;fetchFile&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;npm:@ffmpeg/util&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ffmpeg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;FFmpeg&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ffmpeg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ffmpeg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;input.mp4&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetchFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./input.mp4&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ffmpeg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exec&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;-i&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;input.mp4&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;-c:v&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;libx264&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;output.mp4&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ffmpeg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;output.mp4&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;Deno&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;output.mp4&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No binary needed, but 10-20x slower than native FFmpeg. Fine for lightweight operations only.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using a Cloud API (FFmpeg Micro)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://api.ffmpeg-micro.com/v1/transcodes&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Authorization&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;Deno&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;FFMPEG_MICRO_API_KEY&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://example.com/video.mp4&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
    &lt;span class="na"&gt;outputFormat&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mp4&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;preset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;quality&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;high&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;resolution&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;1080p&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;job&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No npm packages. No binary dependencies. Works everywhere Deno runs, including Deno Deploy.&lt;/p&gt;

&lt;p&gt;Read the &lt;a href="https://www.ffmpeg-micro.com/blog/how-to-use-ffmpeg-with-deno" rel="noopener noreferrer"&gt;full guide with polling, download, and advanced examples&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>deno</category>
      <category>ffmpeg</category>
      <category>typescript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How to Add FFmpeg to an Airtable Automation</title>
      <dc:creator>Javid Jamae</dc:creator>
      <pubDate>Sat, 27 Jun 2026 10:18:18 +0000</pubDate>
      <link>https://dev.to/javidjamae/how-to-add-ffmpeg-to-an-airtable-automation-1nfo</link>
      <guid>https://dev.to/javidjamae/how-to-add-ffmpeg-to-an-airtable-automation-1nfo</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.ffmpeg-micro.com/blog/airtable-ffmpeg-video-automation" rel="noopener noreferrer"&gt;ffmpeg-micro.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Airtable is great at tracking content. Rows of video files with status columns, due dates, assignees. But when someone changes a status to "Ready to publish" and you still have to manually export, resize, and transcode each video, the automation stops where it matters most.&lt;/p&gt;

&lt;p&gt;You can fix this by connecting Airtable's scripting automation to the FFmpeg Micro API. When a record hits a trigger condition, a script fires an HTTP request, FFmpeg Micro processes the video, and the output URL writes back into your Airtable row. No manual step in between.&lt;/p&gt;

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

&lt;p&gt;Airtable automations trigger scripts when records change. Those scripts can make HTTP requests. FFmpeg Micro accepts video URLs over HTTP and returns processed results. The flow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A record in your Airtable base changes (new row, status update, checkbox toggle)&lt;/li&gt;
&lt;li&gt;Airtable fires an automation trigger&lt;/li&gt;
&lt;li&gt;A scripting action sends the video URL to the FFmpeg Micro API&lt;/li&gt;
&lt;li&gt;FFmpeg Micro transcodes the video and returns a job ID&lt;/li&gt;
&lt;li&gt;A second automation polls for completion and writes the download URL back&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;No middleware. No Zapier. No n8n. Just Airtable's built-in scripting block talking directly to an API.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Get Your FFmpeg Micro API Key
&lt;/h2&gt;

&lt;p&gt;Sign up at &lt;a href="https://www.ffmpeg-micro.com" rel="noopener noreferrer"&gt;ffmpeg-micro.com&lt;/a&gt; and grab your API key from the dashboard. The free tier gives you enough minutes to test and run small batches.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Set Up Your Airtable Base
&lt;/h2&gt;

&lt;p&gt;You need a table with at least these fields:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field Name&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Video URL&lt;/td&gt;
&lt;td&gt;URL&lt;/td&gt;
&lt;td&gt;Source video link (public URL)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Output Format&lt;/td&gt;
&lt;td&gt;Single select&lt;/td&gt;
&lt;td&gt;mp4, webm, mov&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Status&lt;/td&gt;
&lt;td&gt;Single select&lt;/td&gt;
&lt;td&gt;Pending, Processing, Done, Error&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Job ID&lt;/td&gt;
&lt;td&gt;Single line text&lt;/td&gt;
&lt;td&gt;FFmpeg Micro job ID for polling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Output URL&lt;/td&gt;
&lt;td&gt;URL&lt;/td&gt;
&lt;td&gt;Where the processed video ends up&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Quality&lt;/td&gt;
&lt;td&gt;Single select&lt;/td&gt;
&lt;td&gt;low, medium, high (optional)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;Video URL&lt;/code&gt; field should contain a publicly accessible link. FFmpeg Micro needs to download the source file, so private Airtable attachments won't work directly. Use a public cloud storage URL (Google Cloud Storage, S3, or any CDN).&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Create the Automation
&lt;/h2&gt;

&lt;p&gt;In Airtable, go to Automations and create a new one:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trigger:&lt;/strong&gt; "When record matches conditions"&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Table: your video table&lt;/li&gt;
&lt;li&gt;Condition: Status = "Pending"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Action:&lt;/strong&gt; "Run a script"&lt;/p&gt;

&lt;p&gt;Paste this script into the scripting action:&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;config&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;config&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;recordId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;recordId&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;videoUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;videoUrl&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;outputFormat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;outputFormat&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mp4&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;quality&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;quality&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="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;API_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;YOUR_FFMPEG_MICRO_API_KEY&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Update status to Processing&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;updateRecordAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;recordId&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;Status&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;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Processing&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Send transcode request to FFmpeg Micro&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;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="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;videoUrl&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;
        &lt;span class="na"&gt;outputFormat&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;outputFormat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;preset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;quality&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;quality&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;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;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="c1"&gt;// Store the job ID for polling later&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;updateRecordAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;recordId&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;Job ID&lt;/span&gt;&lt;span class="dl"&gt;'&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="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Transcode job created: &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;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;updateRecordAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;recordId&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;Status&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;name&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;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="s2"&gt;`FFmpeg Micro error: &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;error&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;&lt;strong&gt;Configure the input variables&lt;/strong&gt; in the script settings:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;recordId&lt;/code&gt; = the record ID from the trigger&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;videoUrl&lt;/code&gt; = the Video URL field value&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;outputFormat&lt;/code&gt; = the Output Format field value&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;quality&lt;/code&gt; = the Quality field value&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;table&lt;/code&gt; = the table object (Airtable provides this automatically)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 4: Poll for Completion
&lt;/h2&gt;

&lt;p&gt;FFmpeg Micro transcodes are asynchronous. The API returns a job ID immediately, and processing happens in the background. Create a second automation to check job status:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trigger:&lt;/strong&gt; "When record matches conditions"&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Condition: Status = "Processing" AND Job ID is not empty&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Or use a scheduled automation that runs every 5 minutes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Action:&lt;/strong&gt; "Run a script"&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;config&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;config&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;recordId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;recordId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;jobId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;config&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;API_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;YOUR_FFMPEG_MICRO_API_KEY&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Check job status&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;statusResponse&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;API_KEY&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;job&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;statusResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;completed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Get downloadable URL (output_url is a gs:// path, not directly fetchable)&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;downloadResponse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s2"&gt;`https://api.ffmpeg-micro.com/v1/transcodes/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;jobId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/download`&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;API_KEY&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;download&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;downloadResponse&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;await&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;updateRecordAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;recordId&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;Status&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;name&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Output URL&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;download&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&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;failed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;updateRecordAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;recordId&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;Status&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;name&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Most transcodes finish in under a minute for short videos. The polling automation catches results on its next run.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use Cases That Work Well With This Setup
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Batch thumbnail generation.&lt;/strong&gt; Drop 50 video URLs into Airtable, set all to "Pending," and the automation generates a thumbnail from each one. FFmpeg Micro can extract frames at specific timestamps.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Social media reformatting.&lt;/strong&gt; Your content team uploads one master video. The automation creates vertical crops for TikTok/Reels, square crops for Instagram feed, and landscape versions for YouTube, all from the same source.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Client delivery.&lt;/strong&gt; Agencies managing video projects can let clients upload raw footage to a shared Airtable base. The automation transcodes to the client's required format and resolution, then writes the download link back into the record.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Content pipeline tracking.&lt;/strong&gt; Track every video from draft to published in Airtable, with automated format conversion as part of the workflow. Status columns let your team see exactly where each piece is.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Does Airtable's scripting block support fetch/HTTP requests?
&lt;/h3&gt;

&lt;p&gt;Yes. Airtable's scripting automation actions support the standard &lt;code&gt;fetch&lt;/code&gt; API for making HTTP requests. You can call any REST API, including FFmpeg Micro, directly from a script.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I process Airtable attachment fields directly?
&lt;/h3&gt;

&lt;p&gt;Not directly. Airtable attachment URLs are temporary and authenticated. You need to copy the file to a public storage location first (like Google Cloud Storage or S3), then pass that public URL to FFmpeg Micro.&lt;/p&gt;

&lt;h3&gt;
  
  
  What happens if the transcode fails?
&lt;/h3&gt;

&lt;p&gt;The polling script checks for &lt;code&gt;failed&lt;/code&gt; status and sets the record to "Error." Check the Airtable automation run history for details. Common causes: invalid video URL, unsupported format, or an expired source link.&lt;/p&gt;

&lt;h3&gt;
  
  
  How many videos can I process at once?
&lt;/h3&gt;

&lt;p&gt;FFmpeg Micro's free tier includes a limited number of processing minutes per month. The API handles concurrent requests, but Airtable automations have their own run limits depending on your plan (25,000 runs/month on Pro).&lt;/p&gt;

&lt;h3&gt;
  
  
  Is there a way to do this without writing code?
&lt;/h3&gt;

&lt;p&gt;Connect Airtable to FFmpeg Micro through Make.com or Zapier instead of writing scripts. Both platforms have HTTP modules that can call the API without code. But the scripting approach gives you more control over error handling and record updates.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Last verified: June 2026&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ffmpeg</category>
      <category>airtable</category>
      <category>automation</category>
      <category>api</category>
    </item>
    <item>
      <title>FFmpeg filter_complex Explained: Multi-Input Processing and Filter Chains</title>
      <dc:creator>Javid Jamae</dc:creator>
      <pubDate>Sat, 27 Jun 2026 10:17:51 +0000</pubDate>
      <link>https://dev.to/javidjamae/ffmpeg-filtercomplex-explained-multi-input-processing-and-filter-chains-57e2</link>
      <guid>https://dev.to/javidjamae/ffmpeg-filtercomplex-explained-multi-input-processing-and-filter-chains-57e2</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.ffmpeg-micro.com/blog/ffmpeg-filter-complex-explained" rel="noopener noreferrer"&gt;ffmpeg-micro.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;FFmpeg's &lt;code&gt;-vf&lt;/code&gt; flag handles simple, single-stream operations just fine. Scale a video, crop it, add a watermark. But the moment you need two inputs, or want to route one stream through multiple processing paths, &lt;code&gt;-vf&lt;/code&gt; breaks down. That's where &lt;code&gt;filter_complex&lt;/code&gt; comes in.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;filter_complex&lt;/code&gt; is FFmpeg's system for building multi-input, multi-output processing graphs. It's also where most developers hit a wall, because the syntax looks like someone spilled regex into a shell command. This guide breaks down how it actually works, with patterns you can copy and modify.&lt;/p&gt;

&lt;h2&gt;
  
  
  What filter_complex Does That -vf Can't
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;-vf&lt;/code&gt; flag (and its audio counterpart &lt;code&gt;-af&lt;/code&gt;) operates on a single input stream. One video in, one video out. It's a pipeline:&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=1280:720"&lt;/span&gt; output.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;filter_complex&lt;/code&gt; removes that single-stream constraint. It lets you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Combine multiple input files (overlay, concat, blend)&lt;/li&gt;
&lt;li&gt;Split one input into multiple processing paths&lt;/li&gt;
&lt;li&gt;Chain filters across different streams with explicit routing&lt;/li&gt;
&lt;li&gt;Produce multiple output files from one command&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The syntax is different because it has to be. When you have three inputs and two outputs, you need labels to say which stream goes where.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Label System
&lt;/h2&gt;

&lt;p&gt;Labels are how filter_complex routes streams. Every input gets an automatic label based on its position and stream type:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;[0:v]&lt;/code&gt; is the video stream from the first input&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;[0:a]&lt;/code&gt; is the audio stream from the first input&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;[1:v]&lt;/code&gt; is the video stream from the second input&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can also create your own labels for intermediate results. Anything in square brackets before a filter is an input label. Anything after a filter (also in square brackets) is an output label:&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="nt"&gt;-filter_complex&lt;/span&gt; &lt;span class="s2"&gt;"[0:v]scale=1280:720[scaled]"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This takes the video from input 0, scales it to 1280x720, and labels the result &lt;code&gt;[scaled]&lt;/code&gt;. You then use &lt;code&gt;-map "[scaled]"&lt;/code&gt; to route that labeled stream to an output file.&lt;/p&gt;

&lt;p&gt;Labels must be unique within a filtergraph. If you use &lt;code&gt;[out]&lt;/code&gt; once, you can't use it again.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern 1: Chain Multiple Filters on One Input
&lt;/h2&gt;

&lt;p&gt;The simplest filter_complex pattern. You want to apply several operations in sequence without running FFmpeg multiple times.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffmpeg &lt;span class="nt"&gt;-i&lt;/span&gt; input.mp4 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-filter_complex&lt;/span&gt; &lt;span class="s2"&gt;"[0:v]scale=1280:720,crop=640:360:320:180[out]"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-map&lt;/span&gt; &lt;span class="s2"&gt;"[out]"&lt;/span&gt; output.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Filters chained with commas run left to right on the same stream. This scales to 1280x720 first, then crops a 640x360 region from the center.&lt;/p&gt;

&lt;p&gt;You could do this with &lt;code&gt;-vf "scale=1280:720,crop=640:360:320:180"&lt;/code&gt; and get the same result. The filter_complex version only becomes necessary when you need labels for routing, which leads to the next two patterns.&lt;/p&gt;

&lt;p&gt;A more practical chain adds text overlay after scaling:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffmpeg &lt;span class="nt"&gt;-i&lt;/span&gt; input.mp4 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-filter_complex&lt;/span&gt; &lt;span class="s2"&gt;"[0:v]scale=1920:1080,drawtext=text='Demo':fontsize=48:x=10:y=10:fontcolor=white[out]"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-map&lt;/span&gt; &lt;span class="s2"&gt;"[out]"&lt;/span&gt; output.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Pattern 2: Combine Multiple Inputs
&lt;/h2&gt;

&lt;p&gt;This is the most common reason developers reach for filter_complex. You have two or more files and need to merge them into one output.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Overlay (picture-in-picture):&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; main.mp4 &lt;span class="nt"&gt;-i&lt;/span&gt; webcam.mp4 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-filter_complex&lt;/span&gt; &lt;span class="s2"&gt;"[1:v]scale=160:90[pip]; [0:v][pip]overlay=W-w-10:H-h-10[out]"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-map&lt;/span&gt; &lt;span class="s2"&gt;"[out]"&lt;/span&gt; &lt;span class="nt"&gt;-map&lt;/span&gt; &lt;span class="s2"&gt;"0:a"&lt;/span&gt; output.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice the semicolon. In filter_complex, semicolons separate independent filter chains. The first chain scales the second input down. The second chain overlays the scaled result onto the first input.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;W&lt;/code&gt; and &lt;code&gt;H&lt;/code&gt; are the width and height of the background video. &lt;code&gt;w&lt;/code&gt; and &lt;code&gt;h&lt;/code&gt; are the overlay's dimensions. So &lt;code&gt;W-w-10:H-h-10&lt;/code&gt; puts the overlay 10 pixels from the bottom-right corner.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;-map "0:a"&lt;/code&gt; at the end grabs audio from the first input, since filter_complex doesn't automatically pass through audio.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Concatenation (join videos end to end):&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; part1.mp4 &lt;span class="nt"&gt;-i&lt;/span&gt; part2.mp4 &lt;span class="nt"&gt;-i&lt;/span&gt; part3.mp4 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-filter_complex&lt;/span&gt; &lt;span class="s2"&gt;"[0:v][0:a][1:v][1:a][2:v][2:a]concat=n=3:v=1:a=1[outv][outa]"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-map&lt;/span&gt; &lt;span class="s2"&gt;"[outv]"&lt;/span&gt; &lt;span class="nt"&gt;-map&lt;/span&gt; &lt;span class="s2"&gt;"[outa]"&lt;/span&gt; output.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;concat&lt;/code&gt; filter takes all input streams in order. &lt;code&gt;n=3&lt;/code&gt; tells it there are 3 segments. &lt;code&gt;v=1:a=1&lt;/code&gt; means each segment has one video and one audio stream.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern 3: Split One Input Into Multiple Outputs
&lt;/h2&gt;

&lt;p&gt;Sometimes you need to produce several versions of the same source video in one pass. The &lt;code&gt;split&lt;/code&gt; filter duplicates a stream:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffmpeg &lt;span class="nt"&gt;-i&lt;/span&gt; input.mp4 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-filter_complex&lt;/span&gt; &lt;span class="s2"&gt;"[0:v]split=2[a][b]; [a]scale=320:180[small]; [b]scale=1280:720[big]"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-map&lt;/span&gt; &lt;span class="s2"&gt;"[small]"&lt;/span&gt; small.mp4 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-map&lt;/span&gt; &lt;span class="s2"&gt;"[big]"&lt;/span&gt; big.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;split=2&lt;/code&gt; creates two copies of the input stream, labeled &lt;code&gt;[a]&lt;/code&gt; and &lt;code&gt;[b]&lt;/code&gt;. Each copy goes through its own scale filter. Each labeled output maps to a different output file.&lt;/p&gt;

&lt;p&gt;This is significantly faster than running FFmpeg twice because you only decode the source once.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running filter_complex Operations via the FFmpeg Micro API
&lt;/h2&gt;

&lt;p&gt;Writing filter_complex commands by hand is where self-hosted FFmpeg gets painful. The syntax is fiddly, errors are cryptic ("Output pad 'default' of filter..." means you forgot a label), and every typo requires re-reading the docs.&lt;/p&gt;

&lt;p&gt;FFmpeg Micro's API handles the most common filter_complex patterns without you writing filtergraph syntax. For operations like overlay, concatenation, and multi-step processing, 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 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/main.mp4"},
      {"url": "https://example.com/overlay.png"}
    ],
    "outputFormat": "mp4",
    "options": [
      {"option": "-c:v", "argument": "libx264"},
      {"option": "-crf", "argument": "23"}
    ]
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The API accepts multiple inputs via the &lt;code&gt;inputs&lt;/code&gt; array and lets you pass raw FFmpeg options when you need fine-grained control. For operations that don't need custom filtergraphs (most transcoding, format conversion, quality adjustment), the &lt;code&gt;preset&lt;/code&gt; field is even simpler:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;No filter_complex syntax. No stream labels. No debugging "pad already used" errors at 2 AM.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;"Output pad already used" error.&lt;/strong&gt; You tried to use a labeled stream twice without splitting it first. Every label can only be consumed once. Use &lt;code&gt;split&lt;/code&gt; to duplicate it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Audio disappearing.&lt;/strong&gt; filter_complex doesn't pass through unmapped streams. If your filtergraph only processes video, you need &lt;code&gt;-map "0:a"&lt;/code&gt; to keep the audio track.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Semicolons vs. commas.&lt;/strong&gt; Commas chain filters on the same stream. Semicolons separate independent chains. Mix them up and FFmpeg either errors out or produces garbage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Forgetting -map.&lt;/strong&gt; When using filter_complex, FFmpeg won't guess which outputs you want. You must explicitly &lt;code&gt;-map&lt;/code&gt; every labeled output to a file. No map, no output.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Label naming.&lt;/strong&gt; Labels can't contain special characters or spaces. Stick to alphanumeric names: &lt;code&gt;[scaled]&lt;/code&gt;, &lt;code&gt;[pip]&lt;/code&gt;, &lt;code&gt;[out]&lt;/code&gt;, &lt;code&gt;[v1]&lt;/code&gt;.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  When should I use filter_complex instead of -vf?
&lt;/h3&gt;

&lt;p&gt;Use &lt;code&gt;filter_complex&lt;/code&gt; whenever you have more than one input file, need to split a stream into multiple processing paths, or want multiple output files from one command. If you're doing simple single-input processing (scale, crop, rotate), &lt;code&gt;-vf&lt;/code&gt; is simpler and does the same thing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I mix video and audio filters in one filter_complex?
&lt;/h3&gt;

&lt;p&gt;Yes. Video and audio filters run in the same filtergraph. Use video stream labels (&lt;code&gt;[0:v]&lt;/code&gt;) and audio stream labels (&lt;code&gt;[0:a]&lt;/code&gt;) to route each through their respective filters. Just remember that video filters can't process audio and vice versa.&lt;/p&gt;

&lt;h3&gt;
  
  
  What does the semicolon do in filter_complex?
&lt;/h3&gt;

&lt;p&gt;Semicolons separate independent filter chains within the filtergraph. Each chain processes its own input and produces its own output. Commas chain filters sequentially on the same stream.&lt;/p&gt;

&lt;h3&gt;
  
  
  How many inputs can filter_complex handle?
&lt;/h3&gt;

&lt;p&gt;There's no hard limit. FFmpeg can handle dozens of inputs in a single filter_complex command. The practical limit is usually memory and the complexity of your filtergraph. For batch operations with hundreds of files, you're better off running separate commands or using an API.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does FFmpeg Micro support custom filter_complex commands?
&lt;/h3&gt;

&lt;p&gt;FFmpeg Micro handles the most common multi-input operations (concatenation, overlay, multi-step processing) through its &lt;code&gt;inputs&lt;/code&gt; array and &lt;code&gt;options&lt;/code&gt; fields. For standard use cases, you don't need to write filtergraph syntax at all. For advanced scenarios requiring raw filter_complex strings, pass them through the options array.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Last verified: June 2026&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ffmpeg</category>
      <category>video</category>
      <category>api</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Notion to Video: Auto-Render on Row Change with Make</title>
      <dc:creator>Javid Jamae</dc:creator>
      <pubDate>Fri, 26 Jun 2026 10:19:17 +0000</pubDate>
      <link>https://dev.to/javidjamae/notion-to-video-auto-render-on-row-change-with-make-14ol</link>
      <guid>https://dev.to/javidjamae/notion-to-video-auto-render-on-row-change-with-make-14ol</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.ffmpeg-micro.com/blog/notion-to-video-auto-render-make-ffmpeg" rel="noopener noreferrer"&gt;ffmpeg-micro.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;You've got a Notion database full of product names, launch dates, and promo copy. Every time you add or update a row, someone has to manually open a video editor, swap in the new text, export, and upload. That process takes 15 minutes if you're fast.&lt;/p&gt;

&lt;p&gt;There's a better way. Connect Notion to Make.com, hit the FFmpeg Micro API on every row change, and get a rendered video back automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Notion + Make + FFmpeg Micro
&lt;/h2&gt;

&lt;p&gt;Notion works well as a lightweight CMS. Make.com watches for changes in that Notion database and fires off actions. FFmpeg Micro handles the actual video rendering via a simple REST API.&lt;/p&gt;

&lt;p&gt;The full loop: Notion row updates, Make picks it up, Make calls FFmpeg Micro with the row data as a text overlay on a template video, and the rendered file URL comes back in the response.&lt;/p&gt;

&lt;h2&gt;
  
  
  The FFmpeg Micro API Request
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;@text-overlay&lt;/code&gt; option burns text directly onto your template video:&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="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;"{{1.properties.Template Video URL.url}}"&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;"@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;"{{1.properties.Title.title[0].plain_text}}"&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;"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;48&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;"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="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;Test it outside of Make with curl:&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/template.mp4"}],
    "outputFormat": "mp4",
    "options": [{"option": "@text-overlay", "argument": {"text": "Product Launch: Acme Widget", "style": {"fontSize": 48, "charsPerLine": 20, "x": "(w-text_w)/2", "y": "(h-text_h)/2"}}}]
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Triggering on every edit&lt;/strong&gt;: Gate renders behind a Status value like "Ready to Render"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Template not accessible&lt;/strong&gt;: FFmpeg Micro fetches via URL, so ensure it's publicly reachable&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Text overflow&lt;/strong&gt;: Set reasonable &lt;code&gt;charsPerLine&lt;/code&gt; values for your template&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Make.com timeouts&lt;/strong&gt;: Bump HTTP module timeout to 60+ seconds&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;&lt;strong&gt;Does Make.com trigger on new rows or just updates?&lt;/strong&gt; Both. Use "Watch Database Items" for updates or "Watch Database Items (Created)" for new rows.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How long does a render take?&lt;/strong&gt; 3-8 seconds for a 30-second template with text overlay.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What formats work?&lt;/strong&gt; Any format FFmpeg can decode as input, including MP4, MOV, WebM, and AVI.&lt;/p&gt;

</description>
      <category>notion</category>
      <category>automation</category>
      <category>video</category>
      <category>nocode</category>
    </item>
    <item>
      <title>FFmpeg Overlay Filter: Picture-in-Picture and Compositing</title>
      <dc:creator>Javid Jamae</dc:creator>
      <pubDate>Fri, 26 Jun 2026 10:18:29 +0000</pubDate>
      <link>https://dev.to/javidjamae/ffmpeg-overlay-filter-picture-in-picture-and-compositing-5fog</link>
      <guid>https://dev.to/javidjamae/ffmpeg-overlay-filter-picture-in-picture-and-compositing-5fog</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.ffmpeg-micro.com/blog/ffmpeg-overlay-filter-picture-in-picture" rel="noopener noreferrer"&gt;ffmpeg-micro.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;You need to put one video on top of another, and FFmpeg's documentation reads like a math textbook. The overlay filter is one of the most used filters in FFmpeg, but getting the positioning, scaling, and timing right takes more trial and error than it should.&lt;/p&gt;

&lt;p&gt;This guide covers the overlay filter from basic image-on-video compositing to timed picture-in-picture with multiple streams.&lt;/p&gt;

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

&lt;p&gt;The overlay filter takes two video inputs and stacks one on top of the other. The first input is the background (main video), and the second is the foreground (the overlay). You control placement with x and y coordinates.&lt;/p&gt;

&lt;p&gt;The basic syntax inside &lt;code&gt;filter_complex&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; main.mp4 &lt;span class="nt"&gt;-i&lt;/span&gt; logo.png &lt;span class="nt"&gt;-filter_complex&lt;/span&gt; &lt;span class="s2"&gt;"[0:v][1:v]overlay=10:10"&lt;/span&gt; output.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This places &lt;code&gt;logo.png&lt;/code&gt; at 10 pixels from the left and 10 pixels from the top of &lt;code&gt;main.mp4&lt;/code&gt;. Two inputs, one filter, x and y coordinates.&lt;/p&gt;

&lt;p&gt;You need &lt;code&gt;-filter_complex&lt;/code&gt; instead of &lt;code&gt;-vf&lt;/code&gt; whenever you're working with multiple inputs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Position Variables for Dynamic Placement
&lt;/h2&gt;

&lt;p&gt;Hardcoding pixel values breaks the moment your input resolution changes. FFmpeg gives you position variables that reference the dimensions of each stream at runtime:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;W&lt;/code&gt; and &lt;code&gt;H&lt;/code&gt; refer to the main video's width and height&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;w&lt;/code&gt; and &lt;code&gt;h&lt;/code&gt; refer to the overlay's width and height&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Bottom-right corner with 10px padding:&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; main.mp4 &lt;span class="nt"&gt;-i&lt;/span&gt; watermark.png &lt;span class="nt"&gt;-filter_complex&lt;/span&gt; &lt;span class="s2"&gt;"[0:v][1:v]overlay=W-w-10:H-h-10"&lt;/span&gt; output.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Center the overlay:&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; main.mp4 &lt;span class="nt"&gt;-i&lt;/span&gt; overlay.png &lt;span class="nt"&gt;-filter_complex&lt;/span&gt; &lt;span class="s2"&gt;"[0:v][1:v]overlay=(W-w)/2:(H-h)/2"&lt;/span&gt; output.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  FFmpeg Picture-in-Picture with Scaling
&lt;/h2&gt;

&lt;p&gt;Chain a &lt;code&gt;scale&lt;/code&gt; filter before the overlay:&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; main.mp4 &lt;span class="nt"&gt;-i&lt;/span&gt; pip.mp4 &lt;span class="nt"&gt;-filter_complex&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"[1:v]scale=320:180[pip];[0:v][pip]overlay=W-w-20:20"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  output.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Video-on-Video vs. Image-on-Video
&lt;/h2&gt;

&lt;p&gt;When overlaying video on video, set &lt;code&gt;eof_action&lt;/code&gt; to handle duration mismatches:&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; main.mp4 &lt;span class="nt"&gt;-i&lt;/span&gt; overlay.mp4 &lt;span class="nt"&gt;-filter_complex&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"[0:v][1:v]overlay=10:10:eof_action=pass"&lt;/span&gt; output.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Timed Overlays
&lt;/h2&gt;

&lt;p&gt;Show overlays only during specific time windows:&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; main.mp4 &lt;span class="nt"&gt;-i&lt;/span&gt; lower_third.png &lt;span class="nt"&gt;-filter_complex&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"[0:v][1:v]overlay=0:H-h:enable='between(t,5,15)'"&lt;/span&gt; output.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Running Overlays via API
&lt;/h2&gt;

&lt;p&gt;FFmpeg Micro runs your overlay commands as API calls. Same filter syntax, no infrastructure:&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/main.mp4"},
      {"url": "https://storage.example.com/overlay.png"}
    ],
    "outputFormat": "mp4",
    "options": [
      {"option": "-filter_complex", "argument": "[0:v][1:v]overlay=W-w-10:H-h-10"}
    ]
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pixel format mismatches&lt;/strong&gt;: Use &lt;code&gt;format=auto&lt;/code&gt; on the overlay filter for PNG transparency&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Missing audio&lt;/strong&gt;: Add &lt;code&gt;-map 0:a -c:a copy&lt;/code&gt; to carry audio through&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Using -vf instead of -filter_complex&lt;/strong&gt;: &lt;code&gt;-vf&lt;/code&gt; only works with a single input&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;&lt;strong&gt;How do I put a logo in the corner of a video?&lt;/strong&gt; Use &lt;code&gt;overlay=W-w-10:H-h-10&lt;/code&gt; for bottom-right placement.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I overlay video on video?&lt;/strong&gt; Yes, same syntax. Use &lt;code&gt;eof_action=pass&lt;/code&gt; if the overlay is shorter.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I make picture-in-picture?&lt;/strong&gt; Scale first, then overlay: &lt;code&gt;[1:v]scale=320:180[pip];[0:v][pip]overlay=W-w-20:20&lt;/code&gt;&lt;/p&gt;

</description>
      <category>ffmpeg</category>
      <category>video</category>
      <category>api</category>
      <category>tutorial</category>
    </item>
    <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.to/javidjamae/how-to-post-100-videos-a-week-without-editing-software-2nhn</link>
      <guid>https://dev.to/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.to/javidjamae/how-to-use-ffmpeg-with-kotlin-no-installation-required-37nn</link>
      <guid>https://dev.to/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.to/javidjamae/build-an-automated-youtube-channel-that-publishes-itself-55o2</link>
      <guid>https://dev.to/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.to/javidjamae/ffmpeg-c-copy-explained-how-to-remux-video-without-re-encoding-3k79</link>
      <guid>https://dev.to/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.to/javidjamae/repurpose-a-podcast-episode-into-10-social-clips-automatically-4mpk</link>
      <guid>https://dev.to/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.to/javidjamae/how-to-encode-vp9-video-with-ffmpeg-libvpx-vp9-crf-bitrate-and-quality-guide-2ljo</link>
      <guid>https://dev.to/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>
  </channel>
</rss>
