<?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.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>How to Split Video into Segments with FFmpeg (CLI + API)</title>
      <dc:creator>Javid Jamae</dc:creator>
      <pubDate>Sat, 23 May 2026 10:15:08 +0000</pubDate>
      <link>https://dev.to/javidjamae/how-to-split-video-into-segments-with-ffmpeg-cli-api-2n8p</link>
      <guid>https://dev.to/javidjamae/how-to-split-video-into-segments-with-ffmpeg-cli-api-2n8p</guid>
      <description>&lt;p&gt;Originally published at &lt;a href="https://www.ffmpeg-micro.com/blog/ffmpeg-split-video-into-segments" rel="noopener noreferrer"&gt;ffmpeg-micro.com&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;You need to chop a long video into equal-length clips. Maybe you're building a social media repurposing pipeline, batch-processing uploads for a CMS, or splitting recordings into chapters. FFmpeg's segment muxer handles this, but getting it right means fighting with keyframe alignment, timestamp resets, and infrastructure you don't want to manage.&lt;/p&gt;

&lt;p&gt;This guide covers the exact FFmpeg commands for splitting video, the flags that matter, and how to skip the server setup entirely with an API.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick answer
&lt;/h2&gt;

&lt;p&gt;Split a video into 10-second segments with no re-encoding:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffmpeg &lt;span class="nt"&gt;-i&lt;/span&gt; input.mp4 &lt;span class="nt"&gt;-c&lt;/span&gt; copy &lt;span class="nt"&gt;-f&lt;/span&gt; segment &lt;span class="nt"&gt;-segment_time&lt;/span&gt; 10 &lt;span class="nt"&gt;-reset_timestamps&lt;/span&gt; 1 segment_%03d.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This uses the segment muxer to cut at the nearest keyframe boundary. Each output file gets its own timestamps starting at zero.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the FFmpeg segment muxer works
&lt;/h2&gt;

&lt;p&gt;The segment muxer (&lt;code&gt;-f segment&lt;/code&gt;) tells FFmpeg to write output to multiple files instead of one. It splits based on a time interval you set with &lt;code&gt;-segment_time&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffmpeg &lt;span class="nt"&gt;-i&lt;/span&gt; input.mp4 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-c&lt;/span&gt; copy &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-f&lt;/span&gt; segment &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-segment_time&lt;/span&gt; 30 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-reset_timestamps&lt;/span&gt; 1 &lt;span class="se"&gt;\&lt;/span&gt;
  output_%03d.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Key flags:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;-f segment&lt;/code&gt; activates the segment muxer&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-segment_time 30&lt;/code&gt; sets the target duration for each segment in seconds&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-reset_timestamps 1&lt;/code&gt; resets timestamps to zero for each segment (without this, players show wrong seek positions)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-c copy&lt;/code&gt; copies streams without re-encoding (fast, but cuts only at keyframes)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;output_%03d.mp4&lt;/code&gt; is the numbered output pattern (&lt;code&gt;000&lt;/code&gt;, &lt;code&gt;001&lt;/code&gt;, &lt;code&gt;002&lt;/code&gt;, ...)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;code&gt;%03d&lt;/code&gt; pattern in the output filename is required. FFmpeg increments the number for each new segment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting a segment manifest
&lt;/h2&gt;

&lt;p&gt;If your pipeline needs to know what segments were created and their exact timestamps, add &lt;code&gt;-segment_list&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffmpeg &lt;span class="nt"&gt;-i&lt;/span&gt; input.mp4 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-c&lt;/span&gt; copy &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-f&lt;/span&gt; segment &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-segment_time&lt;/span&gt; 30 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-reset_timestamps&lt;/span&gt; 1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-segment_list&lt;/span&gt; segments.csv &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-segment_list_type&lt;/span&gt; csv &lt;span class="se"&gt;\&lt;/span&gt;
  segment_%03d.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The CSV output looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;segment_000.mp4,0.000000,30.030000
segment_001.mp4,30.030000,60.060000
segment_002.mp4,60.060000,85.418750
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each row has the filename, start time in seconds, and end time in seconds. You can also use &lt;code&gt;-segment_list_type flat&lt;/code&gt; for just filenames or &lt;code&gt;-segment_list_type ffconcat&lt;/code&gt; for FFmpeg concat demuxer format.&lt;/p&gt;

&lt;h2&gt;
  
  
  Timestamp-based filenames with strftime
&lt;/h2&gt;

&lt;p&gt;For automation pipelines where you're processing videos on a schedule, timestamp-based filenames prevent collisions:&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;-c&lt;/span&gt; copy &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-f&lt;/span&gt; segment &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-segment_time&lt;/span&gt; 30 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-reset_timestamps&lt;/span&gt; 1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-strftime&lt;/span&gt; 1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"clip_%Y%m%d_%H%M%S.mp4"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This produces files like &lt;code&gt;clip_20260523_143000.mp4&lt;/code&gt;. Useful when you're processing multiple source videos into the same output directory.&lt;/p&gt;

&lt;h2&gt;
  
  
  The keyframe problem
&lt;/h2&gt;

&lt;p&gt;When you use &lt;code&gt;-c copy&lt;/code&gt; (stream copy, no re-encoding), FFmpeg can only cut at keyframe boundaries. If your video has keyframes every 2 seconds and you request 5-second segments, your actual segments might be 4 or 6 seconds long.&lt;/p&gt;

&lt;p&gt;Two ways to handle this:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Accept keyframe-aligned cuts&lt;/strong&gt; (fast, slightly imprecise):&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;-f&lt;/span&gt; segment &lt;span class="nt"&gt;-segment_time&lt;/span&gt; 5 &lt;span class="nt"&gt;-reset_timestamps&lt;/span&gt; 1 output_%03d.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Force exact timing&lt;/strong&gt; (requires re-encoding, 10-50x slower):&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;-c&lt;/span&gt;:v libx264 &lt;span class="nt"&gt;-crf&lt;/span&gt; 23 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-force_key_frames&lt;/span&gt; &lt;span class="s2"&gt;"expr:gte(t,n_forced*5)"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-f&lt;/span&gt; segment &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-segment_time&lt;/span&gt; 5 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-reset_timestamps&lt;/span&gt; 1 &lt;span class="se"&gt;\&lt;/span&gt;
  output_%03d.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second approach re-encodes the video and inserts keyframes at exact 5-second intervals. Accurate, but dramatically slower depending on your hardware and encoding settings.&lt;/p&gt;

&lt;p&gt;For most batch processing and content repurposing workflows, keyframe-aligned cuts are good enough.&lt;/p&gt;

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

&lt;p&gt;Running FFmpeg on your own server means managing binaries, scaling for concurrent jobs, and handling timeouts on long videos. The FFmpeg Micro API handles the infrastructure so you can focus on your pipeline logic.&lt;/p&gt;

&lt;p&gt;The API uses &lt;code&gt;-ss&lt;/code&gt; (seek) and &lt;code&gt;-t&lt;/code&gt; (duration) per transcode job, which gives you precise control over exactly what you extract from each video.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Extract a 30-second segment starting at 1 minute:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;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;"b5f5a9c0-9e33-4e77-8a5b-6a0c2cd9c0b3"&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;"output_format"&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="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;Check status and download the result:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl https://api.ffmpeg-micro.com/v1/transcodes/JOB_ID &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;

curl https://api.ffmpeg-micro.com/v1/transcodes/JOB_ID/download &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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Split a 5-minute video into 30-second segments programmatically:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="n"&gt;API_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;YOUR_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;BASE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.ffmpeg-micro.com/v1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;VIDEO_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://storage.example.com/video.mp4&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;SEGMENT_SECONDS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;
&lt;span class="n"&gt;TOTAL_SECONDS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;

&lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bearer &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;API_KEY&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Content-Type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;jobs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&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="n"&gt;TOTAL_SECONDS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SEGMENT_SECONDS&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&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="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;BASE&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/transcodes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;inputs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;VIDEO_URL&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;outputFormat&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mp4&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;options&lt;/span&gt;&lt;span class="sh"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;option&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-ss&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;argument&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;)},&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;option&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-t&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;argument&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SEGMENT_SECONDS&lt;/span&gt;&lt;span class="p"&gt;)},&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;option&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-c&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;argument&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;copy&lt;/span&gt;&lt;span class="sh"&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="n"&gt;job&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;resp&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="n"&gt;jobs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Segment &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;s-&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;SEGMENT_SECONDS&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;s: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All 10 jobs run in parallel on cloud infrastructure. No server, no queue management, no timeout handling on your end.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.ffmpeg-micro.com" rel="noopener noreferrer"&gt;Get a free API key&lt;/a&gt; and try splitting your first video.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common pitfalls when splitting video
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Forgetting &lt;code&gt;-reset_timestamps 1&lt;/code&gt;.&lt;/strong&gt; Without this flag, each segment keeps the timestamps from the original video. Players show the wrong position on the seek bar, and some players won't play the segments at all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Audio sync drift with &lt;code&gt;-c copy&lt;/code&gt;.&lt;/strong&gt; Stream copy can sometimes drift audio out of sync at cut points. If you hear pops or sync issues, switch to &lt;code&gt;-c:a aac&lt;/code&gt; to re-encode just the audio track while keeping video as copy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output pattern without &lt;code&gt;%d&lt;/code&gt;.&lt;/strong&gt; If you forget the number pattern in the output filename, FFmpeg overwrites the same file for every segment. Always include &lt;code&gt;%03d&lt;/code&gt; or similar.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Segment time vs. actual duration.&lt;/strong&gt; With &lt;code&gt;-c copy&lt;/code&gt;, segments won't be exactly the duration you requested. They'll be close, but keyframe alignment means they might vary by 1-2 seconds. If you need frame-accurate cuts, you have to re-encode.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Large video timeouts.&lt;/strong&gt; Running the segment muxer locally on a 2-hour video is fine. Running it on a server with a 30-second HTTP timeout kills the job mid-split. If you're processing long videos in a web pipeline, use an async API or background worker.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  How do I split a video into equal parts with FFmpeg?
&lt;/h3&gt;

&lt;p&gt;Use the segment muxer: &lt;code&gt;ffmpeg -i input.mp4 -c copy -f segment -segment_time 60 -reset_timestamps 1 part_%03d.mp4&lt;/code&gt;. Replace &lt;code&gt;60&lt;/code&gt; with your desired segment length in seconds. Segments will be approximately equal, with variation depending on keyframe positions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I split video without re-encoding?
&lt;/h3&gt;

&lt;p&gt;Yes. Use &lt;code&gt;-c copy&lt;/code&gt; with the segment muxer for near-instant splitting. The tradeoff is that cuts happen at keyframe boundaries, so segment durations won't be frame-accurate. For most batch processing and content repurposing workflows, keyframe-aligned cuts are close enough.&lt;/p&gt;

&lt;h3&gt;
  
  
  What's the difference between the segment muxer and using -ss with -t?
&lt;/h3&gt;

&lt;p&gt;The segment muxer (&lt;code&gt;-f segment&lt;/code&gt;) automatically splits into multiple files in a single FFmpeg pass. The &lt;code&gt;-ss&lt;/code&gt;/&lt;code&gt;-t&lt;/code&gt; approach extracts one specific clip per command. The segment muxer is faster for creating many segments from one video locally. The &lt;code&gt;-ss&lt;/code&gt;/&lt;code&gt;-t&lt;/code&gt; approach works better with APIs and cloud pipelines where each segment runs as an independent job.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does the FFmpeg segment muxer work with audio files?
&lt;/h3&gt;

&lt;p&gt;Yes. Same syntax with audio containers: &lt;code&gt;ffmpeg -i podcast.mp3 -c copy -f segment -segment_time 600 chunk_%03d.mp3&lt;/code&gt;. This splits a podcast into 10-minute chunks without re-encoding.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I split video into segments with an API?
&lt;/h3&gt;

&lt;p&gt;FFmpeg Micro lets you submit transcode jobs via HTTP with &lt;code&gt;-ss&lt;/code&gt; and &lt;code&gt;-t&lt;/code&gt; options to extract specific segments. Each job runs on cloud infrastructure with automatic scaling. &lt;a href="https://www.ffmpeg-micro.com" rel="noopener noreferrer"&gt;Sign up for a free API key&lt;/a&gt; and POST to &lt;code&gt;/v1/transcodes&lt;/code&gt; with your input URL, output format, and seek/duration options.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Last verified: May 2026 with FFmpeg 7.x and FFmpeg Micro API v1&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ffmpeg</category>
      <category>video</category>
      <category>api</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Secure Video Transcoding API: No FFmpeg Server to Patch</title>
      <dc:creator>Javid Jamae</dc:creator>
      <pubDate>Fri, 22 May 2026 10:28:05 +0000</pubDate>
      <link>https://dev.to/javidjamae/secure-video-transcoding-api-no-ffmpeg-server-to-patch-3cdi</link>
      <guid>https://dev.to/javidjamae/secure-video-transcoding-api-no-ffmpeg-server-to-patch-3cdi</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.ffmpeg-micro.com/blog/secure-video-transcoding-api" rel="noopener noreferrer"&gt;ffmpeg-micro.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;FFmpeg has over 100 known CVEs. If you’re running it on a server you manage, every one of those is your problem. Patching, network isolation, access control, storage permissions, log scrubbing. That’s before you write a single line of video processing code.&lt;/p&gt;

&lt;p&gt;A managed transcoding API flips the security model. You send an HTTP request. The API processes your video on isolated infrastructure and hands back the result. Your attack surface shrinks from "an entire FFmpeg server" to "one API key."&lt;/p&gt;

&lt;h2&gt;
  
  
  What "secure video transcoding" actually means
&lt;/h2&gt;

&lt;p&gt;Most guides about secure video transcoding focus on the wrong layer. They talk about JWT tokens and reverse proxies in front of self-hosted FFmpeg. That's bolting a lock onto a door you don't need to open.&lt;/p&gt;

