video rungs stop upscaling, which is why the one video failed

`web-1080` was asked for whenever the source was at least 720 tall, so a 720p
video was upscaled to 1080p — the exact thing the image ladder is careful never
to do. The output was a 14.6 MB file that looks worse than the 7.8 MB one beside
it, and it cost 84 seconds of a single core on an idle box. Under a running sync
that overran av-club's 255 s idle timeout and homepage_video.mp4 failed
outright, which is the only reason the waste was noticed.

A rung is now made only when the source is at least that tall. `web-480` becomes
a floor rather than a rung, so a source smaller than 720 still gets something
instead of nothing or an enlargement. An unreadable height makes everything: a
probe that told us nothing is not evidence the video is small.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-29 11:32:34 +01:00
co-authored by Claude Opus 5
parent 6db5b1e85d
commit b9f543c2b6
+27 -7
View File
@@ -55,12 +55,25 @@ export const LADDER = [320, 640, 960, 1280, 1920, 2560] as const;
/** WebP quality. 82 is the usual knee: visually clean on photographs, well off the top of the curve. */
const WEBP_QUALITY = 82;
/** Video renditions, as av-club preset names paired with the source height each one needs to be worth making. */
/**
* Video renditions, tallest first. `height` is what the preset scales TO.
*
* A rung is made only when the source is at least that tall — the same no-upscaling rule the image
* ladder follows, and it was got wrong here first: `web-1080` was being asked for from a 720p source,
* which ffmpeg obligingly did. The result was a 14.6 MB file that looks worse than the 7.8 MB one
* beside it, and 84 seconds of a single core to make it. Under a running sync that overran av-club's
* idle timeout and the video failed outright, which is how the bug surfaced at all.
*
* `web-480` is the floor, not a rung: it exists so a source smaller than 720 still gets something,
* rather than nothing or an upscale.
*/
const VIDEO_RUNGS = [
{ file: '1080.mp4', preset: 'web-1080', minSourceHeight: 720 },
{ file: '720.mp4', preset: 'web-720', minSourceHeight: 0 },
{ file: '1080.mp4', preset: 'web-1080', height: 1080 },
{ file: '720.mp4', preset: 'web-720', height: 720 },
] as const;
const VIDEO_FLOOR = { file: '480.mp4', preset: 'web-480', height: 480 } as const;
const IMAGE_EXT = new Set(['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'avif', 'tif', 'tiff', 'heic', 'heif']);
const VIDEO_EXT = new Set(['mp4', 'mov', 'm4v', 'webm', 'mkv', 'avi', 'wmv', 'flv', 'mpg', 'mpeg', '3gp', 'm2ts', 'mts']);
@@ -330,10 +343,17 @@ async function buildVideo(
const variants: Variant[] = [];
for (const rung of VIDEO_RUNGS) {
// A 720p source re-encoded as "1080" is bigger and no better. The lowest rung has no floor, so
// every video gets at least one rendition however small it started.
if (height && height < rung.minSourceHeight) continue;
/*
Every rung the source is actually tall enough for, and the floor when it is tall enough for none.
An unreadable height (0) is treated as "make everything": a probe that told us nothing is not
evidence the video is small, and the encoder will decline to enlarge on its own long before we
should decide on its behalf.
*/
const wanted = height ? VIDEO_RUNGS.filter((rung) => height >= rung.height) : [...VIDEO_RUNGS];
const rungs = wanted.length ? wanted : [VIDEO_FLOOR];
for (const rung of rungs) {
const result = await post<{ path: string; bytes: number }>('/v1/ffmpeg', email, {
inputPath: sourcePath,
outputPath: join(staging, rung.file),