ffmpeg Video skill for Codex

Convert, cut and subtitle video with one-pass ffmpeg. Codex probes every input with ffprobe, then chooses stream copy when nothing needs re-encoding and a single CRF encode when it does. Outputs use yuv420p, faststart and the hvc1 tag so they play in browsers and on iPhones, batch loops are rerun-safe with temp files, and results are checked for duration, codecs and clean decoding.

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- ffmpeg-video

Installs to ~/.agents/skills/ffmpeg-video/ for every project on this machine.

104-line POSIX script. No sudo, no telemetry. Read install.sh

Use it in Codex

Start a prompt with $ffmpeg-video, or pick it from /skills. You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.

What Codex reads before loading it

Processes video with ffmpeg and ffprobe - transcode to H.264, HEVC or AV1 MP4 that plays everywhere, trim and cut exactly or losslessly, concatenate, scale and crop, burn in or mux SRT subtitles, extract audio, make thumbnails and GIFs, and batch whole folders with hardware encoders where available. Use when asked to convert or compress a video, cut a clip, join files, add subtitles, shrink a screen recording for upload, fix a video that will not play on iPhone or in a browser, or script ffmpeg over many files.

Use it when

  • A video must be converted, compressed or resized
  • Clips need cutting, joining or turning into GIFs and thumbnails
  • Subtitles must be muxed or burned into a video
  • A video will not play in a browser, on an iPhone or on an upload platform

Use something else when

How to check the result

ffprobe shows the requested codec, pixel format and resolution, duration matches the source or the cut, a null decode reports no errors, and the file plays on the target device.

The mistake it prevents

Encoding 10-bit or yuv422p sources without -pix_fmt yuv420p, producing an MP4 that shows a black screen in many browsers.

~/.agents/skills/ffmpeg-video/SKILL.md

--- name: ffmpeg-video description: Processes video with ffmpeg and ffprobe - transcode to H.264, HEVC or AV1 MP4 that plays everywhere, trim and cut exactly or losslessly, concatenate,… license: MIT ---

A good ffmpeg job starts from ffprobe, picks stream copy whenever no re-encode is needed, re-encodes once with a quality-based rate (CRF) when it is, produces files that play in browsers and on phones (yuv420p, +faststart, right codec tags), and is checked with ffprobe and a look at the output, not assumed from the command.

Probe first

ffprobe -v error -show_entries \
  format=duration,size,bit_rate:stream=index,codec_type,codec_name,profile,width,height,pix_fmt,r_frame_rate,channels,sample_rate \
  -of json in.mov

Note the codecs, pixel format (10-bit yuv420p10le and yuv422p do not play in many browsers), frame rate (variable frame rate from phones and screen recorders), rotation side data, audio channel layout, and subtitle or data streams that may not fit the target container.

Decide: copy or encode

TaskApproach
Change container only (MKV to MP4), codecs already compatible-c copy (seconds, lossless)
Cut at rough points, speed matters-ss before -i with -c copy; cuts snap to keyframes
Cut at exact frames-ss before -i, re-encode video
Smaller file, wider compatibility, new resolutionRe-encode video (CRF), copy audio if it is AAC
Join files with identical codecs and parametersconcat demuxer, -c copy
Join files that differconcat filter, re-encode (see references)

Core commands

# Universal MP4: H.264 + AAC, plays in every browser and phone
ffmpeg -i in.mov -c:v libx264 -crf 23 -preset medium -pix_fmt yuv420p \
  -c:a aac -b:a 128k -movflags +faststart out.mp4

# Downscale to 720p keeping aspect ratio (-2 keeps the width even)
ffmpeg -i in.mp4 -vf "scale=-2:720" -c:v libx264 -crf 23 -c:a copy out-720p.mp4

# HEVC for Apple devices (hvc1 tag is required for QuickTime/Safari playback)
ffmpeg -i in.mov -c:v libx265 -crf 26 -preset medium -tag:v hvc1 -pix_fmt yuv420p \
  -c:a aac -b:a 128k -movflags +faststart out-hevc.mp4

# AV1 (smallest files, slower encode, modern players only)
ffmpeg -i in.mov -c:v libsvtav1 -crf 35 -preset 8 -pix_fmt yuv420p10le -c:a libopus -b:a 96k out.webm

# Trim, fast and lossless (starts on the keyframe at or before 00:01:05)
ffmpeg -ss 00:01:05 -i in.mp4 -t 30 -c copy clip.mp4