&lt;p&gt;Secure video transcoding means three things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Transport security.&lt;/strong&gt; Every byte moves over HTTPS. No plaintext video data on the wire.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Authentication.&lt;/strong&gt; Only your code can trigger jobs. No anonymous access, no shared credentials.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data lifecycle.&lt;/strong&gt; Video files don't persist after processing unless you explicitly store them. No orphaned files in a misconfigured S3 bucket.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;FFmpeg Micro covers all three by default. There's nothing to configure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Authentication: Bearer token, one header
&lt;/h2&gt;

&lt;p&gt;Every request to the FFmpeg Micro API requires a Bearer token in the Authorization header. No API key in query strings. No unsigned requests.&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;The API key is hashed with SHA-256 before storage. The raw key never hits a database. If someone compromises the database, they get hashes, not keys.&lt;/p&gt;

&lt;p&gt;Compare that to self-hosted FFmpeg, where you're responsible for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Implementing your own auth middleware&lt;/li&gt;
&lt;li&gt;Rotating credentials&lt;/li&gt;
&lt;li&gt;Rate limiting to prevent abuse&lt;/li&gt;
&lt;li&gt;Logging access without leaking video URLs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With a managed API, all of that is handled. You store one API key, send one header, and you're done.&lt;/p&gt;

&lt;h2&gt;
  
  
  Secure file uploads: presigned URLs
&lt;/h2&gt;

&lt;p&gt;Uploading video to a self-hosted FFmpeg server usually means accepting file uploads directly. That opens you to oversized payloads, malicious files, and storage exhaustion attacks.&lt;/p&gt;

&lt;p&gt;FFmpeg Micro uses a three-step presigned URL flow instead:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Request an upload URL&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://api.ffmpeg-micro.com/v1/upload/presigned-url &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;'{
    "filename": "interview-raw.mp4",
    "contentType": "video/mp4",
    "fileSize": 52428800
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The API validates the file type and size before generating the URL. Files over 2GB are rejected. Only video, audio, and image MIME types are accepted.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: Upload directly to cloud storage&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; PUT &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$UPLOAD_URL&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: video/mp4"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data-binary&lt;/span&gt; @interview-raw.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your video goes straight to Google Cloud Storage. It never passes through the API server. No bandwidth bottleneck, no server-side file handling vulnerabilities.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3: Confirm the upload&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://api.ffmpeg-micro.com/v1/upload/confirm &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;'{
    "filename": "20260522-143045-interview-raw.mp4",
    "fileSize": 52428800
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The API verifies the file actually landed in storage and the size matches. If the file isn't there or the sizes don't match, the confirmation fails. No phantom references to files that don't exist.&lt;/p&gt;

&lt;h2&gt;
  
  
  No persistent video storage
&lt;/h2&gt;

&lt;p&gt;After a transcode completes, you get a signed download URL that expires in 10 minutes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl https://api.ffmpeg-micro.com/v1/transcodes/JOB_ID/download &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;/code&gt;&lt;/pre&gt;

&lt;/div&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;"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.googleapis.com/...?X-Goog-Algorithm=GOOG4-RSA-SHA256&amp;amp;X-Goog-Expires=600&amp;amp;..."&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 URL is time-limited and cryptographically signed. After 10 minutes, it's dead. The output files themselves are cleaned up automatically. Your video doesn't sit in someone else's storage indefinitely, waiting for a misconfigured bucket policy to expose it.&lt;/p&gt;

&lt;p&gt;This is the opposite of how most self-hosted setups work, where output files accumulate in /tmp or an S3 bucket until someone remembers to write a cleanup cron job.&lt;/p&gt;

&lt;h2&gt;
  
  
  The self-hosted security burden
&lt;/h2&gt;

&lt;p&gt;Running your own FFmpeg server means owning a stack of security responsibilities that have nothing to do with video processing:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;FFmpeg CVEs are constant.&lt;/strong&gt; The FFmpeg project has disclosed over 100 CVEs since 2010. Heap overflows, null pointer dereferences, out-of-bounds reads. Every one of them is a potential remote code execution if you're accepting untrusted input files. Keeping FFmpeg patched on your own server is a recurring ops task that never ends.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Storage misconfigurations are the #1 cloud data breach vector.&lt;/strong&gt; If you're writing transcoded output to S3 or GCS, one wrong bucket policy and your customers' videos are public. AWS reports that storage misconfiguration is involved in the majority of cloud data exposures.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Network exposure.&lt;/strong&gt; A self-hosted FFmpeg server needs inbound access to receive files and outbound access to deliver results. That's two attack surfaces to manage, plus whatever internal network the server sits on.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No isolation between jobs.&lt;/strong&gt; On a shared FFmpeg server, one user's malformed input file could crash the process handling another user's job. Container isolation helps, but you're building and maintaining that yourself.&lt;/p&gt;

&lt;p&gt;With a managed API, all of these are someone else's full-time job. You don't patch FFmpeg. You don't configure bucket policies. You don't manage network ACLs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common security pitfalls (and how to avoid them)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Leaking API keys in client-side code.&lt;/strong&gt; Never put your FFmpeg Micro API key in frontend JavaScript. Call the API from your backend server. If you need client-side uploads, use the presigned URL flow. The upload URL is temporary and scoped to one file.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Not validating file types before upload.&lt;/strong&gt; The presigned URL endpoint validates MIME types for you, but you should also validate on your end before sending. Don't let users upload arbitrary files to your workflow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ignoring download URL expiry.&lt;/strong&gt; The signed download URLs expire in 10 minutes. Don't store them. Fetch a fresh one each time you need it. If you're passing URLs to downstream services, make sure they consume the file within the window.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hardcoding API keys.&lt;/strong&gt; Use environment variables. Store them in your platform's secret manager (Vercel, Railway, AWS Secrets Manager). Not in your repo, not in a config file committed to git.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Is my video data encrypted in transit?
&lt;/h3&gt;

&lt;p&gt;Yes. All FFmpeg Micro API endpoints are HTTPS-only. Video uploads go directly to Google Cloud Storage over TLS. Download URLs are also HTTPS with cryptographic signatures.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I restrict API access by IP address?
&lt;/h3&gt;

&lt;p&gt;FFmpeg Micro authenticates by API key, not IP allowlist. For additional network-level restrictions, call the API from a backend service within your own VPC and control access at that layer.&lt;/p&gt;

&lt;h3&gt;
  
  
  What happens to my video files after processing?
&lt;/h3&gt;

&lt;p&gt;Output files are accessible via signed URLs for 10 minutes. After that, the URLs expire. Files are cleaned up automatically. There's no long-term storage of your video content on FFmpeg Micro's infrastructure.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I rotate my API key?
&lt;/h3&gt;

&lt;p&gt;Generate a new key from your FFmpeg Micro dashboard, update your environment variables, and the old key stops working immediately. No downtime, no overlap window where both keys are active.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is FFmpeg Micro SOC 2 compliant?
&lt;/h3&gt;

&lt;p&gt;FFmpeg Micro runs on Google Cloud Platform, which is SOC 2 Type II certified. The API layer enforces authentication, encrypted transport, and automatic data cleanup. For specific compliance questions, contact support.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Last verified: 2026-05-22 against FFmpeg Micro API v1&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>api</category>
      <category>ffmpeg</category>
      <category>devops</category>
    </item>
    <item>
      <title>How to Use FFmpeg with Ruby on Rails (No Installation Required)</title>
      <dc:creator>Javid Jamae</dc:creator>
      <pubDate>Thu, 21 May 2026 10:13:58 +0000</pubDate>
      <link>https://dev.to/javidjamae/how-to-use-ffmpeg-with-ruby-on-rails-no-installation-required-j5b</link>
      <guid>https://dev.to/javidjamae/how-to-use-ffmpeg-with-ruby-on-rails-no-installation-required-j5b</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-ruby-on-rails" rel="noopener noreferrer"&gt;ffmpeg-micro.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;You need to process video in your Ruby on Rails app. Users upload clips, and you need to convert formats, resize for mobile, or extract thumbnails. You search "ruby ffmpeg" and find streamio-ffmpeg, a gem that wraps the FFmpeg binary.&lt;/p&gt;

&lt;p&gt;It works great locally. Then you deploy to Heroku.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; You can process video from Ruby without installing FFmpeg anywhere. Send an HTTP request to a cloud API, get results back. Works on Heroku, Render, Fly.io, or any platform that can make outbound HTTP calls.&lt;/p&gt;

&lt;h2&gt;
  
  
  The System Call Approach (And Why It Breaks)
&lt;/h2&gt;

&lt;p&gt;The raw Ruby approach looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="nb"&gt;system&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"ffmpeg -i input.mp4 -c:v libx264 -crf 23 output.mp4"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or with backticks for capturing output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sb"&gt;`ffmpeg -i input.mp4 -vf scale=-2:720 output.mp4 2&amp;gt;&amp;amp;1`&lt;/span&gt;
&lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="s2"&gt;"FFmpeg failed: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="vg"&gt;$?&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;success?&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On your dev machine, fine. In production:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Heroku doesn't include FFmpeg. You need a buildpack, which adds cold-start time and maintenance burden.&lt;/li&gt;
&lt;li&gt;Render and Fly.io require custom Dockerfiles just to get the binary available.&lt;/li&gt;
&lt;li&gt;System calls block your Puma worker. That thread is stuck until FFmpeg finishes.&lt;/li&gt;
&lt;li&gt;You own codec updates, security patches, and temp file cleanup.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The streamio-ffmpeg Gem
&lt;/h2&gt;

&lt;p&gt;streamio-ffmpeg is the most popular Ruby FFmpeg wrapper:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s2"&gt;"streamio-ffmpeg"&lt;/span&gt;

&lt;span class="n"&gt;movie&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;FFMPEG&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Movie&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"input.mp4"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;movie&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transcode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"output.mp4"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"-c:v libx264 -crf 23"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cleaner syntax, but it still shells out to the FFmpeg binary. No binary, no transcoding. The gem hasn't had a significant update in years, and it doesn't handle async processing or cloud storage.&lt;/p&gt;

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

&lt;p&gt;FFmpeg Micro is a cloud API that runs FFmpeg for you. One HTTP request from Ruby, and you get a transcoded file back. No binary, no Dockerfile, no buildpack.&lt;/p&gt;

&lt;p&gt;With Ruby's built-in Net::HTTP:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s2"&gt;"net/http"&lt;/span&gt;
&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s2"&gt;"json"&lt;/span&gt;

&lt;span class="n"&gt;api_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;ENV&lt;/span&gt;&lt;span class="p"&gt;.&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;"FFMPEG_MICRO_API_KEY"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;uri&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;URI&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="n"&gt;request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Net&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uri&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="s2"&gt;"Authorization"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Bearer &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Content-Type"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"application/json"&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;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="ss"&gt;inputs: &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="ss"&gt;url: &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="ss"&gt;outputFormat: &lt;/span&gt;&lt;span class="s2"&gt;"mp4"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;preset: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;quality: &lt;/span&gt;&lt;span class="s2"&gt;"high"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;resolution: &lt;/span&gt;&lt;span class="s2"&gt;"1080p"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Net&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;HTTP&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;uri&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hostname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;port&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;use_ssl: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;request&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="k"&gt;end&lt;/span&gt;

&lt;span class="n"&gt;job&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&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;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"Job ID: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you're using Faraday (common in Rails apps):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;conn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Faraday&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;url: &lt;/span&gt;&lt;span class="s2"&gt;"https://api.ffmpeg-micro.com"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;request&lt;/span&gt; &lt;span class="ss"&gt;:json&lt;/span&gt;
  &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;response&lt;/span&gt; &lt;span class="ss"&gt;:json&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;conn&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="s2"&gt;"/v1/transcodes"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Authorization"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Bearer &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="no"&gt;ENV&lt;/span&gt;&lt;span class="p"&gt;.&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;"FFMPEG_MICRO_API_KEY"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="ss"&gt;inputs: &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="ss"&gt;url: &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="ss"&gt;outputFormat: &lt;/span&gt;&lt;span class="s2"&gt;"mp4"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;preset: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;quality: &lt;/span&gt;&lt;span class="s2"&gt;"high"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;resolution: &lt;/span&gt;&lt;span class="s2"&gt;"1080p"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="n"&gt;job&lt;/span&gt; &lt;span class="o"&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;body&lt;/span&gt;
&lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"Job ID: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both do the same thing. Pick whichever fits your project.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checking Job Status and Downloading Results
&lt;/h2&gt;

&lt;p&gt;Transcoding takes seconds to minutes depending on file size. Poll for completion:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;job_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="kp"&gt;loop&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="nb"&gt;sleep&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
  &lt;span class="n"&gt;uri&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;URI&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="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;job_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Net&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uri&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="s2"&gt;"Authorization"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Bearer &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

  &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Net&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;HTTP&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;uri&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hostname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;port&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;use_ssl: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;request&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="k"&gt;end&lt;/span&gt;

  &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&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;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;break&lt;/span&gt; &lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="sx"&gt;%w[queued processing]&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;include?&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="s2"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"completed"&lt;/span&gt;
  &lt;span class="n"&gt;uri&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;URI&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="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;job_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/download"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Net&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uri&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="s2"&gt;"Authorization"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Bearer &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

  &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Net&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;HTTP&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;uri&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hostname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;port&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;use_ssl: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;request&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="k"&gt;end&lt;/span&gt;

  &lt;span class="n"&gt;download&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&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;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"Download URL: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;download&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The download endpoint returns a signed URL valid for 10 minutes. Save the file or redirect your user to it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Advanced: Custom FFmpeg Options
&lt;/h2&gt;

