Lesson 7 of 10

Custom FFmpeg Options and Filters

Go beyond presets: pass raw FFmpeg options to resize, crop, set quality, and add text overlays. Learn which options are allowed.

Follow along with a live API key. 100 free minutes, no credit card.

Get free API key

When presets aren't enough

Presets cover the common cases, but sometimes you need exact control: a specific width, a particular CRF value, or a text overlay. For that, FFmpeg Micro exposes an options array that lets you pass raw FFmpeg options directly. Each element is an object shaped like { option, argument? }, and when you provide options, it overrides preset entirely.

You already have createTranscode, waitForJob, and getDownloadUrl from Lesson 3, so here we'll focus on building the options array and calling createTranscode(...).

Your first custom job

Here's a verified job that resizes a video to 480px wide (keeping aspect ratio) and re-encodes it as H.264 at CRF 28. Notice each option and its argument are separate fields:

// resize.mjs
import { createTranscode, waitForJob, getDownloadUrl } from './lib.mjs'

const job = await createTranscode({
  inputs: [{ url: 'https://example.com/input.mp4' }],
  outputFormat: 'mp4',
  options: [
    { option: '-vf', argument: 'scale=480:-2' },
    { option: '-c:v', argument: 'libx264' },
    { option: '-crf', argument: '28' },
  ],
})

await waitForJob(job.id)
console.log('Done:', await getDownloadUrl(job.id))

The -vf option is FFmpeg's video filtergraph. scale=480:-2 sets the width to 480 and lets FFmpeg compute a height that keeps the aspect ratio (the -2 rounds to an even number, which H.264 requires).

The allowed options

The API only accepts a fixed allowlist of options. Anything outside this list is rejected with a 400. These are the options you can use:

-c        codec (all streams)        -movflags   e.g. +faststart
-c:v      video codec                 -f          force format
-c:a      audio codec                 -q:v        video quality (mjpeg)
-crf      constant rate factor        -map        stream mapping
-preset   encoder speed/quality       -ss         seek / start time
-b:v      video bitrate               -t          duration
-b:a      audio bitrate               -stream_loop  loop input
-s        frame size (WxH)            -loop       loop
-r        frame rate                  -framerate  input frame rate
-pix_fmt  pixel format                -frames:v   number of frames
-vf       video filtergraph
-af       audio filtergraph

Flag-only (send with NO argument):  -an   -vn   -shortest

Codec arguments (for -c, -c:v, -c:a) are also constrained to: libx264, libx265, libvpx, libvpx-vp9, libaom-av1, aac, libopus, libvorbis, mp3, and copy. The -preset value must be one of: ultrafast, superfast, veryfast, faster, fast, medium, slow, slower, veryslow, placebo.

Common recipes

A few practical options arrays you can drop straight into createTranscode:

Resize to 720p wide, keep aspect ratio

options: [
  { option: '-vf', argument: 'scale=1280:-2' },
]

Crop to a square, then scale

Filters chain inside a single -vf string, separated by commas. This crops to a centered square (in_h is the input height) and scales it to 512x512:

options: [
  { option: '-vf', argument: 'crop=in_h:in_h,scale=512:512' },
]

Higher-quality H.264 for the web

A lower CRF means higher quality, and a slower preset squeezes out more compression. -movflags +faststart moves the metadata to the front so the video starts playing before it fully downloads:

options: [
  { option: '-c:v', argument: 'libx264' },
  { option: '-crf', argument: '18' },
  { option: '-preset', argument: 'slow' },
  { option: '-movflags', argument: '+faststart' },
]

Copy streams without re-encoding

When you only want to change the container (say, rewrap into MP4) and not touch the pixels or audio, copy is nearly instant and lossless:

options: [
  { option: '-c', argument: 'copy' },
]

Text overlays

Burning text into a video with raw FFmpeg's drawtext filter is fiddly, so FFmpeg Micro provides a virtual option: @text-overlay. Unlike every other option, its argument is an object, not a string:

const job = await createTranscode({
  inputs: [{ url: 'https://example.com/input.mp4' }],
  outputFormat: 'mp4',
  options: [
    {
      option: '@text-overlay',
      argument: {
        text: 'FFmpeg Micro',
        style: { fontSize: 48, fontColor: 'white' },
      },
    },
  ],
})

Only text is required. The optional style object supports fontSize (12 to 200), fontColor, font, x and y (FFmpeg position expressions), lineSpacing, boxBorderW, and charsPerLine.

Putting it together

Build the array in code so you can compose options conditionally, then hand it to createTranscode:

// custom.mjs
import { createTranscode, waitForJob, getDownloadUrl } from './lib.mjs'

const options = [
  { option: '-vf', argument: 'scale=1280:-2' },
  { option: '-c:v', argument: 'libx264' },
  { option: '-crf', argument: '20' },
  { option: '-preset', argument: 'slow' },
  { option: '-movflags', argument: '+faststart' },
]

const job = await createTranscode({
  inputs: [{ url: 'https://example.com/input.mp4' }],
  outputFormat: 'mp4',
  options, // overrides preset
})

await waitForJob(job.id)
console.log('Ready:', await getDownloadUrl(job.id))

What could go wrong: An option that isn't on the allowlist returns a 400 with a details array naming each unsupported option, so check there first. A few specifics: -y and -i are forbidden (overwrite is automatic and inputs come from the inputs field), and -filter_complex is rejected. Use -vf for video filters, or the filters array for multi-input and complex graphs. Flag-only options (-an, -vn, -shortest) must be sent with no argument field at all.

Key takeaways

  • The options array overrides preset when both are present.
  • Each item is { option, argument }; flag-only options omit argument.
  • Stick to the allowlist. Anything else returns a 400.
  • Codec and -preset values are constrained to fixed sets.
  • -filter_complex is out; use -vf, or the filters array for complex graphs.
  • @text-overlay is a virtual option whose argument is an object.

Next up: send more than one input in a single job to merge and combine video and audio.

Build this into your app. No FFmpeg install

One call kicks off a job. No local FFmpeg, no servers, no worker queue to run.

const res = await fetch('https://api.ffmpeg-micro.com/v1/transcodes', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.FFMPEG_MICRO_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    inputs: [{ url: 'gs://your-bucket/input.mp4' }],
    outputFormat: 'mp4',
    preset: { quality: 'medium', resolution: '1080p' },
  }),
})
const job = await res.json() // { id, status: 'pending' }