Whole-frame PSNR is a terrible way to judge a compressed screen recording, and it took me an afternoon of contradictory numbers to work out why.
The setup is boring. I build front-end for an enterprise SaaS, and at the end of every iteration someone records a walkthrough of the admin console and sends it to the business side. 1920×1080, 30 fps, about a minute, and the file comes out around three hundred megabytes. It has to be compressed. The question that actually mattered to me was not how small it gets, but whether the SKU column is still readable afterwards, because that column is the reason anyone watches the video at all. Those are 11 to 13 pixel monospaced characters.
I could not use a real recording for this, so I drew one. Fictional warehouse brand, invented SKUs, invented amounts, invented transfer order numbers — none of it is real business data. The clip is a near-lossless screen capture: all-intra, QP 14, 40 Mbps, 62 seconds, 295.7 MB, with a synthesized AAC track. That premise matters more than anything else in this post, and I will come back to it. The compression ran in ImgIng ( https://imging.ai/ ), which does the whole thing in the browser, so an internal console recording never leaves the machine. After a compliance review a few years ago, anything that uploads is a conversation with legal, and I would rather not have that conversation over a demo video.
Three codecs, one variable
Quality preset pinned to visually-near-lossless, resolution pinned to source, frame rate pinned to source. The only thing I changed was the output format.
MP4 with H.264/AAC came out at 20.3 MB. WebM with VP9/Opus came out at 24.3 MB — 19.7% larger than H.264. WebM with AV1/Opus came out at 6.7 MB, another 67.0% below H.264. All three kept 1920×1080 at 30 fps, all 1860 frames, audio intact.
The VP9 result is the one worth sitting with. Its frame-averaged PSNR is actually 0.68 dB higher than H.264's. By that metric it won. It also cost two extra megabytes per ten, for a difference I cannot see. I am not going to write that VP9 is worse than H.264 — I have exactly one clip, and screen content is a specific kind of content. On this clip it was not worth it.
Where the frame average lies to you
Here is the part I did not expect. I dropped the longest edge from 1920 to 1280, which is the first thing anyone reaches for when a video is too big. The output was 25.73 MB — 26.6% larger than keeping the source resolution at 20.32 MB. The tool had allocated 3 200 kb/s to the 1280 preset against 2 502 kb/s for the native one. So that step cost quality and did not buy size.
And it did cost quality. Frame-averaged PSNR went from 46.11 to 31.68, which sounds bad but is still an abstract number. So I cropped the table region and measured it separately: PSNR there fell 14.28 dB, and a simple stroke-sharpness figure — mean horizontal neighbour difference in the grayscale crop — went from 4.13 down to 2.81. At 854 it went to 1.36, and at that point the SKUs are not characters anymore, they are texture.
Everything else left the text alone. Four quality presets, from 22.4 MB down to 15.8 MB: sharpness stayed at 4.13 the whole way. 30 fps to 24 fps, another 14.4% off: still 4.13. VP9 4.14, AV1 4.09 against a source of 4.15. One knob out of four damages small text, and it is the one people reach for first.
So the order I use now is inverted from the intuitive one: change the codec, then the frame rate, then walk down the quality presets, and touch resolution last if at all.
Two things I cannot claim
The frame rate step has a measurement blind spot I want to be explicit about. PSNR and sharpness did not move at all between 30 and 24 fps, which looks like free savings. But I sampled frames at fixed wall-clock timestamps and compared still images, and that method is structurally blind to dropped frames. 372 frames — a fifth of the clip — are gone, and what they carried was motion continuity. I measured nothing that would catch that.
The AV1 result also comes with a gate. That option only appears when a runtime capability check passes; on this machine the encoder config query for av01.0.08M.08 returned supported, so the option was live. What the interface does on a machine where it is not supported, I never saw — I could not find an engine that reports it as unsupported. Which is exactly why AV1 cannot be the default in a team workflow. We ship MP4/H.264, and AV1 is an extra artifact for whoever's machine can produce one.
On the decoding side I did check, because "smaller" is worthless if it will not open. Each of the three outputs went into a <video> element on a local page in three engines — Chromium 149, WebKit 26.5, Firefox 151 — reading duration and dimensions from loadedmetadata, then playing for 2.5 seconds to confirm currentTime actually advanced rather than trusting a canplay event. Nine out of nine, duration 62.04 s everywhere, 2.2 to 2.5 seconds of real advance, no media errors. That is decode support in three engines, not a statement about any real phone or any shipped browser build, and it says nothing about what a platform's own transcoder would do to the file after upload. I tested none of that.
And the premise I promised to come back to: my source was an all-intra near-lossless capture. Encode the same frames with ordinary inter-frame prediction and the source is 10 to 18 MB instead of 310, and the ratios in this post collapse. The percentages describe this clip. The ordering — codec first, resolution last — is the part I would actually carry over.
Top comments (0)