&lt;p&gt;Presets handle most use cases. For full control, pass FFmpeg options directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;conn&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="s2"&gt;"/v1/transcodes"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Authorization"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Bearer &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="no"&gt;ENV&lt;/span&gt;&lt;span class="p"&gt;.&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;"FFMPEG_MICRO_API_KEY"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="ss"&gt;inputs: &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="ss"&gt;url: &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="ss"&gt;outputFormat: &lt;/span&gt;&lt;span class="s2"&gt;"webm"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;options: &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;option: &lt;/span&gt;&lt;span class="s2"&gt;"-c:v"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;argument: &lt;/span&gt;&lt;span class="s2"&gt;"libvpx-vp9"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;option: &lt;/span&gt;&lt;span class="s2"&gt;"-crf"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;argument: &lt;/span&gt;&lt;span class="s2"&gt;"30"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;option: &lt;/span&gt;&lt;span class="s2"&gt;"-b:v"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;argument: &lt;/span&gt;&lt;span class="s2"&gt;"0"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;option: &lt;/span&gt;&lt;span class="s2"&gt;"-c:a"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;argument: &lt;/span&gt;&lt;span class="s2"&gt;"libopus"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same FFmpeg flags you'd use on the command line, passed as structured JSON. This converts MP4 to WebM using VP9 and Opus.&lt;/p&gt;

&lt;h2&gt;
  
  
  Uploading Local Files
&lt;/h2&gt;

&lt;p&gt;If your video isn't at a public URL, upload it first. Three steps:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Step 1: Get a presigned upload URL&lt;/span&gt;
&lt;span class="n"&gt;presign&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;conn&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="s2"&gt;"/v1/upload/presigned-url"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Authorization"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Bearer &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="ss"&gt;filename: &lt;/span&gt;&lt;span class="s2"&gt;"my-video.mp4"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;contentType: &lt;/span&gt;&lt;span class="s2"&gt;"video/mp4"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;fileSize: &lt;/span&gt;&lt;span class="no"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;size&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"/path/to/my-video.mp4"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="n"&gt;upload_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;presign&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"result"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"uploadUrl"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;filename&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;presign&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"result"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"filename"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Step 2: Upload directly to cloud storage&lt;/span&gt;
&lt;span class="n"&gt;upload_conn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Faraday&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;
&lt;span class="n"&gt;upload_conn&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="n"&gt;upload_url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Content-Type"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"video/mp4"&lt;/span&gt;
  &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;binread&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"/path/to/my-video.mp4"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="c1"&gt;# Step 3: Confirm the upload&lt;/span&gt;
&lt;span class="n"&gt;confirm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;conn&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="s2"&gt;"/v1/upload/confirm"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Authorization"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Bearer &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="ss"&gt;filename: &lt;/span&gt;&lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;fileSize: &lt;/span&gt;&lt;span class="no"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;size&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"/path/to/my-video.mp4"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="n"&gt;gcs_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;confirm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"result"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"gcsUrl"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After confirmation, pass the GCS URL as input in your transcode request.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Blocking Puma workers with polling.&lt;/strong&gt; Don't poll in a web request. Use ActiveJob or Sidekiq to check transcode status in a background job.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Forgetting &lt;code&gt;use_ssl: true&lt;/code&gt; with Net::HTTP.&lt;/strong&gt; The API is HTTPS-only. Without SSL, you get a connection refused error, not a helpful message.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Polling too fast.&lt;/strong&gt; A 2-second interval is plenty. Don't hammer the status endpoint every 100ms.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ignoring the &lt;code&gt;failed&lt;/code&gt; status.&lt;/strong&gt; Always check for failure in your polling loop. A failed job never transitions to &lt;code&gt;completed&lt;/code&gt;, so an infinite loop wastes resources.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hardcoding API keys.&lt;/strong&gt; Use &lt;code&gt;ENV.fetch("FFMPEG_MICRO_API_KEY")&lt;/code&gt; or Rails credentials (&lt;code&gt;Rails.application.credentials.ffmpeg_micro_api_key&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Does this work on Heroku?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes. The API approach only needs Ruby's Net::HTTP (included in stdlib) or any HTTP client gem. No FFmpeg binary, no buildpack, no special configuration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What about ActiveStorage videos?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;ActiveStorage handles upload and storage. For processing, grab the blob URL and pass it to the API. You don't need ActiveStorage variants or an FFmpeg preview processor.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How much does FFmpeg Micro cost?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;FFmpeg Micro bills per minute of video processed. The free tier gives you enough minutes to build and test. A 5-minute video costs a few cents to transcode. See the &lt;a href="https://www.ffmpeg-micro.com/pricing" rel="noopener noreferrer"&gt;pricing page&lt;/a&gt; for current rates.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I process video inside a Rails controller action?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You can kick off the job in the controller, but don't wait for it to finish. Return a 202 to the user and check the result with a background job. Sidekiq, GoodJob, or Solid Queue all work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What Ruby version do I need?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Ruby 2.7 or later. The code examples use modern syntax like &lt;code&gt;ENV.fetch&lt;/code&gt; and &lt;code&gt;dig&lt;/code&gt;, but the API works with any Ruby version that supports HTTPS.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Last verified: May 2026. All API examples tested against FFmpeg Micro v1.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ruby</category>
      <category>rails</category>
      <category>ffmpeg</category>
      <category>api</category>
    </item>
    <item>
      <title>How to Normalize Audio with FFmpeg (Volume, Loudnorm, and API)</title>
      <dc:creator>Javid Jamae</dc:creator>
      <pubDate>Wed, 20 May 2026 10:12:57 +0000</pubDate>
      <link>https://dev.to/javidjamae/how-to-normalize-audio-with-ffmpeg-volume-loudnorm-and-api-11cb</link>
      <guid>https://dev.to/javidjamae/how-to-normalize-audio-with-ffmpeg-volume-loudnorm-and-api-11cb</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.ffmpeg-micro.com/blog/ffmpeg-normalize-audio-volume" rel="noopener noreferrer"&gt;ffmpeg-micro.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;FFmpeg audio normalization trips up even experienced developers. The &lt;code&gt;loudnorm&lt;/code&gt; filter alone has 6 parameters, a two-pass workflow that requires parsing JSON from stderr, and behavior that changes depending on whether you feed it MP4, WAV, or MKV. Most guides skip the hard parts.&lt;/p&gt;

&lt;p&gt;This post covers three approaches to fixing audio levels with FFmpeg: simple volume scaling, broadcast-standard loudnorm normalization (EBU R128), and a cloud API that handles it without installing anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick answer: normalize audio to -16 LUFS with FFmpeg
&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;-af&lt;/span&gt; &lt;span class="s2"&gt;"loudnorm=I=-16:TP=-1.5:LRA=11"&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt;:v copy output.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This applies EBU R128 loudnorm normalization targeting -16 LUFS (the standard for streaming platforms). The &lt;code&gt;-c:v copy&lt;/code&gt; flag passes video through untouched so only the audio gets re-encoded.&lt;/p&gt;

&lt;h2&gt;
  
  
  Approach 1: Simple volume adjustment with the volume filter
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;volume&lt;/code&gt; filter is the simplest way to change audio levels. It multiplies every sample by a fixed factor.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Halve the volume:&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;-af&lt;/span&gt; &lt;span class="s2"&gt;"volume=0.5"&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt;:v copy output.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Double the volume:&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;-af&lt;/span&gt; &lt;span class="s2"&gt;"volume=2.0"&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt;:v copy output.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Adjust by decibels (more precise):&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;-af&lt;/span&gt; &lt;span class="s2"&gt;"volume=6dB"&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt;:v copy output.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;volume&lt;/code&gt; filter is predictable but dumb. It doesn't analyze the source audio first, so you can easily clip loud sections or amplify noise in quiet ones. For consistent levels across multiple files, you need actual normalization.&lt;/p&gt;

&lt;h2&gt;
  
  
  Approach 2: EBU R128 normalization with loudnorm
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;loudnorm&lt;/code&gt; filter implements the EBU R128 broadcast standard. Instead of blindly scaling amplitude, it measures perceived loudness (LUFS) and adjusts dynamically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Single-pass normalization (good enough for most cases):&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;-af&lt;/span&gt; &lt;span class="s2"&gt;"loudnorm=I=-16:TP=-1.5:LRA=11"&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt;:v copy output.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What the parameters mean:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;I=-16&lt;/code&gt; sets the target integrated loudness to -16 LUFS. YouTube uses -14, Spotify uses -14, podcasts typically use -16 to -19.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;TP=-1.5&lt;/code&gt; sets the true peak ceiling to -1.5 dBTP. This prevents clipping on lossy codecs like AAC and MP3 that can overshoot 0 dB during decoding.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;LRA=11&lt;/code&gt; sets the loudness range to 11 LU. This controls how much dynamic range the output keeps.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Two-pass normalization (more accurate):&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Pass 1 - Analyze and capture stats:&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;-af&lt;/span&gt; &lt;span class="s2"&gt;"loudnorm=I=-16:TP=-1.5:LRA=11:print_format=json"&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; null - 2&amp;gt;&amp;amp;1 | &lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-12&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pass 2 - Apply the measured values:&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;-af&lt;/span&gt; &lt;span class="s2"&gt;"loudnorm=I=-16:TP=-1.5:LRA=11:measured_I=-19.6:measured_TP=-5.9:measured_LRA=0.6:measured_thresh=-29.6:offset=0.1:linear=true"&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt;:v copy output.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Which approach should you use?
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Best approach&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Quick fix on one file&lt;/td&gt;
&lt;td&gt;volume filter&lt;/td&gt;
&lt;td&gt;Simple, predictable, no analysis needed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Batch normalize for a platform&lt;/td&gt;
&lt;td&gt;loudnorm single-pass&lt;/td&gt;
&lt;td&gt;EBU R128 standard, good enough for streaming&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mastering or archival quality&lt;/td&gt;
&lt;td&gt;loudnorm two-pass&lt;/td&gt;
&lt;td&gt;Most accurate, preserves dynamics&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Automated pipeline&lt;/td&gt;
&lt;td&gt;Cloud FFmpeg API&lt;/td&gt;
&lt;td&gt;No installation, scales with your workflow&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

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

&lt;p&gt;&lt;strong&gt;Forgetting -c:v copy and re-encoding the entire video.&lt;/strong&gt; Without it, FFmpeg re-encodes the video stream too. This takes 10-50x longer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Using volume instead of loudnorm for batch processing.&lt;/strong&gt; The volume filter applies the same gain to every file. loudnorm measures each file and targets a consistent output level.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Setting the true peak ceiling to 0 dBTP.&lt;/strong&gt; Lossy codecs generate intersample peaks during decoding that can exceed the encoded peak by 1-3 dB.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Running loudnorm on already-normalized audio.&lt;/strong&gt; Double-normalizing compresses dynamics further each pass. Check levels first.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;What LUFS level should I target for YouTube?&lt;/strong&gt;&lt;br&gt;
YouTube normalizes all audio to -14 LUFS. Targeting -14 means YouTube won't touch your audio. -16 is close enough that the difference is barely audible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I normalize audio without re-encoding the video?&lt;/strong&gt;&lt;br&gt;
Yes. Use -c:v copy to pass the video stream through unchanged. Only the audio gets re-encoded.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is the difference between loudnorm and dynaudnorm?&lt;/strong&gt;&lt;br&gt;
loudnorm targets a specific LUFS level per the EBU R128 standard. dynaudnorm adjusts volume frame-by-frame and can introduce pumping artifacts. For most use cases, loudnorm is the right choice.&lt;/p&gt;

</description>
      <category>ffmpeg</category>
      <category>audio</category>
      <category>tutorial</category>
      <category>api</category>
    </item>
    <item>
      <title>How to Add a Watermark to Video with FFmpeg (CLI + API)</title>
      <dc:creator>Javid Jamae</dc:creator>
      <pubDate>Tue, 19 May 2026 10:19:23 +0000</pubDate>
      <link>https://dev.to/javidjamae/how-to-add-a-watermark-to-video-with-ffmpeg-cli-api-20pp</link>
      <guid>https://dev.to/javidjamae/how-to-add-a-watermark-to-video-with-ffmpeg-cli-api-20pp</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.ffmpeg-micro.com/blog/ffmpeg-add-watermark-to-video" rel="noopener noreferrer"&gt;ffmpeg-micro.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Quick Answer
&lt;/h2&gt;

&lt;p&gt;FFmpeg adds a watermark to video using the overlay filter. Two inputs (your video + your logo PNG), one filter, one output.&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;-i&lt;/span&gt; logo.png &lt;span class="nt"&gt;-filter_complex&lt;/span&gt; &lt;span class="s2"&gt;"[1:v]scale=100:-1[wm];[0:v][wm]overlay=W-w-10:H-h-10"&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt;:a copy output.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That command scales your logo to 100px wide, pins it to the bottom-right corner with 10px padding, and copies the audio untouched. If you want to skip the FFmpeg installation and do this through an API call instead, jump to the API section below.&lt;/p&gt;

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

&lt;p&gt;The overlay filter combines two video streams into one. Stream &lt;code&gt;[0:v]&lt;/code&gt; is your base video. Stream &lt;code&gt;[1:v]&lt;/code&gt; is the watermark image. The filter positions the watermark on top of the base at the coordinates you specify.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffmpeg &lt;span class="nt"&gt;-i&lt;/span&gt; video.mp4 &lt;span class="nt"&gt;-i&lt;/span&gt; watermark.png &lt;span class="nt"&gt;-filter_complex&lt;/span&gt; &lt;span class="s2"&gt;"overlay=x:y"&lt;/span&gt; output.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;x&lt;/code&gt; and &lt;code&gt;y&lt;/code&gt; are pixel positions measured from the top-left corner. But hardcoded pixel values break when your input resolution changes. That's where FFmpeg's position variables come in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;W&lt;/code&gt; / &lt;code&gt;H&lt;/code&gt; = base video width / height&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;w&lt;/code&gt; / &lt;code&gt;h&lt;/code&gt; = overlay (watermark) width / height&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Position&lt;/th&gt;
&lt;th&gt;Overlay value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Top-left&lt;/td&gt;
&lt;td&gt;&lt;code&gt;overlay=10:10&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Top-right&lt;/td&gt;
&lt;td&gt;&lt;code&gt;overlay=W-w-10:10&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bottom-left&lt;/td&gt;
&lt;td&gt;&lt;code&gt;overlay=10:H-h-10&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bottom-right&lt;/td&gt;
&lt;td&gt;&lt;code&gt;overlay=W-w-10:H-h-10&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Center&lt;/td&gt;
&lt;td&gt;&lt;code&gt;overlay=(W-w)/2:(H-h)/2&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;10&lt;/code&gt; in each expression is padding in pixels. Adjust it to taste.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scaling the Watermark
&lt;/h2&gt;

