Vertical video: still-image Ken Burns and 9:16 blur-fit

ffmpeg guide · vertical video · shortform

Two ways to end up with a clean 1080×1920 clip: animate a still image with a slow push-in, or fit an already-landscape video into a vertical frame without black bars. Both commands below are lifted directly from the toolkit scripts that ship with the SOP Pack.

1. Still image → Ken Burns push-in

A single still, upscaled first so zoompan has real pixels to zoom into, then cropped to a 9:16 working size and pushed in slowly over the clip's duration.


ffmpeg -hide_banner -y -loop 1 -i hero.png -t 5 \
  -vf "scale=1620:2880:force_original_aspect_ratio=increase,crop=1620:2880,zoompan=z='min(zoom+0.0012,1.15)':d=150:x='iw/2-(iw/zoom/2)':y='ih/2-(ih/zoom/2)':s=1080x1920:fps=30,setsar=1" \
  -c:v libx264 -pix_fmt yuv420p -r 30 hero_5s.mp4

Why d is seconds × fps, not 1, here

zoompan's d is output frames per input frame, not a total frame count. The common shortcut — force the source to the target fps and set d=1, letting the zoom ramp ride the running frame index — only works when the input is already a multi-frame video at that fps. A still loaded with -loop 1 is a single input frame with no running index to drive the ramp, so d=1 just re-emits that one frame forever with the zoom frozen. Set d = seconds * fps (5s × 30fps = 150 in the command above) so zoompan advances its own internal counter across the output instead. This is the exact pitfall the SOP Pack's worked example (§9.5) walks through with the same numbers.

-pix_fmt yuv420p keeps the output playable everywhere (QuickTime, most phones, every platform upload path) — without it, some libx264 outputs default to a pixel format that only decodes correctly in ffmpeg-based players. -r 30 pins the frame rate so the zoompan output and the container agree.

2. Landscape footage → 9:16 with a blurred fill

When you don't want black bars around a 16:9 source, split the stream into a blurred, cropped background and a scaled foreground, then overlay the foreground centered on top.


ffmpeg -hide_banner -y -i landscape.mp4 \
  -vf "split[a][b];[a]scale=1080:1920:force_original_aspect_ratio=increase,crop=1080:1920,boxblur=30[bg];[b]scale=1080:-2[fg];[bg][fg]overlay=(W-w)/2:(H-h)/2,setsar=1" \
  -c:v libx264 -pix_fmt yuv420p -c:a copy vertical.mp4

split duplicates the input stream into two labeled copies so the same source can be processed two different ways in one filter graph. The [a] branch is scaled up until it fully covers 1080×1920 (force_original_aspect_ratio=increase), cropped to exact size, then blurred hard — that's the fill. The [b] branch is scaled to fit the frame width at full quality (scale=1080:-2 keeps the aspect ratio and rounds height to an even number, which libx264 requires) and overlaid centered. -c:a copy leaves the audio untouched since this pass only touches video.

CRF and encoding notes

Neither command above sets -crf explicitly, so libx264 uses its default (23) — fine for drafts. For a delivery-quality export, add -preset veryfast -crf 18 (the same values the toolkit's frame-accurate cut script uses for its final trim) to tighten quality at the cost of a larger file and slower encode.