Install
$ agentstack add skill-damionrashford-media-os-ffmpeg-frames-images ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
About
Ffmpeg Frames Images
Context: $ARGUMENTS
Frame extraction and image I/O with ffmpeg: single stills, periodic frame dumps, contact sheets, sprite sheets for scrubbers, high-quality GIFs, and building video back from an image sequence.
Quick start
- Single screenshot at a timestamp → Step 1 (screenshot) + Step 2
- Dump N frames per second to PNG/JPG → Step 1 (sequence) + Step 2
- Contact sheet / thumbnail grid → Step 1 (sheet) + Step 2
- Sprite sheet for HLS scrubber → Step 1 (sprite) + Step 2
- High-quality GIF from video → Step 1 (gif) + Step 2 (two-pass)
- Build video from PNG/JPG sequence → Step 1 (images-to-video) + Step 2
When to use
- Grab a poster/thumbnail frame at a specific time.
- Produce preview grids (contact sheet) for a long video.
- Generate scrubber sprites for a web player (HLS/DASH image tracks).
- Export frames for ML, computer vision, or VFX pipelines.
- Make a looping GIF for docs/social without visible banding.
- Re-encode a rendered image sequence (e.g. Blender/Nuke output) into MP4.
Step 1 — Pick your output
Choose exactly one shape; each has its own command pattern.
screenshot— one still image at a timestamp.sequence— many images at regular intervals (fps=1= 1/sec,fps=1/10= every 10s).sheet— one image that is a grid (tile=MxN) of stamps across the video.sprite— same as sheet but sized for a web player's scrubber.gif— animated GIF (two-command palette workflow).images-to-video— reverse direction: image sequence → MP4.thumbnail-smart— let ffmpeg pick a "representative" frame.keyframes-only— export only frames marked as keyframes.
Step 2 — Build the command
2a. Single screenshot at 1:30
# Input-side seek is FAST (keyframe-snapped, may be a few ms off).
ffmpeg -ss 00:01:30 -i in.mp4 -vframes 1 -q:v 2 out.jpg
# Frame-accurate (SLOW, decodes from 0):
ffmpeg -i in.mp4 -ss 00:01:30 -vframes 1 -q:v 2 out.jpg
# Lossless PNG instead of JPG:
ffmpeg -ss 90 -i in.mp4 -vframes 1 -c:v png out.png
2b. One frame per second (or every N seconds)
# 1 frame / second:
ffmpeg -i in.mp4 -vf fps=1 frame_%04d.jpg
# 1 frame / 10 seconds:
ffmpeg -i in.mp4 -vf fps=1/10 frame_%04d.jpg
# Higher JPG quality (2 = best, 31 = worst):
ffmpeg -i in.mp4 -vf fps=1 -q:v 2 frame_%04d.jpg
2c. Contact sheet 4x4 at 5-second intervals
ffmpeg -i in.mp4 \
-vf "fps=1/5,scale=320:-1,tile=4x4" \
-frames:v 1 sheet.jpg
2d. Sprite sheet for HLS scrubber (10x10 grid, 10s interval, 160px wide)
ffmpeg -i in.mp4 \
-vf "fps=1/10,scale=160:-1,tile=10x10" \
-frames:v 1 sprites.jpg
Each sprite cell is 160 x (160/aspect). For a 16:9 video that's 160x90. With 100 cells and a 10s interval you cover 1000 seconds of runtime per sheet. See [references/recipes.md](references/recipes.md) for HLS EXT-X-IMAGE-STREAM-INF math.
2e. Smart thumbnail (auto-pick representative frame)
ffmpeg -i in.mp4 -vf "thumbnail=100,scale=640:-1" -frames:v 1 thumb.jpg
thumbnail=100 scans 100 consecutive frames and picks the most "typical" one (lowest histogram distance to the group average). Good for avoiding black/blurred frames.
2f. Keyframes only
ffmpeg -i in.mp4 -vf "select='eq(pict_type,I)'" -vsync vfr key_%04d.jpg
2g. High-quality GIF (TWO commands — palettegen, then paletteuse)
# 1) Generate an optimized palette
ffmpeg -i in.mp4 \
-vf "fps=15,scale=480:-1:flags=lanczos,palettegen=max_colors=256" \
-y palette.png
# 2) Apply the palette with dithering
ffmpeg -i in.mp4 -i palette.png \
-lavfi "fps=15,scale=480:-1:flags=lanczos[x];[x][1:v]paletteuse=dither=bayer:bayer_scale=5" \
-y out.gif
There is no reliable one-shot GIF command that gives this quality. The two-pass palettegen → paletteuse flow is the standard.
2h. Video from image sequence
# Note: -framerate goes BEFORE -i (it's an input option for image2).
ffmpeg -framerate 30 -i img_%04d.png \
-c:v libx264 -pix_fmt yuv420p out.mp4
# Glob instead of numeric pattern:
ffmpeg -framerate 30 -pattern_type glob -i 'img_*.png' \
-c:v libx264 -pix_fmt yuv420p out.mp4
# Start numbering at a non-zero index:
ffmpeg -framerate 24 -start_number 100 -i img_%04d.png out.mp4
Step 3 — Verify frame count
Before shipping, confirm you got what you expected.
# How many images landed on disk:
ls frame_*.jpg | wc -l
# Expected count for fps=1/N on a duration T:
# floor(T / N) + 1
# (ffmpeg rounds to the nearest frame; off-by-one is normal.)
# Re-check a produced sheet's dimensions:
ffprobe -v error -select_streams v:0 -show_entries stream=width,height \
-of csv=p=0 sheet.jpg
Available scripts
scripts/frames.py— wrapper around all of the above. Subcommands:
screenshot, every, sheet, sprite, gif, images-to-video. Supports --dry-run and --verbose. Stdlib-only Python; no prompts.
Workflow
# Screenshot at 1m30s:
uv run ${CLAUDE_SKILL_DIR}/scripts/frames.py screenshot \
--input in.mp4 --time 00:01:30 --output thumb.jpg
# 4x4 contact sheet at 5s intervals:
uv run ${CLAUDE_SKILL_DIR}/scripts/frames.py sheet \
--input in.mp4 --cols 4 --rows 4 --interval 5 --width 320 --output sheet.jpg
# Two-pass GIF:
uv run ${CLAUDE_SKILL_DIR}/scripts/frames.py gif \
--input in.mp4 --fps 15 --width 480 --output out.gif
# Rebuild video from PNGs:
uv run ${CLAUDE_SKILL_DIR}/scripts/frames.py images-to-video \
--pattern 'img_%04d.png' --fps 30 --output out.mp4
Reference docs
Read [references/recipes.md](references/recipes.md) when:
- You need more than 8 recipes (there are 30+ in there).
- You need the JPEG quality scale, PNG compression flags, or image2 demuxer options (
pattern_type,start_number,loop,framerate). - You're tuning GIF size vs quality (dither tables, fps trade-offs).
- You're computing sprite sheet geometry for
EXT-X-IMAGE-STREAM-INFplaylists.
Gotchas
-ssbefore-iis FAST but keyframe-snapped — the chosen time snaps to the nearest preceding keyframe. Off by up to the GOP length (often ~2s). Great for thumbnails.-ssafter-iis FRAME-ACCURATE but slow — ffmpeg decodes from frame 0 to the target. Use only when exact frame matters.-q:v 2is best-JPEG-quality. Scale is 2–31, LOWER is better.2is visually lossless-ish,5is web-quality,31is unusable.-frames:v 1vs-vframes 1— both work. Prefer-frames:v(stream-specific, modern form).image2numeric pattern zero-padding must match the%0Ndwidth.img_%04d.pngmeans files must beimg_0001.png, notimg_1.png, or you'll get "could not find codec parameters" errors.- Glob only works if you add
-pattern_type glob. Otherwise ffmpeg treatsimg_*.pngas a literal filename. - GIF quality REQUIRES the two-pass palette workflow. There is no acceptable single-command alternative; a naked
ffmpeg -i in.mp4 out.gifproduces banded, washed-out garbage. paletteuse=dither=bayer:bayer_scale=5is the quality/size sweet spot.dither=nonebands heavily;dither=floyd_steinbergis prettiest but noticeably larger.-frameratevs-r.-framerateis an INPUT option for the image2 demuxer (rate at which images are READ).-ris an OUTPUT option (rate STAMPED on the output). Forimage sequence → video, set-framerate BEFORE -i.- JPG kills alpha. If the source has transparency (captures, overlays), use PNG or WebP.
tilemust match what you feed it. A 4x4 tile expects exactly 16 frames — use-frames:v 1to stop after the first tiled output and trim thefpsto produce at least 16 frames' worth.thumbnail=Nneeds N frames in the window. If the source is shorter than N frames, the filter still works but samples what it has. DefaultNis 100.- On macOS,
ffmpegmay not be on PATH in non-interactive shells. If your script fails withcommand not found, invoke with an absolute path (e.g./opt/homebrew/bin/ffmpeg).
Examples
Example 1: YouTube-style poster frame at 1:30
ffmpeg -ss 00:01:30 -i lecture.mp4 -vframes 1 -q:v 2 -vf "scale=1280:-1" poster.jpg
Result: 1280-wide JPEG, keyframe-snapped (fast).
Example 2: Scrubber sprite sheet for a 1h video
For a 3600s video, sampling every 10s = 360 thumbs. A 19x19 grid (361 cells) covers it:
ffmpeg -i movie.mp4 \
-vf "fps=1/10,scale=160:-1,tile=19x19" \
-frames:v 1 sprites.jpg
Result: one JPG, ~3040x1710 pixels at 16:9, ready to slice on the client.
Example 3: Animated GIF of a 10s clip
# 1) palette
ffmpeg -ss 10 -t 10 -i clip.mp4 \
-vf "fps=15,scale=480:-1:flags=lanczos,palettegen" -y palette.png
# 2) use palette
ffmpeg -ss 10 -t 10 -i clip.mp4 -i palette.png \
-lavfi "fps=15,scale=480:-1:flags=lanczos[x];[x][1:v]paletteuse=dither=bayer:bayer_scale=5" \
-y demo.gif
Result: ~2–3 MB GIF, no visible banding.
Example 4: Blender render → MP4
Blender wrote render_0001.png … render_0240.png at 24 fps:
ffmpeg -framerate 24 -i render_%04d.png \
-c:v libx264 -pix_fmt yuv420p -crf 18 render.mp4
Result: 10-second MP4, high quality, broadly compatible.
Troubleshooting
Error: Could not find codec parameters for stream 0 (Video: none): unknown codec
Cause: image pattern's zero-padding doesn't match the actual filenames. Solution: rename files, or change %04d to match the real padding, or switch to -pattern_type glob -i 'img_*.png'.
Error: Output file #0 does not contain any stream
Cause: -ss placed after -i with an offset past the end of the video, OR thumbnail/select filtered everything out. Solution: verify the timestamp with ffprobe -show_entries format=duration; for select filters, loosen the predicate or pair with -vsync vfr.
Error: GIF looks washed out / banded / has dithering noise halo
Cause: skipped the palette step, or used dither=none. Solution: run the two-pass palettegen + paletteuse=dither=bayer:bayer_scale=5 workflow in section 2g.
Error: Image sequence imports at wrong speed / too fast / too slow
Cause: you used -r (output rate) when you needed -framerate (input rate). ffmpeg defaults to 25 fps on the image2 demuxer if neither is set. Solution: put -framerate N BEFORE -i img_%04d.png.
Error: tile filter complains about missing frames / produces a partial grid
Cause: fps didn't produce enough frames to fill MxN. Solution: either lower the interval (more frames), reduce the grid (2x2), or accept a partial tile.
Error: JPG is huge / low-quality
Cause: missing -q:v. Solution: add -q:v 2 (best) through -q:v 7 (web). Remember: lower = better.
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: damionrashford
- Source: damionrashford/media-os
- License: MIT
- Homepage: https://damionrashford.github.io/media-os/
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.