&lt;p&gt;If your logo is 2000px wide and your video is 1280px, the watermark will cover the entire frame. Always scale first using &lt;code&gt;scale2ref&lt;/code&gt; or a fixed-width scale:&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;-i&lt;/span&gt; logo.png &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=100:-1[wm];[0:v][wm]overlay=W-w-10:H-h-10"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-c&lt;/span&gt;:a copy output.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;scale=100:-1&lt;/code&gt; sets the width to 100px and calculates the height automatically to preserve aspect ratio. For a watermark that's always 10% of the video width:&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;"[1:v]scale=main_w/10:-1[wm];[0:v][wm]overlay=W-w-10:H-h-10"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Semi-Transparent Watermarks
&lt;/h2&gt;

&lt;p&gt;If your watermark PNG already has an alpha channel (transparent background), overlay preserves it automatically. For adding opacity to a fully opaque logo, use the &lt;code&gt;colorchannelmixer&lt;/code&gt; filter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffmpeg &lt;span class="nt"&gt;-i&lt;/span&gt; input.mp4 &lt;span class="nt"&gt;-i&lt;/span&gt; logo.png &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]colorchannelmixer=aa=0.3,scale=100:-1[wm];[0:v][wm]overlay=W-w-10:H-h-10"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-c&lt;/span&gt;:a copy output.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;aa=0.3&lt;/code&gt; sets the alpha channel to 30% opacity. Lower values make the watermark more transparent.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding a Watermark via API (No Server Required)
&lt;/h2&gt;

&lt;p&gt;Running FFmpeg locally works for one-off jobs. But if you're building an app that watermarks user-uploaded video, or automating a content pipeline, you don't want to manage FFmpeg servers.&lt;/p&gt;

&lt;p&gt;FFmpeg Micro handles this with two API calls. Upload your logo once, then watermark any video with a single POST request.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Upload your logo&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Upload your watermark PNG using the 3-step presigned URL flow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Get a presigned upload URL for your logo&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://api.ffmpeg-micro.com/v1/upload/presigned-url &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;$API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "filename": "my-logo.png",
    "contentType": "image/png",
    "fileSize": 24680
  }'&lt;/span&gt;

&lt;span class="c"&gt;# Upload the file to the returned URL&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; PUT &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$UPLOAD_URL&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: image/png"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data-binary&lt;/span&gt; @my-logo.png

&lt;span class="c"&gt;# Confirm the upload&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://api.ffmpeg-micro.com/v1/upload/confirm &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;$API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "filename": "20260519-my-logo.png",
    "fileSize": 24680
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The confirm response gives you a &lt;code&gt;gcsUrl&lt;/code&gt; like &lt;code&gt;gs://bucket/20260519-my-logo.png&lt;/code&gt;. Save this. You only upload the logo once and reuse it across all your watermark jobs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: Watermark a video&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;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 &lt;/span&gt;&lt;span class="nv"&gt;$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://example.com/my-video.mp4" },
      { "url": "gs://your-bucket/20260519-my-logo.png" }
    ],
    "outputFormat": "mp4",
    "filters": [
      { "filter": "[1:v]scale=100:-1[wm];[0:v][wm]overlay=main_w-overlay_w-10:main_h-overlay_h-10" }
    ]
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;filters&lt;/code&gt; field takes the same FFmpeg filter graph syntax you'd use locally. Two inputs, one filter, one API call. No FFmpeg binary to install, no server to scale.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3: Poll and download&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The response includes a job &lt;code&gt;id&lt;/code&gt;. Poll &lt;code&gt;GET /v1/transcodes/{id}&lt;/code&gt; until &lt;code&gt;status&lt;/code&gt; is &lt;code&gt;completed&lt;/code&gt;, then grab the output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl https://api.ffmpeg-micro.com/v1/transcodes/&lt;span class="nv"&gt;$JOB_ID&lt;/span&gt;/download &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;$API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This returns a signed download URL valid for 10 minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Text Watermarks via API
&lt;/h2&gt;

&lt;p&gt;For text-based watermarks (copyright notices, channel branding), FFmpeg Micro has a built-in &lt;code&gt;@text-overlay&lt;/code&gt; virtual option that handles font rendering, word wrap, and positioning:&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 &lt;/span&gt;&lt;span class="nv"&gt;$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://example.com/my-video.mp4" }],
    "outputFormat": "mp4",
    "options": [
      {
        "option": "@text-overlay",
        "argument": {
          "text": "(c) 2026 My Brand",
          "style": {
            "fontSize": 24,
            "fontColor": "white",
            "x": "main_w-text_w-20",
            "y": "main_h-text_h-20",
            "boxBorderW": 8
          }
        }
      }
    ]
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No font files to install, no &lt;code&gt;drawtext&lt;/code&gt; filter compilation issues.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Your FFmpeg build might not have &lt;code&gt;drawtext&lt;/code&gt;.&lt;/strong&gt; The &lt;code&gt;drawtext&lt;/code&gt; filter requires FFmpeg compiled with &lt;code&gt;--enable-libfreetype&lt;/code&gt;. Many default installations skip it. If you see &lt;code&gt;No such filter: 'drawtext'&lt;/code&gt;, you need to rebuild FFmpeg or switch to the image overlay approach. The FFmpeg Micro API handles this for you since &lt;code&gt;@text-overlay&lt;/code&gt; runs on infrastructure where fonts are pre-installed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PNG without alpha channel = ugly rectangle.&lt;/strong&gt; If your logo is a JPEG or a PNG with a solid background, the watermark renders as a rectangle covering your video. Export your logo as PNG with a transparent background before using it as a watermark.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scaling breaks aspect ratio if you set both width and height.&lt;/strong&gt; Use &lt;code&gt;scale=100:-1&lt;/code&gt; (set width, auto-calculate height) instead of &lt;code&gt;scale=100:100&lt;/code&gt; (which stretches the logo).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Position variables differ between &lt;code&gt;-vf&lt;/code&gt; and &lt;code&gt;-filter_complex&lt;/code&gt;.&lt;/strong&gt; In &lt;code&gt;-filter_complex&lt;/code&gt;, use &lt;code&gt;main_w&lt;/code&gt; and &lt;code&gt;main_h&lt;/code&gt; for the base video dimensions, and &lt;code&gt;overlay_w&lt;/code&gt; / &lt;code&gt;overlay_h&lt;/code&gt; for the watermark. In &lt;code&gt;-vf&lt;/code&gt; mode, the shorthand &lt;code&gt;W&lt;/code&gt;, &lt;code&gt;H&lt;/code&gt;, &lt;code&gt;w&lt;/code&gt;, &lt;code&gt;h&lt;/code&gt; also works.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Watermark disappears on short videos.&lt;/strong&gt; If the watermark image has a longer duration than the video, FFmpeg may drop it. Add &lt;code&gt;-shortest&lt;/code&gt; to your command to stop encoding at the end of the shortest input stream.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Can I add a watermark to multiple videos at once?&lt;/strong&gt;&lt;br&gt;
With the CLI, you'd loop through files in a shell script. Through the FFmpeg Micro API, fire off one POST request per video. Each job runs independently on cloud infrastructure, so 100 watermark jobs process in parallel without you managing concurrency.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does watermarking re-encode the entire video?&lt;/strong&gt;&lt;br&gt;
Yes. The overlay filter requires decoding and re-encoding. This means some quality loss unless you set a low CRF value (18 or lower for near-lossless). There's no way to avoid re-encoding when compositing two streams.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What image formats work for watermarks?&lt;/strong&gt;&lt;br&gt;
PNG is the standard choice because it supports transparency. JPEG works but renders with a solid background. SVG doesn't work directly with FFmpeg. Convert SVGs to PNG first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I animate the watermark (fade in, move across the screen)?&lt;/strong&gt;&lt;br&gt;
Yes. FFmpeg's overlay filter accepts expressions for &lt;code&gt;x&lt;/code&gt; and &lt;code&gt;y&lt;/code&gt; that reference the frame number (&lt;code&gt;n&lt;/code&gt;) or timestamp (&lt;code&gt;t&lt;/code&gt;). For example, &lt;code&gt;overlay=10:'if(lt(t,3),H,H-h-10)'&lt;/code&gt; keeps the watermark off-screen for the first 3 seconds, then snaps it into position.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How much does watermarking cost through the API?&lt;/strong&gt;&lt;br&gt;
FFmpeg Micro bills by processed input duration. A 1-minute video costs the same whether you're watermarking, transcoding, or trimming. Check the pricing page at ffmpeg-micro.com/pricing for current rates.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Last verified: 2026-05-19 against FFmpeg 3.3.3 (CLI) and FFmpeg Micro API v1&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ffmpeg</category>
      <category>webdev</category>
      <category>tutorial</category>
      <category>api</category>
    </item>
    <item>
      <title>How to Convert Video Format with FFmpeg (MP4, WebM, MKV, AVI)</title>
      <dc:creator>Javid Jamae</dc:creator>
      <pubDate>Mon, 18 May 2026 10:17:50 +0000</pubDate>
      <link>https://dev.to/javidjamae/how-to-convert-video-format-with-ffmpeg-mp4-webm-mkv-avi-5f77</link>
      <guid>https://dev.to/javidjamae/how-to-convert-video-format-with-ffmpeg-mp4-webm-mkv-avi-5f77</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.ffmpeg-micro.com/blog/ffmpeg-convert-video-format" rel="noopener noreferrer"&gt;ffmpeg-micro.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Converting video between formats is one of the most common FFmpeg operations, and one of the most error-prone. Pick the wrong codec for a container and FFmpeg exits with a cryptic error. Pick the right codec but wrong settings and your file size doubles.&lt;/p&gt;

&lt;p&gt;This guide covers the exact FFmpeg commands for converting between MP4, WebM, MOV, and AVI. Each conversion includes the CLI command and the equivalent API call using FFmpeg Micro, so you can run it from any app or automation tool without installing FFmpeg.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick Answer
&lt;/h2&gt;

&lt;p&gt;To convert MP4 to WebM with FFmpeg:&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;-c&lt;/span&gt;:a libopus output.webm
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or via the FFmpeg Micro API (no FFmpeg install needed):&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 &lt;/span&gt;&lt;span class="nv"&gt;$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://example.com/input.mp4"}],
    "outputFormat": "webm",
    "options": [
      {"option": "-c:v", "argument": "libvpx-vp9"},
      {"option": "-crf", "argument": "30"},
      {"option": "-b:v", "argument": "0"},
      {"option": "-c:a", "argument": "libopus"}
    ]
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Container vs. Codec: Why Format Conversion Breaks
&lt;/h2&gt;

&lt;p&gt;Most format conversion failures come from one misunderstanding: the container (MP4, WebM, MKV) is just a wrapper. The codec (H.264, VP9, AAC) is the actual encoding inside it.&lt;/p&gt;

&lt;p&gt;Every container only accepts certain codecs:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Container&lt;/th&gt;
&lt;th&gt;Video Codecs&lt;/th&gt;
&lt;th&gt;Audio Codecs&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;MP4&lt;/td&gt;
&lt;td&gt;H.264, H.265/HEVC, AV1&lt;/td&gt;
&lt;td&gt;AAC, MP3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WebM&lt;/td&gt;
&lt;td&gt;VP8, VP9, AV1&lt;/td&gt;
&lt;td&gt;Vorbis, Opus&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MOV&lt;/td&gt;
&lt;td&gt;H.264, H.265, ProRes&lt;/td&gt;
&lt;td&gt;AAC, PCM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AVI&lt;/td&gt;
&lt;td&gt;Most codecs (legacy)&lt;/td&gt;
&lt;td&gt;Most codecs (legacy)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MKV&lt;/td&gt;
&lt;td&gt;Virtually any codec&lt;/td&gt;
&lt;td&gt;Virtually any codec&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If you put H.264 video inside a WebM container, FFmpeg fails with: "Only VP8 or VP9 or AV1 video and Vorbis or Opus audio are supported for WebM." The container is rejecting the codec. We confirmed this by running a real API call without specifying VP9, and got exactly this error.&lt;/p&gt;

&lt;h2&gt;
  
  
  Convert MP4 to WebM
&lt;/h2&gt;

&lt;p&gt;WebM is the standard for web video when you need better compression than H.264. VP9 gives you smaller files at the same quality, but encoding takes longer.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffmpeg &lt;span class="nt"&gt;-i&lt;/span&gt; input.mp4 &lt;span class="nt"&gt;-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;-c&lt;/span&gt;:a libopus output.webm
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;-b:v 0&lt;/code&gt; flag tells VP9 to use pure constant quality mode. Without it, VP9 defaults to constrained quality with a target bitrate that usually wastes space.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://api.ffmpeg-micro.com/v1/transcodes &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$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://example.com/input.mp4"}],
    "outputFormat": "webm",
    "options": [
      {"option": "-c:v", "argument": "libvpx-vp9"},
      {"option": "-crf", "argument": "30"},
      {"option": "-b:v", "argument": "0"},
      {"option": "-c:a", "argument": "libopus"}
    ]
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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;"67e11131-a21c-4f60-ba9e-c524f74c3357"&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;"pending"&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;"webm"&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;We ran this against a 13-second 640x360 MP4. The API completed processing in under 1 second.&lt;/p&gt;

&lt;h2&gt;
  
  
  Convert MKV or MOV to MP4
&lt;/h2&gt;

&lt;p&gt;MKV files from video editors and MOV files from iPhones often already contain H.264 video and AAC audio. Since MP4 supports both codecs, you can remux without re-encoding:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CLI (remux, near-instant):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffmpeg &lt;span class="nt"&gt;-i&lt;/span&gt; input.mkv &lt;span class="nt"&gt;-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;p&gt;This copies the streams into a new container. No re-encoding means it finishes in seconds regardless of file length. The &lt;code&gt;-movflags +faststart&lt;/code&gt; flag moves MP4 metadata to the front of the file so browsers can start playback before the download finishes. Always add this for web delivery.&lt;/p&gt;

