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.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- ffmpeg-video Installs to ~/.agents/skills/ffmpeg-video/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install ffmpeg-video The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project ffmpeg-video Installs to .agents/skills/ffmpeg-video/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/ffmpeg-video.zipunzip -o ffmpeg-video.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) ffmpeg-video PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
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
- The job is loudness, noise cleanup or speech prep on the audio track: use audio-processing
- You need captions generated from speech: use speech-to-text
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.
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
| Task | Approach |
|---|---|
| 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 resolution | Re-encode video (CRF), copy audio if it is AAC |
| Join files with identical codecs and parameters | concat demuxer, -c copy |
| Join files that differ | concat 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
| Platform | Encoder | Rate control |
|---|---|---|
| macOS (Apple silicon) | h264_videotoolbox, hevc_videotoolbox | -b:v 6M or -q:v 60 (quality 1 to 100) |
| NVIDIA | h264_nvenc, hevc_nvenc, av1_nvenc | -cq 23 -preset p5 |
| Intel/AMD on Linux | h264_vaapi, h264_qsv | Device 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
-ssafter-idecodes and discards everything before the point: correct but slow. Before-iit seeks fast and, when re-encoding, is still frame-accurate in modern ffmpeg.-tis a duration,-tois a position; with-ssbefore-i,-tocounts from the new start.- Odd dimensions fail with libx264 (
width not divisible by 2); use-2inscale. - Phone video with rotation metadata is auto-rotated on re-encode; with
-c copythe metadata stays and players handle it. - Variable frame rate screen recordings can drift audio after cutting; add
-fps_mode cfr -r 30when re-encoding if sync drifts. -map 0copies 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.yaml
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.md
# 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.