Take a mostly static screen recording, re-encode it with x264 at CRF 23, preset medium, and touch only -g. Here, writing a keyframe every 10 seconds instead of every 2 brought the output down by 75.6%, while a 30-second camera clip (Wikimedia Commons, CC BY-SA, re-encoded) went down by just 5.0%. The interval is the cheapest size knob on this kind of video, and the obvious thing it breaks is seeking, so that is what this post measures.
The input is synthetic: a mock lesson assembled from screenshots of a test page, with four slides, typing in an editor pane, a scrolled handout and a wandering pointer, 4 minutes at 1920×1080 and 30 fps, written out at a constant 12 Mbps (360.0 MB). The screen is still for roughly 78% of the running time, which is why keyframes end up carrying most of the bytes, and it is cleaner and easier to compress than a real capture. Setup: one Apple M4 Mac, ffmpeg with x264, no audio, one encode per setting.
Context for the absolute sizes: HandBrake's Fast 1080p30 preset produced 6.0 MB from the same input and default CRF 23 produced 5.2 MB. A browser-side reference point is the video compressor in ImgIng https://imging.ai/ , which runs on the local file in a desktop browser; its smart-recommend H.264 setting gave 20.2 MB with small text on par with HandBrake's. The sweep itself stays in ffmpeg so that one parameter changes at a time.
Two proxies for seek cost
Neither is the delay a viewer feels when dragging a scrubber. The first is average frames to decode: for each of the 7,200 frames, count the frames decoded from the nearest earlier keyframe up to it, in display order, then average. It ignores B-frame reordering, but it depends only on the GOP layout. The second is a timed accurate seek with ffmpeg -threads 1 -ss T, which jumps to the keyframe before T and decodes forward to T, run at 30 fixed random timestamps three times each, median taken. Single-threaded on purpose, so the timings only rank the files.
Frames to decode grow with half the GOP
| Keyframe interval | Recording size | vs 1 s | Avg frames to decode | Accurate seek, median |
|---|---|---|---|---|
1 s (-g 30) |
34,550,226 B | 100% | 15.5 | 51.9 ms |
| 2 s | 18,065,145 B | 52% | 30.5 | 62.2 ms |
| 5 s | 7,835,468 B | 23% | 75.5 | 98.0 ms |
| 10 s | 4,402,121 B | 13% | 150.5 | 163.4 ms |
| x264 default, 250 frames (8.3 s) | 5,222,157 B | 15% | 124.8 | 111.9 ms |
| 30 s | 2,167,967 B | 6.3% | 450.5 | 369.1 ms |
Unlimited (keyint=infinite) |
1,299,050 B | 3.8% | 3,600.5 | 2,623.5 ms (max 4,867.7) |
The frame count is half a GOP plus a half: 30 frames per GOP gives 15.5, a single keyframe for 7,200 frames gives 3,600.5. The size curve flattens quickly. Going from 1 s to 10 s removes 87% of the bytes, while 10 s to unlimited removes another 3.1 MB and multiplies the decode work by 24. Timed seeks grow more slowly than frame counts at the short end, 51.9 ms against 163.4 ms for ten times the frames; my assumption is a fixed cost for opening the file under every run, which was not separated out. Worth noting: the x264 default of 250 is counted in frames, so it is 8.3 s here but exactly 10 s on the 25 fps camera clip. Text quality did not move. SSIM on small English text at second 60 was 0.9686 at 1 s, 0.9689 at 10 s and 0.9677 unlimited, so on this sample the trade is purely bytes against seek work.
A seek near the end shows the worst case
The same accurate seek to second 200, timed once on the 2 s, 10 s and unlimited encodes:
/usr/bin/time -p ffmpeg -v error -threads 1 -ss 200 -i gop2s.mp4 -frames:v 1 -f null -
gop2s.mp4 real 0.04
gop10s.mp4 real 0.04
gopinf.mp4 real 4.07
At the 0.01 s granularity of time -p, 2 s and 10 s cannot be told apart. The unlimited file decodes from frame 0 to frame 6,000 and comes out about a hundred times slower. The table's 2.6 s median is milder; my reading is that the random timestamps include cheap early ones, while second 200 sits near the end, so this single run shows something close to the worst case.
What was measured and what is inferred
Measured, one encode per setting: the sizes, frames-to-decode averages, seek medians, unchanged text SSIM, and the camera clip's 5.0%. That clip spends 59.4% of its bytes on P-frames and 4.5% on keyframes, which fits how little the interval mattered for it. Inferred, not tested: why (my reading is that sensor noise leaves a residual in every P-frame); how a browser player actually scrubs, exact frame or nearest keyframe; and segmented delivery. With HLS or DASH each segment normally has to start on a keyframe, so I would expect segment duration to cap the interval, and a 30-second or unlimited GOP would not fit short segments. No segments were packaged in this test, so that stays an inference, as does anything about real captures with noise, Retina resolution or 60 fps.
Where I would set it
For recordings people scrub through I would stop at 10 s: 75.6% smaller than 2 s, about 150 frames per seek, 163 ms single-threaded against 62 ms. For rarely opened archives, 30 s (-g 900) is 58.5% smaller than the x264 default at about 450 frames per seek. Unlimited stays out of anything someone drags. Camera footage gets 2 to 5 s and its size comes from CRF, which saved 43.4% there at CRF 28 against 17.2% on the recording. To check your own file, encode it at 2 s and at the interval you plan, then time the seek above near the end of both. What I still cannot tell is how much of that 163 ms a viewer would notice in a real player.
Top comments (0)