&lt;p&gt;If the MKV or MOV contains codecs that MP4 doesn't support (like VP9 inside MKV), you need to re-encode:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CLI (re-encode):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffmpeg &lt;span class="nt"&gt;-i&lt;/span&gt; input.mkv &lt;span class="nt"&gt;-c&lt;/span&gt;:v libx264 &lt;span class="nt"&gt;-crf&lt;/span&gt; 23 &lt;span class="nt"&gt;-c&lt;/span&gt;:a aac output.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://api.ffmpeg-micro.com/v1/transcodes &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$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://example.com/input.mkv"}],
    "outputFormat": "mp4"
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The API selects H.264 + AAC by default when you set &lt;code&gt;outputFormat&lt;/code&gt; to &lt;code&gt;mp4&lt;/code&gt;. No codec flags needed for the common case.&lt;/p&gt;

&lt;h2&gt;
  
  
  Convert AVI to MP4
&lt;/h2&gt;

&lt;p&gt;AVI is a legacy format. Most AVI files use older codecs (MPEG-4 Part 2, DivX, Xvid) that need re-encoding for modern playback:&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffmpeg &lt;span class="nt"&gt;-i&lt;/span&gt; input.avi &lt;span class="nt"&gt;-c&lt;/span&gt;:v libx264 &lt;span class="nt"&gt;-crf&lt;/span&gt; 23 &lt;span class="nt"&gt;-c&lt;/span&gt;:a aac output.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://api.ffmpeg-micro.com/v1/transcodes &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$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://example.com/input.avi"}],
    "outputFormat": "mp4"
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Batch Convert Videos via API
&lt;/h2&gt;

&lt;p&gt;The CLI approach breaks down at scale. Converting 50 or 500 files means writing a shell loop and hoping your machine doesn't run out of memory.&lt;/p&gt;

&lt;p&gt;The API handles this natively. Each job runs on its own cloud instance:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;API_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;FFMPEG_MICRO_API_KEY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;convertVideo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;inputUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;outputFormat&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="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="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;inputUrl&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="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;videos&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://storage.example.com/video1.mov&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;https://storage.example.com/video2.avi&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;https://storage.example.com/video3.mkv&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;jobs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;videos&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;convertVideo&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="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="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;`Started &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;jobs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; conversions`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No concurrency limits to manage, no server to scale up.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;WebM + H.264 = instant failure.&lt;/strong&gt; The WebM container only accepts VP8, VP9, or AV1 video. If you set &lt;code&gt;outputFormat: "webm"&lt;/code&gt; without specifying a VP9 codec, the job fails. Always pass &lt;code&gt;-c:v libvpx-vp9&lt;/code&gt; (or &lt;code&gt;libvpx&lt;/code&gt; for VP8) when targeting WebM.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Missing &lt;code&gt;-b:v 0&lt;/code&gt; for VP9 CRF mode.&lt;/strong&gt; Without this flag, VP9 uses constrained quality with a default target bitrate. The result is usually a larger file than necessary. Pair &lt;code&gt;-crf&lt;/code&gt; with &lt;code&gt;-b:v 0&lt;/code&gt; for pure quality-based encoding.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Forgetting &lt;code&gt;-movflags +faststart&lt;/code&gt; for web MP4s.&lt;/strong&gt; Without this, the browser must download the entire file before playback starts. Add it to every MP4 destined for web delivery or streaming.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Re-encoding when remuxing would do.&lt;/strong&gt; If the source codec is compatible with the target container (H.264 in MKV going to MP4), use &lt;code&gt;-c copy&lt;/code&gt; to remux. It finishes instantly and is completely lossless.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Assuming "format" means "codec."&lt;/strong&gt; "Convert to MP4" is ambiguous. MP4 can contain H.264, H.265, or AV1. If you need a specific codec for device compatibility, spell it out with &lt;code&gt;-c:v libx264&lt;/code&gt;.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Can I convert video format without installing FFmpeg?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes. FFmpeg Micro is a cloud API that runs FFmpeg on managed infrastructure. Send an HTTP request with your video URL and target format. No FFmpeg binary, no server to maintain. Get a free API key at ffmpeg-micro.com.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's the difference between remuxing and transcoding?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Remuxing (&lt;code&gt;-c copy&lt;/code&gt;) changes the container without touching the video or audio data. It's near-instant and lossless. Transcoding (&lt;code&gt;-c:v libx264&lt;/code&gt;) decodes and re-encodes the streams, which takes longer and can reduce quality slightly, but lets you change the actual codec.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Which format should I use for web video?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;MP4 with H.264 has the widest browser support. WebM with VP9 gives better compression but slightly less compatibility on older devices. For maximum reach, go with MP4. For bandwidth savings on modern browsers, WebM.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How long does format conversion take via API?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It depends on video length and whether re-encoding is needed. A 13-second 640x360 MP4 to WebM conversion completed in under 1 second on FFmpeg Micro. Longer videos scale roughly linearly with duration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I convert MKV or AVI files to MP4 via API?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes. Pass any publicly accessible video URL (or upload via the presigned upload flow) and set &lt;code&gt;outputFormat: "mp4"&lt;/code&gt;. The API handles codec detection and re-encoding automatically.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Last verified: 2026-05-18 against FFmpeg Micro API v1 and FFmpeg 7.0.2&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ffmpeg</category>
      <category>webdev</category>
      <category>api</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How to Encode H.265 Video with FFmpeg (CRF + Preset Guide)</title>
      <dc:creator>Javid Jamae</dc:creator>
      <pubDate>Sun, 17 May 2026 10:12:57 +0000</pubDate>
      <link>https://dev.to/javidjamae/how-to-encode-h265-video-with-ffmpeg-crf-preset-guide-2efj</link>
      <guid>https://dev.to/javidjamae/how-to-encode-h265-video-with-ffmpeg-crf-preset-guide-2efj</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.ffmpeg-micro.com/blog/ffmpeg-libx265-crf-preset-guide" rel="noopener noreferrer"&gt;ffmpeg-micro.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;H.265 (HEVC) produces files 30-50% smaller than H.264 at the same visual quality. The trade-off is encoding time. Getting the right CRF and preset combination for your use case is the difference between a file that's too big and an encode that takes all night.&lt;/p&gt;

&lt;h2&gt;
  
  
  What CRF and Preset Actually Control
&lt;/h2&gt;

&lt;p&gt;CRF (Constant Rate Factor) sets your quality target. Lower numbers mean higher quality and larger files. The libx265 default is 28, which is roughly equivalent to CRF 23 in H.264.&lt;/p&gt;

&lt;p&gt;Preset controls how hard the encoder works to hit that quality target. Slower presets find better compression at the same CRF, producing smaller files without losing quality. The trade-off is encode time.&lt;/p&gt;

&lt;p&gt;The two settings are independent. CRF picks the quality. Preset picks how efficiently the encoder reaches that quality.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Basic libx265 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 libx265 &lt;span class="nt"&gt;-crf&lt;/span&gt; 28 &lt;span class="nt"&gt;-preset&lt;/span&gt; medium &lt;span class="nt"&gt;-c&lt;/span&gt;:a aac &lt;span class="nt"&gt;-b&lt;/span&gt;:a 128k &lt;span class="nt"&gt;-tag&lt;/span&gt;:v hvc1 output.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Flag breakdown:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;-c:v libx265&lt;/code&gt; selects the H.265/HEVC encoder&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-crf 28&lt;/code&gt; sets quality (lower = better quality, larger file)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-preset medium&lt;/code&gt; balances speed and compression (default)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-c:a aac -b:a 128k&lt;/code&gt; re-encodes audio to AAC at 128kbps&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-tag:v hvc1&lt;/code&gt; marks the stream for Apple/browser playback compatibility&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That &lt;code&gt;-tag:v hvc1&lt;/code&gt; flag is one people miss. Without it, Safari and iOS won't play the file. QuickTime will refuse to open it. If you're encoding for web delivery, always include it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which CRF Value for Which Use Case
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Use Case&lt;/th&gt;
&lt;th&gt;CRF Range&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Archival / master copy&lt;/td&gt;
&lt;td&gt;18-22&lt;/td&gt;
&lt;td&gt;Visually lossless. Large files, but you keep everything.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Streaming / web delivery&lt;/td&gt;
&lt;td&gt;23-28&lt;/td&gt;
&lt;td&gt;Good quality at reasonable file sizes. CRF 26 is the sweet spot for most web video.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Social media / batch processing&lt;/td&gt;
&lt;td&gt;28-32&lt;/td&gt;
&lt;td&gt;Smaller files, acceptable quality for short-form content where compression artifacts are less noticeable.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Preview / thumbnail generation&lt;/td&gt;
&lt;td&gt;32-38&lt;/td&gt;
&lt;td&gt;Low quality is fine. You're optimizing for speed and small size.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Start at CRF 28 and adjust. Encode a 30-second sample, check the output, then move the CRF up or down by 2-3 points until you hit your target file size and quality.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preset Speed vs Compression
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Preset&lt;/th&gt;
&lt;th&gt;Relative Speed&lt;/th&gt;
&lt;th&gt;File Size (same CRF)&lt;/th&gt;
&lt;th&gt;Best For&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ultrafast&lt;/td&gt;
&lt;td&gt;10x&lt;/td&gt;
&lt;td&gt;~40% larger&lt;/td&gt;
&lt;td&gt;Testing, previews&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;fast&lt;/td&gt;
&lt;td&gt;4x&lt;/td&gt;
&lt;td&gt;~15% larger&lt;/td&gt;
&lt;td&gt;Real-time or near-real-time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;medium&lt;/td&gt;
&lt;td&gt;1x (baseline)&lt;/td&gt;
&lt;td&gt;baseline&lt;/td&gt;
&lt;td&gt;General use&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;slow&lt;/td&gt;
&lt;td&gt;0.4x&lt;/td&gt;
&lt;td&gt;~5-10% smaller&lt;/td&gt;
&lt;td&gt;Final renders, overnight batches&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;veryslow&lt;/td&gt;
&lt;td&gt;0.1x&lt;/td&gt;
&lt;td&gt;~10-15% smaller&lt;/td&gt;
&lt;td&gt;Archival. Only worth it for content you'll serve thousands of times.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For most developers building automation pipelines, &lt;code&gt;medium&lt;/code&gt; or &lt;code&gt;slow&lt;/code&gt; is the right answer. &lt;code&gt;veryslow&lt;/code&gt; gives diminishing returns that don't justify 10x the encode time unless you're Netflix.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running H.265 Encodes via API (No libx265 Install)
&lt;/h2&gt;

&lt;p&gt;Installing libx265 means compiling FFmpeg from source on most Linux distros, managing codec dependencies, and dealing with version mismatches. If you're building an automation pipeline, you can skip all of that.&lt;/p&gt;

&lt;p&gt;The FFmpeg Micro API accepts raw FFmpeg options. Same flags, no local install:&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 &lt;/span&gt;&lt;span class="nv"&gt;$API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "inputs": [{"url": "https://storage.example.com/input.mp4"}],
    "outputFormat": "mp4",
    "options": [
      {"option": "-c:v", "argument": "libx265"},
      {"option": "-crf", "argument": "28"},
      {"option": "-preset", "argument": "medium"},
      {"option": "-tag:v", "argument": "hvc1"}
    ]
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The response comes back immediately with a job ID:&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;"trc_abc123"&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;"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;"createdAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-05-17T10:00:00.000Z"&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;Poll &lt;code&gt;GET /v1/transcodes/trc_abc123&lt;/code&gt; until status is &lt;code&gt;completed&lt;/code&gt;, then grab the output from &lt;code&gt;GET /v1/transcodes/trc_abc123/download&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For high-quality archival encodes, just change the CRF and preset:&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 &lt;/span&gt;&lt;span class="nv"&gt;$API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "inputs": [{"url": "https://storage.example.com/input.mp4"}],
    "outputFormat": "mp4",
    "options": [
      {"option": "-c:v", "argument": "libx265"},
      {"option": "-crf", "argument": "22"},
      {"option": "-preset", "argument": "slow"},
      {"option": "-tag:v", "argument": "hvc1"}
    ]
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same two-line change you'd make in the CLI command, but no server to maintain.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Forgetting &lt;code&gt;-tag:v hvc1&lt;/code&gt; for web delivery.&lt;/strong&gt; Apple devices and most browsers need the hvc1 tag to recognize the HEVC stream. Without it, the file plays fine in VLC but fails in Safari, iOS, and many HTML5 video players.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Using H.264 CRF values with libx265.&lt;/strong&gt; CRF 23 in H.264 and CRF 23 in H.265 are not the same quality. H.265's CRF 28 matches H.264's CRF 23. If you copy your H.264 settings directly, you'll get unnecessarily large files.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Choosing &lt;code&gt;veryslow&lt;/code&gt; for batch pipelines.&lt;/strong&gt; The compression gain from &lt;code&gt;slow&lt;/code&gt; to &lt;code&gt;veryslow&lt;/code&gt; is 3-5% smaller files. The time cost is 3-5x longer encodes. For automated pipelines processing hundreds of videos, &lt;code&gt;medium&lt;/code&gt; or &lt;code&gt;slow&lt;/code&gt; is almost always the right trade-off.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Not specifying audio codec explicitly.&lt;/strong&gt; If your input has audio in a format incompatible with the MP4 container (like Vorbis), FFmpeg will fail silently or produce a broken file. Always set &lt;code&gt;-c:a aac&lt;/code&gt; or &lt;code&gt;-c:a copy&lt;/code&gt; (if the source audio is already AAC).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Expecting hardware encoders to support CRF.&lt;/strong&gt; NVENC, VideoToolbox, and QSV don't support FFmpeg's &lt;code&gt;-crf&lt;/code&gt; flag. They use &lt;code&gt;-qp&lt;/code&gt; or &lt;code&gt;-b:v&lt;/code&gt; instead. If you're running on GPU, the libx265 CRF workflow doesn't apply.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Is H.265 always better than H.264?&lt;/strong&gt;&lt;br&gt;
For file size at the same quality, yes. But H.265 encoding is 2-5x slower, and some older devices can't decode it. If your audience is on modern browsers and mobile devices (2020+), H.265 is the better choice. For maximum compatibility with legacy players, stick with H.264.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's the difference between CRF and two-pass encoding?&lt;/strong&gt;&lt;br&gt;
CRF targets constant quality (file size varies). Two-pass targets a specific bitrate (quality varies). Use CRF when you care about quality and can tolerate variable file sizes. Use two-pass when you have a strict file size budget, like streaming with a fixed bitrate cap.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I use CRF with the FFmpeg Micro API?&lt;/strong&gt;&lt;br&gt;
Yes. Pass &lt;code&gt;-crf&lt;/code&gt; and &lt;code&gt;-preset&lt;/code&gt; as options in the API request. The API runs the same FFmpeg binary with the same flags. No difference in output quality or behavior.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What CRF value gives "visually lossless" H.265?&lt;/strong&gt;&lt;br&gt;
CRF 18-20 with preset slow or veryslow. At these settings, most viewers can't distinguish the encode from the source in an A/B comparison. File sizes will be 60-70% smaller than the raw source, compared to 80-90% smaller at CRF 28.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does preset affect quality or just file size?&lt;/strong&gt;&lt;br&gt;
At the same CRF, slower presets produce smaller files at the same quality. They don't improve quality. They improve compression efficiency. Think of it as: same visual result, less wasted bits.&lt;/p&gt;

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

