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. */ /** WebP quality. 82 is the usual knee: visually clean on photographs, well off the top of the curve. */
const WEBP_QUALITY = 82; 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 = [ const VIDEO_RUNGS = [
{ file: '1080.mp4', preset: 'web-1080', minSourceHeight: 720 }, { file: '1080.mp4', preset: 'web-1080', height: 1080 },
{ file: '720.mp4', preset: 'web-720', minSourceHeight: 0 }, { file: '720.mp4', preset: 'web-720', height: 720 },
] as const; ] 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 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']); 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[] = []; 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 rung the source is actually tall enough for, and the floor when it is tall enough for none.
// every video gets at least one rendition however small it started.
if (height && height < rung.minSourceHeight) continue; 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, { const result = await post<{ path: string; bytes: number }>('/v1/ffmpeg', email, {
inputPath: sourcePath, inputPath: sourcePath,
outputPath: join(staging, rung.file), outputPath: join(staging, rung.file),