# Trim, frame-accurate (re-encodes)
ffmpeg -ss 00:01:05.200 -i in.mp4 -t 30 -c:v libx264 -crf 20 -c:a aac clip-exact.mp4

# Concatenate identical-format files
printf "file '%s'\n" part1.mp4 part2.mp4 part3.mp4 > list.txt
ffmpeg -f concat -safe 0 -i list.txt -c copy joined.mp4

# Extract audio without re-encoding, or as MP3
ffmpeg -i in.mp4 -vn -c:a copy audio.m4a
ffmpeg -i in.mp4 -vn -c:a libmp3lame -q:a 2 audio.mp3

# Thumbnail at 10 s, and a poster sheet of 1 frame every 30 s
ffmpeg -ss 10 -i in.mp4 -frames:v 1 -q:v 2 thumb.jpg
ffmpeg -i in.mp4 -vf "fps=1/30,scale=320:-2,tile=4x3" -frames:v 1 sheet.jpg

CRF guide for libx264: 18 is visually lossless, 23 default, 28 small. Each +6 roughly halves the size. libx265 CRF about 4 to 6 higher gives similar quality; SVT-AV1 around 30 to 38. Use -preset slow when encode time does not matter.

Subtitles

# Soft subtitles (toggleable): MP4 needs mov_text, MKV takes SRT as-is
ffmpeg -i in.mp4 -i subs.srt -map 0 -map 1 -c copy -c:s mov_text \
  -metadata:s:s:0 language=eng out-subs.mp4

# Burned in (always visible; required for most social platforms)
ffmpeg -i in.mp4 -vf "subtitles=subs.srt:force_style='FontSize=22,Outline=2'" \
  -c:v libx264 -crf 20 -c:a copy out-burned.mp4

The subtitles filter needs ffmpeg built with libass. Check with ffmpeg -hide_banner -filters | grep subtitles; some package-manager builds omit it. Paths with colons or quotes must be escaped inside the filter string; copy the SRT to a simple name first. Generating the SRT from speech belongs to $speech-to-text.

Hardware encoders

PlatformEncoderRate control
macOS (Apple silicon)h264_videotoolbox, hevc_videotoolbox-b:v 6M or -q:v 60 (quality 1 to 100)
NVIDIAh264_nvenc, hevc_nvenc, av1_nvenc-cq 23 -preset p5
Intel/AMD on Linuxh264_vaapi, h264_qsvDevice setup flags required

Hardware encoders are much faster but larger at equal quality. Use them for previews and bulk screen recordings; use software encoders for final masters. List what your build has with ffmpeg -hide_banner -encoders | grep -E 'videotoolbox|nvenc|qsv|vaapi'.

Batch a folder