</description>
      <category>ffmpeg</category>
      <category>video</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How to Extract Frames from Video with FFmpeg</title>
      <dc:creator>Javid Jamae</dc:creator>
      <pubDate>Sat, 16 May 2026 10:14:47 +0000</pubDate>
      <link>https://dev.to/javidjamae/how-to-extract-frames-from-video-with-ffmpeg-7kk</link>
      <guid>https://dev.to/javidjamae/how-to-extract-frames-from-video-with-ffmpeg-7kk</guid>
      <description>&lt;p&gt;Originally published at &lt;a href="https://www.ffmpeg-micro.com/blog/extract-frames-from-video-ffmpeg" rel="noopener noreferrer"&gt;ffmpeg-micro.com&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;Extracting frames from video is one of the most common FFmpeg operations, and one of the most confusing. The flags aren't intuitive, the output naming is easy to get wrong, and quality control requires knowing which JPEG encoder settings FFmpeg actually respects.&lt;/p&gt;

&lt;p&gt;This guide covers the patterns developers actually need: single frame at a timestamp, periodic extraction, keyframe-only extraction, quality control, and scaled thumbnails. Every command was tested against a real 640x360 H.264 MP4, and the actual file sizes are included so you know what to expect.&lt;/p&gt;

&lt;h2&gt;
  
  
  Extract a Single Frame at a Specific Timestamp
&lt;/h2&gt;

&lt;p&gt;The most common case. Grab one frame from a specific point in the video.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffmpeg &lt;span class="nt"&gt;-ss&lt;/span&gt; 00:00:05 &lt;span class="nt"&gt;-i&lt;/span&gt; input.mp4 &lt;span class="nt"&gt;-frames&lt;/span&gt;:v 1 frame.jpg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things matter here.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Put &lt;code&gt;-ss&lt;/code&gt; before &lt;code&gt;-i&lt;/code&gt;.&lt;/strong&gt; This tells FFmpeg to seek before decoding, which is dramatically faster on long videos. If you put &lt;code&gt;-ss&lt;/code&gt; after &lt;code&gt;-i&lt;/code&gt;, FFmpeg decodes every frame from the start up to your timestamp. On a 2-hour file, that's the difference between milliseconds and minutes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;-frames:v 1&lt;/code&gt; limits output to one frame.&lt;/strong&gt; Without it, FFmpeg extracts every frame from that timestamp onward. Thousands of JPEGs filling your disk.&lt;/p&gt;

&lt;p&gt;The timestamp accepts &lt;code&gt;HH:MM:SS.ms&lt;/code&gt; or raw seconds:&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;-ss&lt;/span&gt; 90.5 &lt;span class="nt"&gt;-i&lt;/span&gt; input.mp4 &lt;span class="nt"&gt;-frames&lt;/span&gt;:v 1 frame.jpg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output: a single 23KB JPEG at the video's native resolution.&lt;/p&gt;

&lt;h2&gt;
  
  
  Extract Frames at Regular Intervals
&lt;/h2&gt;

&lt;p&gt;To grab one frame per second across the entire video:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffmpeg &lt;span class="nt"&gt;-i&lt;/span&gt; input.mp4 &lt;span class="nt"&gt;-vf&lt;/span&gt; &lt;span class="s2"&gt;"fps=1"&lt;/span&gt; frame_%03d.jpg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;fps&lt;/code&gt; filter value controls the interval:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;fps=0.5&lt;/code&gt; gives one frame every 2 seconds&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;fps=0.1&lt;/code&gt; gives one frame every 10 seconds&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;fps=5&lt;/code&gt; gives five frames per second&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;code&gt;%03d&lt;/code&gt; in the output filename is a C-style format specifier. FFmpeg increments it per frame: &lt;code&gt;frame_001.jpg&lt;/code&gt;, &lt;code&gt;frame_002.jpg&lt;/code&gt;, and so on. Use &lt;code&gt;%04d&lt;/code&gt; if you expect more than 999 frames.&lt;/p&gt;

&lt;p&gt;On a 13-second test video at &lt;code&gt;fps=1&lt;/code&gt;, this produced 14 frames ranging from 23KB to 38KB each. Total extraction time: under 1 second.&lt;/p&gt;

&lt;h2&gt;
  
  
  Extract Only Keyframes
&lt;/h2&gt;

&lt;p&gt;If you don't need every frame and just want the visually significant ones, extract keyframes (I-frames). These are fully encoded frames that mark scene changes or major visual shifts.&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;"select='eq(pict_type,I)'"&lt;/span&gt; &lt;span class="nt"&gt;-vsync&lt;/span&gt; vfr keyframe_%03d.jpg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;-vsync vfr&lt;/code&gt; is critical.&lt;/strong&gt; Without it, FFmpeg duplicates frames to maintain constant frame rate, and you get hundreds of identical images instead of just the keyframes.&lt;/p&gt;

&lt;p&gt;On the same 13-second video, this produced 2 keyframes instead of 14. Much less disk usage, and the frames tend to be the ones you'd pick manually.&lt;/p&gt;

&lt;h2&gt;
  
  
  Generate Scaled Thumbnails
&lt;/h2&gt;

&lt;p&gt;For thumbnail grids or preview strips, combine extraction with the scale filter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffmpeg &lt;span class="nt"&gt;-i&lt;/span&gt; input.mp4 &lt;span class="nt"&gt;-vf&lt;/span&gt; &lt;span class="s2"&gt;"fps=1,scale=320:180"&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt;:v 2 thumb_%03d.jpg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This chains two filters: &lt;code&gt;fps=1&lt;/code&gt; for the extraction interval, and &lt;code&gt;scale=320:180&lt;/code&gt; for the output dimensions. Filter order matters. &lt;code&gt;fps&lt;/code&gt; first reduces the frame count, then &lt;code&gt;scale&lt;/code&gt; resizes only those frames. Reversing works but wastes CPU.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The &lt;code&gt;-q:v&lt;/code&gt; flag controls JPEG quality.&lt;/strong&gt; Range is 2 (highest) to 31 (lowest). Default is around 8-10. For thumbnails displayed at small sizes, &lt;code&gt;-q:v 5&lt;/code&gt; balances quality and file size well.&lt;/p&gt;

&lt;p&gt;At 320x180 with &lt;code&gt;-q:v 2&lt;/code&gt;, thumbnails were 9-12KB each. At native 640x360 resolution, the same quality setting produced 36-38KB files.&lt;/p&gt;

&lt;p&gt;To auto-calculate one dimension and preserve aspect ratio:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffmpeg &lt;span class="nt"&gt;-i&lt;/span&gt; input.mp4 &lt;span class="nt"&gt;-vf&lt;/span&gt; &lt;span class="s2"&gt;"fps=1,scale=320:-1"&lt;/span&gt; thumb_%03d.jpg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;scale=320:-1&lt;/code&gt; sets width to 320 and calculates height automatically. Use &lt;code&gt;-2&lt;/code&gt; instead of &lt;code&gt;-1&lt;/code&gt; if the codec requires even dimensions.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Wrong &lt;code&gt;-ss&lt;/code&gt; placement.&lt;/strong&gt; Putting &lt;code&gt;-ss&lt;/code&gt; after &lt;code&gt;-i&lt;/code&gt; decodes from the start. On long videos, this turns a millisecond operation into a multi-minute wait. Always put &lt;code&gt;-ss&lt;/code&gt; before &lt;code&gt;-i&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Missing &lt;code&gt;-frames:v 1&lt;/code&gt; for single frames.&lt;/strong&gt; Without it, you get every frame from the seek point to the end. On a 1080p video, that's tens of thousands of files.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Forgetting &lt;code&gt;-vsync vfr&lt;/code&gt; with &lt;code&gt;select&lt;/code&gt;.&lt;/strong&gt; Keyframe extraction without &lt;code&gt;-vsync vfr&lt;/code&gt; produces duplicated frames. Hundreds of output files that are mostly identical.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Wrong format specifier.&lt;/strong&gt; &lt;code&gt;frame_%d.jpg&lt;/code&gt; works but produces &lt;code&gt;frame_1.jpg&lt;/code&gt;, &lt;code&gt;frame_2.jpg&lt;/code&gt; without padding. Sorting breaks after 9. Use &lt;code&gt;%03d&lt;/code&gt; or &lt;code&gt;%04d&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output directory doesn't exist.&lt;/strong&gt; FFmpeg won't create directories. &lt;code&gt;ffmpeg -i input.mp4 output/frame_%03d.jpg&lt;/code&gt; fails if &lt;code&gt;output/&lt;/code&gt; doesn't exist. Create it first with &lt;code&gt;mkdir -p output&lt;/code&gt;.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Can I extract frames as PNG instead of JPEG?
&lt;/h3&gt;

&lt;p&gt;Yes. Change the extension to &lt;code&gt;.png&lt;/code&gt;. FFmpeg picks the encoder from the file extension. PNG is lossless but much larger. The same frame that was 23KB as JPEG was 269KB as PNG. About 11x bigger.&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;-ss&lt;/span&gt; 5 &lt;span class="nt"&gt;-i&lt;/span&gt; input.mp4 &lt;span class="nt"&gt;-frames&lt;/span&gt;:v 1 frame.png
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  How do I extract every Nth frame instead of every Nth second?
&lt;/h3&gt;

&lt;p&gt;Use the &lt;code&gt;select&lt;/code&gt; filter with a modulo expression:&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;"select='not(mod(n,30))'"&lt;/span&gt; &lt;span class="nt"&gt;-vsync&lt;/span&gt; vfr every_30th_%03d.jpg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This grabs every 30th frame. For a 30fps video, that's roughly one per second, but frame-count-based instead of time-based.&lt;/p&gt;

&lt;h3&gt;
  
  
  What's the fastest way to extract a single frame?
&lt;/h3&gt;

&lt;p&gt;Put &lt;code&gt;-ss&lt;/code&gt; before &lt;code&gt;-i&lt;/code&gt; and add &lt;code&gt;-frames:v 1&lt;/code&gt;. FFmpeg seeks to the nearest keyframe, decodes forward to your timestamp, outputs one frame, and exits. On a 2GB file, this typically completes in under 100ms.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I extract frames from a specific time range?
&lt;/h3&gt;

&lt;p&gt;Combine &lt;code&gt;-ss&lt;/code&gt; (start) with &lt;code&gt;-t&lt;/code&gt; (duration):&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;-ss&lt;/span&gt; 00:01:00 &lt;span class="nt"&gt;-i&lt;/span&gt; input.mp4 &lt;span class="nt"&gt;-t&lt;/span&gt; 10 &lt;span class="nt"&gt;-vf&lt;/span&gt; &lt;span class="s2"&gt;"fps=1"&lt;/span&gt; range_%03d.jpg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This extracts one frame per second from the 1:00 to 1:10 mark.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is JPEG or PNG better for extracted frames?
&lt;/h3&gt;

&lt;p&gt;JPEG for most cases. It's 10x smaller (23KB vs 269KB for the same 640x360 frame) and good enough for thumbnails, preview images, and visual inspection. Use PNG only when you need lossless pixel data for machine learning training sets or when frames feed into another image processing pipeline.&lt;/p&gt;

&lt;p&gt;If you're building a video processing pipeline and need transcoding, format conversion, or watermarking alongside frame extraction, &lt;a href="https://www.ffmpeg-micro.com" rel="noopener noreferrer"&gt;FFmpeg Micro&lt;/a&gt; handles the heavy lifting through a simple API. One HTTP call handles the transcode, so you can keep your local FFmpeg work focused on frame extraction. Grab a free API key at &lt;a href="https://www.ffmpeg-micro.com" rel="noopener noreferrer"&gt;ffmpeg-micro.com&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Last verified: 2026-05-16 against FFmpeg 3.3.3&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ffmpeg</category>
      <category>video</category>
      <category>thumbnails</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>FFmpeg Subtitles Filter: How to Burn SRT Subtitles Into Video</title>
      <dc:creator>Javid Jamae</dc:creator>
      <pubDate>Thu, 14 May 2026 10:22:01 +0000</pubDate>
      <link>https://dev.to/javidjamae/ffmpeg-subtitles-filter-how-to-burn-srt-subtitles-into-video-2e45</link>
      <guid>https://dev.to/javidjamae/ffmpeg-subtitles-filter-how-to-burn-srt-subtitles-into-video-2e45</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.ffmpeg-micro.com/blog/ffmpeg-subtitles-filter-guide" rel="noopener noreferrer"&gt;ffmpeg-micro.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;FFmpeg's &lt;code&gt;subtitles&lt;/code&gt; filter burns SRT or ASS captions directly into the video pixels. Unlike soft subtitles (which viewers can toggle off), burned-in subtitles are permanent. They show up everywhere: social media players that strip subtitle tracks, messaging apps, and platforms that don't support external caption files.&lt;/p&gt;

&lt;p&gt;The basic command is one line, but the filter's syntax for styling, positioning, and encoding has a few traps that waste hours if you don't know about them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Basic 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;-vf&lt;/span&gt; &lt;span class="s2"&gt;"subtitles=captions.srt"&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt;:v libx264 &lt;span class="nt"&gt;-crf&lt;/span&gt; 23 &lt;span class="nt"&gt;-c&lt;/span&gt;:a copy output.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;FFmpeg reads &lt;code&gt;captions.srt&lt;/code&gt;, renders each subtitle at its timestamp, and encodes the result into a new MP4. The &lt;code&gt;-c:a copy&lt;/code&gt; flag passes audio through untouched so you don't re-encode it for no reason.&lt;/p&gt;

&lt;p&gt;Two things to know up front:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;subtitles&lt;/code&gt; filter requires FFmpeg compiled with libass. If you get &lt;code&gt;No such filter: 'subtitles'&lt;/code&gt;, your build doesn't have it. Most package managers include it by default (&lt;code&gt;brew install ffmpeg&lt;/code&gt; on macOS, &lt;code&gt;apt install ffmpeg&lt;/code&gt; on Ubuntu), but minimal Docker images often skip it.&lt;/li&gt;
&lt;li&gt;The filter reads the SRT file at filter-graph init time. If the file path has spaces or colons, you need to escape them (more on that below).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Styling with force_style
&lt;/h2&gt;