mkdir -p out
for f in in/*.mov; do
  base=$(basename "${f%.*}")
  [ -e "out/$base.mp4" ] && continue                     # rerun-safe
  ffmpeg -nostdin -hide_banner -loglevel error -i "$f" \
    -c:v libx264 -crf 23 -preset medium -pix_fmt yuv420p \
    -c:a aac -b:a 128k -movflags +faststart "out/$base.tmp.mp4" \
  && mv "out/$base.tmp.mp4" "out/$base.mp4" \
  || echo "FAILED $f" >> out/failures.txt
done

-nostdin stops ffmpeg from eating the loop’s input; the temp name means an interrupted encode never looks finished. Never pass -y against the input folder; write to out/.

Gotchas

  • -ss after -i decodes and discards everything before the point: correct but slow. Before -i it seeks fast and, when re-encoding, is still frame-accurate in modern ffmpeg.
  • -t is a duration, -to is a position; with -ss before -i, -to counts from the new start.
  • Odd dimensions fail with libx264 (width not divisible by 2); use -2 in scale.
  • Phone video with rotation metadata is auto-rotated on re-encode; with -c copy the metadata stays and players handle it.
  • Variable frame rate screen recordings can drift audio after cutting; add -fps_mode cfr -r 30 when re-encoding if sync drifts.
  • -map 0 copies every stream, including data streams MP4 cannot hold; map what you need (-map 0:v:0 -map 0:a:0) when muxing fails.
  • 10-bit HDR sources need tone mapping to look right in SDR; see the references.
  • Encoding needs no network, but installing ffmpeg does; ask before installing in a sandbox.

For more recipes (crop, pad to 9:16, speed changes, watermark, concat filter, HDR to SDR, HLS segments, two-pass target size), read references/filter-recipes.md when the core commands above do not cover the job.

Verification

ffprobe -v error -show_entries format=duration,size:stream=codec_name,width,height,pix_fmt -of compact out.mp4
ffmpeg -v error -i out.mp4 -f null - && echo "decodes cleanly"
  • Duration matches the source (or the requested cut) within one frame or two.
  • Codec, pixel format and resolution are what was asked for.
  • Watch the first and last seconds and one point in the middle; check A/V sync.
  • For delivery targets (browser, iPhone, a platform’s upload limits), test playback there.

Report

**Input:** demo.mov (ProRes 422, 3840x2160, 29.97 fps, 4:12, 6.1 GB)
**Output:** demo-1080p.mp4 (H.264 High, yuv420p, 1920x1080, AAC 128k, faststart, 182 MB)
**Command:** ffmpeg -i demo.mov -vf scale=-2:1080 -c:v libx264 -crf 22 -preset slow -pix_fmt yuv420p -c:a aac -b:a 128k -movflags +faststart demo-1080p.mp4
**Checks:** duration 4:12.13 both, decodes without errors, plays in Safari and Chrome

What the zip contains

Codex reads bundled files only when a step points to them. Every one is shown here, so you can review it before installing.

agents/openai.yamlgenerated from this page

Gives the Codex app a display name, a one-line description and a starter prompt for this skill.

interface:
  display_name: "ffmpeg Video"
  short_description: "Convert, cut and subtitle video with one-pass ffmpeg"
  default_prompt: "$ffmpeg-video Compress this 4 GB screen recording to under 200 MB without it looking bad"
references/filter-recipes.md132 lines, 5.3 KB
# ffmpeg filter recipes

Read this when the core commands in SKILL.md do not cover the job. Each recipe re-encodes
video unless it says otherwise; append the usual output options
(`-c:v libx264 -crf 23 -pix_fmt yuv420p -c:a aac -b:a 128k -movflags +faststart`) for MP4.

## Framing

```bash
# Fit 16:9 into a vertical 1080x1920 frame with black bars (no cropping)
ffmpeg -i in.mp4 -vf "scale=1080:1920:force_original_aspect_ratio=decrease,pad=1080:1920:(ow-iw)/2:(oh-ih)/2:color=black,setsar=1" out-916.mp4

# Centre-crop 16:9 to 9:16 (fills the frame, loses the sides)
ffmpeg -i in.mp4 -vf "crop=ih*9/16:ih,scale=1080:1920" out-916-crop.mp4

# Blurred background instead of black bars
ffmpeg -i in.mp4 -filter_complex "[0:v]split[a][b];[a]scale=1080:1920:force_original_aspect_ratio=increase,crop=1080:1920,boxblur=20:2[bg];[b]scale=1080:-2[fg];[bg][fg]overlay=(W-w)/2:(H-h)/2" out-916-blur.mp4

# Remove black borders: detect, then crop with the suggested values
ffmpeg -ss 30 -i in.mp4 -t 10 -vf cropdetect -f null - 2>&1 | grep -o 'crop=[0-9:]*' | sort | uniq -c | sort -rn | head -1
```

## Time

```bash
# 2x speed, video and audio (atempo accepts 0.5 to 100; chain for more)
ffmpeg -i in.mp4 -filter_complex "[0:v]setpts=PTS/2[v];[0:a]atempo=2.0[a]" -map "[v]" -map "[a]" fast.mp4

# Slow motion 0.5x, video only
ffmpeg -i in.mp4 -vf "setpts=2*PTS" -an slow.mp4

# Timelapse: keep 1 frame in 30
ffmpeg -i in.mp4 -vf "select='not(mod(n,30))',setpts=N/(30*TB)" -an timelapse.mp4

# Change frame rate (drops/duplicates frames; use minterpolate only if you accept artefacts)
ffmpeg -i in.mp4 -vf "fps=30" out-30fps.mp4
```

## Joining files that differ

The concat demuxer needs identical codecs, resolution, frame rate and audio layout. When
they differ, normalise inside the concat filter:

```bash
ffmpeg -i a.mp4 -i b.mov -filter_complex \
 "[0:v]scale=1920:1080:force_original_aspect_ratio=decrease,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,setsar=1,fps=30[v0];
  [1:v]scale=1920:1080:force_original_aspect_ratio=decrease,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,setsar=1,fps=30[v1];
  [0:a]aresample=48000,aformat=channel_layouts=stereo[a0];
  [1:a]aresample=48000,aformat=channel_layouts=stereo[a1];
  [v0][a0][v1][a1]concat=n=2:v=1:a=1[v][a]" \
 -map "[v]" -map "[a]" -c:v libx264 -crf 22 -c:a aac joined.mp4
```

An input without an audio track breaks `concat` with `a=1`; give it silence with
`-f lavfi -t <duration> -i anullsrc=r=48000:cl=stereo` as an extra input.

## Overlays

```bash
# Logo watermark at 50 percent opacity, 20 px from the bottom-right corner
ffmpeg -i in.mp4 -i logo.png -filter_complex \
 "[1:v]scale=200:-1,format=rgba,colorchannelmixer=aa=0.5[wm];[0:v][wm]overlay=W-w-20:H-h-20" \
 -c:a copy out-wm.mp4

# Text (needs a build with drawtext / libfreetype; check ffmpeg -filters)
ffmpeg -i in.mp4 -vf "drawtext=fontfile=/path/Inter.ttf:text='Draft':fontsize=48:[email protected]:x=w-tw-30:y=30" out-text.mp4

# Picture-in-picture: webcam in the corner of a screen recording
ffmpeg -i screen.mp4 -i cam.mp4 -filter_complex "[1:v]scale=iw/4:-2[pip];[0:v][pip]overlay=W-w-30:H-h-30[v]" -map "[v]" -map '0:a?' out-pip.mp4
```

## Size targets

```bash
# Two-pass to hit a file size: bitrate_kbps = target_MB * 8192 / seconds - audio_kbps
ffmpeg -y -i in.mp4 -c:v libx264 -b:v 1800k -pass 1 -an -f null /dev/null
ffmpeg -i in.mp4 -c:v libx264 -b:v 1800k -pass 2 -c:a aac -b:a 128k out.mp4
rm -f ffmpeg2pass-*.log*
```

On Windows replace `/dev/null` with `NUL`. Use two-pass only for hard size limits; CRF is
better for everything else.

## Frames and previews

```bash
# One PNG per second
ffmpeg -i in.mp4 -vf fps=1 frames/%04d.png

# Frames at scene changes (threshold 0.3; lower catches more)
ffmpeg -i in.mp4 -vf "select='gt(scene,0.3)'" -fps_mode vfr scenes/%03d.png

# High-quality GIF via a generated palette
ffmpeg -ss 5 -t 4 -i in.mp4 -vf "fps=12,scale=480:-1:flags=lanczos,split[a][b];[a]palettegen[p];[b][p]paletteuse" out.gif
```

GIFs are huge; an MP4 or WebM loop is usually 10x smaller if the destination accepts it.

## HDR to SDR

iPhone and drone footage is often HLG or PQ 10-bit. Re-encoding it to 8-bit without tone
mapping looks washed out. With a build that has `zscale` (libzimg):

```bash
ffmpeg -i hdr.mov -vf "zscale=t=linear:npl=100,format=gbrpf32le,zscale=p=bt709,tonemap=hable:desat=0,zscale=t=bt709:m=bt709:r=tv,format=yuv420p" \
  -c:v libx264 -crf 20 -c:a copy sdr.mp4
```

Without `zscale`, either keep HDR (`libx265 -pix_fmt yuv420p10le -tag:v hvc1` with the
colour metadata copied) or convert on a machine with a fuller ffmpeg build.

## Streaming segments

```bash
# Single-rendition HLS for a static host (VOD)
ffmpeg -i in.mp4 -c:v libx264 -crf 22 -g 60 -keyint_min 60 -sc_threshold 0 -c:a aac -b:a 128k \
  -f hls -hls_time 6 -hls_playlist_type vod -hls_segment_filename 'hls/seg_%03d.ts' hls/index.m3u8
```

Fixed GOP (`-g` = 2 s worth of frames) keeps segments equal length. For adaptive bitrate
ladders, produce one rendition per resolution and a master playlist (`-var_stream_map`),
or use a packaging service.

## Audio inside video

```bash
ffmpeg -i in.mp4 -af "volume=0.5" -c:v copy quieter.mp4             # halve volume
ffmpeg -i in.mp4 -an -c:v copy silent.mp4                           # drop audio
ffmpeg -i video.mp4 -i music.m4a -map 0:v -map 1:a -c:v copy -shortest replaced.mp4
```

Loudness normalisation and cleanup belong to the audio-processing skill.

About this skill

Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.

esc
Just launched

Codex Skills Pro is live

79 premium skills for Codex, each with scripts, references and an evidence report that proves the result.

Bought one by one$882.21
All 79 skills$49
94% off

That's $0.62 a skill, paid once.

Get all 79 for $49Or pick single skills from $4.99 →

Secure checkout by Creem