&lt;p&gt;The default appearance (white text, thin black outline) works, but you'll almost always want to customize it. The &lt;code&gt;force_style&lt;/code&gt; parameter accepts ASS override tags:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffmpeg &lt;span class="nt"&gt;-i&lt;/span&gt; input.mp4 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-vf&lt;/span&gt; &lt;span class="s2"&gt;"subtitles=captions.srt:force_style='FontSize=28,PrimaryColour=&amp;amp;H00FFFFFF,OutlineColour=&amp;amp;H00000000,Outline=2,Shadow=1'"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-c&lt;/span&gt;:v libx264 &lt;span class="nt"&gt;-crf&lt;/span&gt; 23 &lt;span class="nt"&gt;-c&lt;/span&gt;:a copy output.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  force_style Parameter Reference
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Parameter&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;th&gt;Example value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;FontSize&lt;/td&gt;
&lt;td&gt;Text size in pixels&lt;/td&gt;
&lt;td&gt;&lt;code&gt;28&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FontName&lt;/td&gt;
&lt;td&gt;Font family&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Arial&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PrimaryColour&lt;/td&gt;
&lt;td&gt;Text fill color (ASS hex: &lt;code&gt;&amp;amp;HAABBGGRR&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;&amp;amp;H00FFFFFF&lt;/code&gt; (white)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OutlineColour&lt;/td&gt;
&lt;td&gt;Outline color&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;&amp;amp;H00000000&lt;/code&gt; (black)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BackColour&lt;/td&gt;
&lt;td&gt;Shadow/background color&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;&amp;amp;H80000000&lt;/code&gt; (50% black)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Outline&lt;/td&gt;
&lt;td&gt;Outline thickness in pixels&lt;/td&gt;
&lt;td&gt;&lt;code&gt;2&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shadow&lt;/td&gt;
&lt;td&gt;Shadow distance in pixels&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BorderStyle&lt;/td&gt;
&lt;td&gt;1 = outline + shadow, 4 = opaque box&lt;/td&gt;
&lt;td&gt;&lt;code&gt;4&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Alignment&lt;/td&gt;
&lt;td&gt;Position on screen (numpad layout)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;2&lt;/code&gt; (bottom center)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MarginV&lt;/td&gt;
&lt;td&gt;Vertical margin from the edge&lt;/td&gt;
&lt;td&gt;&lt;code&gt;30&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bold&lt;/td&gt;
&lt;td&gt;Bold text (1 = on, 0 = off)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;The color format is tricky.&lt;/strong&gt; ASS uses &lt;code&gt;&amp;amp;HAABBGGRR&lt;/code&gt;, not the &lt;code&gt;#RRGGBB&lt;/code&gt; you're used to from CSS. White is &lt;code&gt;&amp;amp;H00FFFFFF&lt;/code&gt;. Yellow is &lt;code&gt;&amp;amp;H0000FFFF&lt;/code&gt;. Red is &lt;code&gt;&amp;amp;H000000FF&lt;/code&gt;. The &lt;code&gt;AA&lt;/code&gt; prefix is transparency (00 = fully opaque, FF = fully transparent).&lt;/p&gt;

&lt;h2&gt;
  
  
  Character Encoding and Escaping
&lt;/h2&gt;

&lt;p&gt;Three escaping rules that catch people off guard:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Colons in file paths.&lt;/strong&gt; On Windows, paths like &lt;code&gt;C:\Users\me\captions.srt&lt;/code&gt; break the filter because &lt;code&gt;:&lt;/code&gt; is a parameter separator. Escape them with backslashes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Single quotes inside force_style.&lt;/strong&gt; The &lt;code&gt;force_style&lt;/code&gt; value is wrapped in single quotes. If your font name contains an apostrophe, escape it with a backslash.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;UTF-8 encoding.&lt;/strong&gt; The SRT file must be UTF-8. If you get garbled characters or blank subtitles, check with &lt;code&gt;file captions.srt&lt;/code&gt;. Convert if needed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;iconv &lt;span class="nt"&gt;-f&lt;/span&gt; UTF-16 &lt;span class="nt"&gt;-t&lt;/span&gt; UTF-8 captions-utf16.srt &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; captions.srt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;&lt;strong&gt;"No such filter: subtitles"&lt;/strong&gt; means your FFmpeg build doesn't include libass. Reinstall with subtitle support: &lt;code&gt;brew install ffmpeg&lt;/code&gt; (macOS), &lt;code&gt;sudo apt install ffmpeg libass-dev&lt;/code&gt; (Ubuntu).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Timing is off after trimming.&lt;/strong&gt; If you use &lt;code&gt;-ss&lt;/code&gt; (seek) to trim the video, subtitle timestamps don't shift automatically. Put &lt;code&gt;-ss&lt;/code&gt; before &lt;code&gt;-i&lt;/code&gt; as an input option.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output file is bigger than the input.&lt;/strong&gt; Burning subtitles forces a full re-encode. Use &lt;code&gt;-crf 23&lt;/code&gt; or lower for quality parity. Don't use &lt;code&gt;-c:v copy&lt;/code&gt; because the filter can't modify copied frames.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Text renders at wrong size after scaling.&lt;/strong&gt; Put subtitles AFTER the scale filter: &lt;code&gt;-vf "scale=1280:720,subtitles=captions.srt"&lt;/code&gt;. Chain order matters.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Does the subtitles filter work with WebM and MKV output?
&lt;/h3&gt;

&lt;p&gt;Yes. The filter burns text into video frames, so it works with any output container.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I use the subtitles filter with a URL instead of a local file?
&lt;/h3&gt;

&lt;p&gt;No. The subtitles filter only reads local files. Download your SRT first with &lt;code&gt;curl&lt;/code&gt; or &lt;code&gt;wget&lt;/code&gt; before passing it to the filter.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I burn subtitles without re-encoding?
&lt;/h3&gt;

&lt;p&gt;You can't. The filter modifies video frames, which requires a full decode-and-encode cycle. To minimize quality loss, match the original codec and use a CRF value equal to or lower than the source.&lt;/p&gt;

&lt;p&gt;Read the &lt;a href="https://www.ffmpeg-micro.com/blog/ffmpeg-subtitles-filter-guide" rel="noopener noreferrer"&gt;full guide with more examples&lt;/a&gt; on ffmpeg-micro.com.&lt;/p&gt;

</description>
      <category>ffmpeg</category>
      <category>video</category>
      <category>tutorial</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How to Learn FFmpeg: The Developer's Guide (2026)</title>
      <dc:creator>Javid Jamae</dc:creator>
      <pubDate>Wed, 13 May 2026 10:16:07 +0000</pubDate>
      <link>https://dev.to/javidjamae/how-to-learn-ffmpeg-the-developers-guide-2026-23pb</link>
      <guid>https://dev.to/javidjamae/how-to-learn-ffmpeg-the-developers-guide-2026-23pb</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.ffmpeg-micro.com/blog/learn-ffmpeg-developer-guide" rel="noopener noreferrer"&gt;ffmpeg-micro.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;FFmpeg has over 400 command-line flags, dozens of codecs, and documentation that reads like a reference manual for people who already know what they're doing. Most developers don't need 90% of it.&lt;/p&gt;

&lt;p&gt;This guide covers the five operations you'll use in almost every project, the tool you should learn before ffmpeg itself, and when it makes sense to stop running FFmpeg locally and use an API instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with ffprobe, Not ffmpeg
&lt;/h2&gt;

&lt;p&gt;Before you process anything, learn what you're working with. &lt;code&gt;ffprobe&lt;/code&gt; ships with FFmpeg and tells you everything about a media file: codecs, resolution, duration, bitrate, frame rate.&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;This prints structured JSON you can pipe into any script. You'll use it constantly to debug why a transcode failed or why a file is bigger than expected.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Five Commands You Actually Need
&lt;/h2&gt;

&lt;p&gt;Most FFmpeg tutorials bury you in options. In practice, these five operations cover about 80% of developer use cases.&lt;/p&gt;

&lt;h3&gt;
  
  
  Convert Between Formats
&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.mov &lt;span class="nt"&gt;-c&lt;/span&gt;:v libx264 &lt;span class="nt"&gt;-c&lt;/span&gt;:a aac output.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This converts a QuickTime file to MP4 with H.264 video and AAC audio. The &lt;code&gt;-c:v&lt;/code&gt; flag sets the video codec, &lt;code&gt;-c:a&lt;/code&gt; sets the audio codec.&lt;/p&gt;

&lt;h3&gt;
  
  
  Compress Video
&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 libx264 &lt;span class="nt"&gt;-crf&lt;/span&gt; 28 &lt;span class="nt"&gt;-preset&lt;/span&gt; fast &lt;span class="nt"&gt;-c&lt;/span&gt;:a aac &lt;span class="nt"&gt;-b&lt;/span&gt;:a 128k compressed.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;CRF (Constant Rate Factor) controls quality. Lower numbers mean higher quality and larger files. 23 is the default. 28 gives you a noticeably smaller file with acceptable quality for most web delivery.&lt;/p&gt;

&lt;h3&gt;
  
  
  Extract 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;-vn&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt;:a libmp3lame &lt;span class="nt"&gt;-q&lt;/span&gt;:a 2 output.mp3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;-vn&lt;/code&gt; flag strips the video stream entirely. &lt;code&gt;-q:a 2&lt;/code&gt; sets MP3 quality on a 0-9 scale (0 is best, 9 is worst).&lt;/p&gt;

&lt;h3&gt;
  
  
  Resize Video
&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;-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 &lt;span class="nt"&gt;-c&lt;/span&gt;:v libx264 &lt;span class="nt"&gt;-preset&lt;/span&gt; fast &lt;span class="nt"&gt;-crf&lt;/span&gt; 23 &lt;span class="nt"&gt;-c&lt;/span&gt;:a copy output.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;-vf scale=-2:720&lt;/code&gt; scales the video to 720 pixels tall and calculates width automatically. The &lt;code&gt;-2&lt;/code&gt; keeps it divisible by 2, which H.264 requires.&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;-ss&lt;/code&gt; is the start time, &lt;code&gt;-t&lt;/code&gt; is the duration in seconds. &lt;code&gt;-c copy&lt;/code&gt; avoids re-encoding, so this runs almost instantly.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to Stop Running FFmpeg Yourself
&lt;/h2&gt;

&lt;p&gt;FFmpeg is the right tool when you're processing files on your own machine. It stops being the right tool when your app needs to process user-uploaded video, or you need video processing inside an automation tool like n8n or Make.com.&lt;/p&gt;

&lt;p&gt;At that point, an API handles the same operations without the infrastructure. &lt;a href="https://www.ffmpeg-micro.com" rel="noopener noreferrer"&gt;FFmpeg Micro&lt;/a&gt; lets you send a video URL and get a processed file back with a single HTTP call.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Forgetting &lt;code&gt;-y&lt;/code&gt; in scripts.&lt;/strong&gt; FFmpeg prompts before overwriting. Add &lt;code&gt;-y&lt;/code&gt; to auto-overwrite in automation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Using &lt;code&gt;-ss&lt;/code&gt; after &lt;code&gt;-i&lt;/code&gt;.&lt;/strong&gt; Put &lt;code&gt;-ss&lt;/code&gt; before &lt;code&gt;-i&lt;/code&gt; for fast seeking on large files.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Assuming codec from extension.&lt;/strong&gt; An &lt;code&gt;.mp4&lt;/code&gt; can hold H.264, H.265, VP9. Always run &lt;code&gt;ffprobe&lt;/code&gt; first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ignoring exit codes.&lt;/strong&gt; Check &lt;code&gt;$?&lt;/code&gt; or you'll silently pass corrupt files downstream.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;&lt;strong&gt;Is FFmpeg hard to learn?&lt;/strong&gt; The basics take an afternoon. Five commands cover 80% of what developers need.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I use FFmpeg without installing it?&lt;/strong&gt; Yes. Cloud APIs like FFmpeg Micro expose the same operations over HTTP.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is FFmpeg free?&lt;/strong&gt; FFmpeg is free and open-source (LGPL/GPL). Running it at scale costs whatever your server costs. API services charge per minute instead.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Last verified May 2026.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ffmpeg</category>
      <category>tutorial</category>
      <category>webdev</category>
      <category>beginners</category>
    </item>
    <item>
      <title>FFmpeg -ss, -t, and -to Flags: Input vs Output Seeking</title>
      <dc:creator>Javid Jamae</dc:creator>
      <pubDate>Mon, 11 May 2026 10:17:17 +0000</pubDate>
      <link>https://dev.to/javidjamae/ffmpeg-ss-t-and-to-flags-input-vs-output-seeking-2b3p</link>
      <guid>https://dev.to/javidjamae/ffmpeg-ss-t-and-to-flags-input-vs-output-seeking-2b3p</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.ffmpeg-micro.com/blog/ffmpeg-ss-t-to-seeking" rel="noopener noreferrer"&gt;ffmpeg-micro.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;Trimming video with FFmpeg looks simple until your 5-second clip comes out as 7 seconds. Or your output starts a few frames late. The culprit is almost always flag order: where you put &lt;code&gt;-ss&lt;/code&gt; relative to &lt;code&gt;-i&lt;/code&gt; completely changes how FFmpeg processes the seek.&lt;/p&gt;

&lt;p&gt;This post covers what &lt;code&gt;-ss&lt;/code&gt;, &lt;code&gt;-t&lt;/code&gt;, and &lt;code&gt;-to&lt;/code&gt; do, why their placement matters, and when to use each combination.&lt;/p&gt;

&lt;h2&gt;
  
  
  What -ss, -t, and -to Actually Do
&lt;/h2&gt;

&lt;p&gt;Three flags control timing in FFmpeg:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;-ss&lt;/code&gt;&lt;/strong&gt; sets the start position (the seek point)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;-t&lt;/code&gt;&lt;/strong&gt; sets the duration of the output&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;-to&lt;/code&gt;&lt;/strong&gt; sets the end timestamp&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;-t&lt;/code&gt; and &lt;code&gt;-to&lt;/code&gt; are mutually exclusive. If you use both, &lt;code&gt;-t&lt;/code&gt; wins and &lt;code&gt;-to&lt;/code&gt; gets ignored silently.&lt;/p&gt;

&lt;p&gt;The critical distinction is where you place &lt;code&gt;-ss&lt;/code&gt;: before &lt;code&gt;-i&lt;/code&gt; (input seeking) or after &lt;code&gt;-i&lt;/code&gt; (output seeking). The two produce different results with different tradeoffs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Input Seeking: Fast but Imprecise
&lt;/h2&gt;

&lt;p&gt;Place &lt;code&gt;-ss&lt;/code&gt; before &lt;code&gt;-i&lt;/code&gt; and FFmpeg jumps directly to the nearest keyframe at or before your timestamp. It doesn't decode anything before that point.&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;-ss&lt;/span&gt; 2 &lt;span class="nt"&gt;-i&lt;/span&gt; input.mp4 &lt;span class="nt"&gt;-t&lt;/span&gt; 5 &lt;span class="nt"&gt;-c&lt;/span&gt; copy output.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is fast. On a 2-hour file, FFmpeg skips straight to the 2-second mark instead of decoding from the beginning.&lt;/p&gt;

&lt;p&gt;The catch: with &lt;code&gt;-c copy&lt;/code&gt; (stream copy), FFmpeg can only cut at keyframes. If the nearest keyframe is at 0.5s instead of 2s, your output starts earlier than you asked for. In testing on a 13-second 640x360 MP4, this command produced a &lt;strong&gt;7.07-second file&lt;/strong&gt; instead of the expected 5 seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Output Seeking: Slow but Frame-Accurate
&lt;/h2&gt;

&lt;p&gt;Place &lt;code&gt;-ss&lt;/code&gt; after &lt;code&gt;-i&lt;/code&gt; and FFmpeg decodes the entire file from the beginning, throwing away frames until it hits your timestamp.&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;-ss&lt;/span&gt; 2 &lt;span class="nt"&gt;-t&lt;/span&gt; 5 output.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This re-encodes by default, which means frame-accurate cuts. The same test file produced exactly &lt;strong&gt;5.005 seconds&lt;/strong&gt;. The timestamp landed right where expected.&lt;/p&gt;

&lt;p&gt;The cost: on long files, FFmpeg decodes everything before the seek point just to discard it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Sweet Spot: Input Seek + Re-encode
&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;-ss&lt;/span&gt; 2 &lt;span class="nt"&gt;-i&lt;/span&gt; input.mp4 &lt;span class="nt"&gt;-t&lt;/span&gt; 5 &lt;span class="nt"&gt;-c&lt;/span&gt;:v libx264 &lt;span class="nt"&gt;-preset&lt;/span&gt; fast output.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Input seeking jumps to the approximate position (fast), then re-encoding trims at exact frame boundaries (precise). Best of both.&lt;/p&gt;

&lt;h2&gt;
  
  
  -t vs -to: Duration vs End Position
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;-t&lt;/code&gt; specifies how long the output should be. &lt;code&gt;-to&lt;/code&gt; specifies an absolute end timestamp.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Extract from 5s, for 3 seconds&lt;/span&gt;
ffmpeg &lt;span class="nt"&gt;-i&lt;/span&gt; input.mp4 &lt;span class="nt"&gt;-ss&lt;/span&gt; 5 &lt;span class="nt"&gt;-t&lt;/span&gt; 3 output.mp4

&lt;span class="c"&gt;# Same result: from 5s, stop at the 8-second mark&lt;/span&gt;
ffmpeg &lt;span class="nt"&gt;-i&lt;/span&gt; input.mp4 &lt;span class="nt"&gt;-ss&lt;/span&gt; 5 &lt;span class="nt"&gt;-to&lt;/span&gt; 8 output.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both produce a 3-second file. When &lt;code&gt;-ss&lt;/code&gt; is before &lt;code&gt;-i&lt;/code&gt;, the timeline resets to zero, so &lt;code&gt;-to&lt;/code&gt; and &lt;code&gt;-t&lt;/code&gt; become equivalent.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Using &lt;code&gt;-c copy&lt;/code&gt; and expecting exact timestamps.&lt;/strong&gt; Stream copy can't cut between keyframes. Drop &lt;code&gt;-c copy&lt;/code&gt; and re-encode for precision.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mixing up &lt;code&gt;-t&lt;/code&gt; and &lt;code&gt;-to&lt;/code&gt; with input seeking.&lt;/strong&gt; When &lt;code&gt;-ss&lt;/code&gt; is before &lt;code&gt;-i&lt;/code&gt;, both flags are relative to the seeked position, not the original file.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Using output seeking on huge files.&lt;/strong&gt; Output seeking at &lt;code&gt;-ss 3:59:00&lt;/code&gt; on a 4-hour file decodes nearly the entire thing. Use input seeking.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Using &lt;code&gt;-to&lt;/code&gt; before &lt;code&gt;-i&lt;/code&gt; on old FFmpeg versions.&lt;/strong&gt; Requires FFmpeg 4.0+. Older versions silently ignore it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Combining &lt;code&gt;-t&lt;/code&gt; and &lt;code&gt;-to&lt;/code&gt;.&lt;/strong&gt; FFmpeg doesn't warn you. It uses &lt;code&gt;-t&lt;/code&gt; and throws away &lt;code&gt;-to&lt;/code&gt;.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Why is my trimmed video longer than expected?&lt;/strong&gt;&lt;br&gt;
You're probably using &lt;code&gt;-c copy&lt;/code&gt;. FFmpeg can only cut at keyframe boundaries with stream copy. Re-encode for frame-accurate timestamps.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's the fastest way to trim a video?&lt;/strong&gt;&lt;br&gt;
Input seeking with stream copy: &lt;code&gt;ffmpeg -ss 30 -i input.mp4 -t 10 -c copy output.mp4&lt;/code&gt;. Finishes in milliseconds but won't be frame-accurate.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Last verified: 2026-05-11 against FFmpeg 7.x&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ffmpeg</category>
      <category>video</category>
      <category>cli</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How to Extract Audio from Video with FFmpeg (MP3, WAV, and API Guide)</title>
      <dc:creator>Javid Jamae</dc:creator>
      <pubDate>Sun, 10 May 2026 10:17:02 +0000</pubDate>
      <link>https://dev.to/javidjamae/how-to-extract-audio-from-video-with-ffmpeg-mp3-wav-and-api-guide-3d5b</link>
      <guid>https://dev.to/javidjamae/how-to-extract-audio-from-video-with-ffmpeg-mp3-wav-and-api-guide-3d5b</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.ffmpeg-micro.com/blog/ffmpeg-extract-audio-from-video" rel="noopener noreferrer"&gt;ffmpeg-micro.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;You recorded a webinar, a YouTube video, or a product demo. Now you need the audio track as a standalone file. Maybe you're repurposing video content as podcast episodes. Maybe your app needs to strip audio from user uploads for transcription. FFmpeg handles this in a single command, but getting the codec and container options right takes some trial and error.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Extract Audio from Video with FFmpeg
&lt;/h2&gt;

&lt;p&gt;The core command is simple. The &lt;code&gt;-vn&lt;/code&gt; flag tells FFmpeg to drop the video stream, and you specify the output format through the file extension or codec flag.&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;-vn&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt;:a libmp3lame &lt;span class="nt"&gt;-b&lt;/span&gt;:a 192k output.mp3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This extracts the audio from &lt;code&gt;input.mp4&lt;/code&gt;, re-encodes it as MP3 at 192 kbps, and writes &lt;code&gt;output.mp3&lt;/code&gt;. The &lt;code&gt;-c:a libmp3lame&lt;/code&gt; flag selects the LAME MP3 encoder, and &lt;code&gt;-b:a 192k&lt;/code&gt; sets a bitrate that balances quality and file size.&lt;/p&gt;

&lt;p&gt;If you just need the raw audio without re-encoding, use &lt;code&gt;-c:a copy&lt;/code&gt; to stream-copy the existing audio codec:&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;-vn&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt;:a copy output.aac
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Stream copying is faster because FFmpeg skips the decode/encode cycle entirely. But the output container must support whatever codec the source video uses. An MP4 with AAC audio can stream-copy to &lt;code&gt;.aac&lt;/code&gt; or &lt;code&gt;.m4a&lt;/code&gt;, but not directly to &lt;code&gt;.mp3&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Extract Audio as WAV (Uncompressed)
&lt;/h2&gt;

&lt;p&gt;WAV is the go-to when you need lossless audio for editing, transcription pipelines, or feeding into speech-to-text models like Whisper.&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;-vn&lt;/span&gt; &lt;span class="nt"&gt;-acodec&lt;/span&gt; pcm_s16le output.wav
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;pcm_s16le&lt;/code&gt; codec produces 16-bit signed PCM, which is what most audio tools expect. WAV files are large. A 5-minute video produces roughly 50 MB of uncompressed audio. If storage matters, use FLAC instead for lossless compression at about half the size:&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;-vn&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt;:a flac output.flac
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Convert Video to MP3 with Custom Quality
&lt;/h2&gt;

&lt;p&gt;MP3 is still the most universal audio format. For podcasts and general distribution, 128-192 kbps covers most use cases. For music or high-fidelity content, bump it to 320 kbps:&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;-vn&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt;:a libmp3lame &lt;span class="nt"&gt;-b&lt;/span&gt;:a 320k output.mp3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also use FFmpeg's variable bitrate mode with &lt;code&gt;-q:a&lt;/code&gt; instead of &lt;code&gt;-b:a&lt;/code&gt;. Lower values mean higher quality:&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;-vn&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt;:a libmp3lame &lt;span class="nt"&gt;-q&lt;/span&gt;:a 2 output.mp3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A &lt;code&gt;-q:a&lt;/code&gt; value of 2 averages around 190 kbps and produces better quality than a fixed 192k bitrate at roughly the same file size.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common Gotchas When Extracting Audio
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Container vs. codec mismatch.&lt;/strong&gt; The most common error is trying to stream-copy audio into an incompatible container. If your source has Opus audio and you try &lt;code&gt;-c:a copy output.mp3&lt;/code&gt;, FFmpeg will fail. Either re-encode or pick a container that supports the source codec (&lt;code&gt;.ogg&lt;/code&gt; or &lt;code&gt;.webm&lt;/code&gt; for Opus).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No audio stream.&lt;/strong&gt; Some screen recordings and generated videos don't have an audio track at all. FFmpeg will error with "Output file does not contain any stream." Check first with &lt;code&gt;ffprobe&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;ffprobe &lt;span class="nt"&gt;-v&lt;/span&gt; quiet &lt;span class="nt"&gt;-show_streams&lt;/span&gt; &lt;span class="nt"&gt;-select_streams&lt;/span&gt; a input.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If this returns nothing, the video has no audio to extract.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sample rate mismatches.&lt;/strong&gt; When feeding extracted audio into pipelines that expect a specific sample rate (like 16kHz for Whisper), set it explicitly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffmpeg &lt;span class="nt"&gt;-i&lt;/span&gt; input.mp4 &lt;span class="nt"&gt;-vn&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt;:a libmp3lame &lt;span class="nt"&gt;-ar&lt;/span&gt; 16000 &lt;span class="nt"&gt;-ac&lt;/span&gt; 1 &lt;span class="nt"&gt;-b&lt;/span&gt;:a 64k output.mp3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;-ar 16000&lt;/code&gt; flag resamples to 16kHz, and &lt;code&gt;-ac 1&lt;/code&gt; downmixes to mono.&lt;/p&gt;

&lt;h2&gt;
  
  
  Extract Audio via API (No FFmpeg Installation)
&lt;/h2&gt;

&lt;p&gt;All those CLI commands work great on your local machine. But if you're building an app that needs to extract audio from user uploads, or automating extraction across hundreds of videos, you don't want to install FFmpeg on a server and manage the infrastructure.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.ffmpeg-micro.com" rel="noopener noreferrer"&gt;FFmpeg Micro&lt;/a&gt; is a cloud API that handles this with a single HTTP request. Send the video URL and your desired output format:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"https://www.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 YOUR_API_KEY"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "inputs": [{"url": "https://example.com/video.mp4"}],
    "outputFormat": "mp3",
    "options": [
      {"option": "-vn", "argument": ""},
      {"option": "-b:a", "argument": "192k"}
    ]
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The API queues the job, processes it in the cloud, and gives you a download URL when it's done. No FFmpeg binary to install. No server to scale. No codec dependencies to manage.&lt;/p&gt;

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