diff --git a/api/derive.ts b/api/derive.ts index f5fd848..4142ff9 100644 --- a/api/derive.ts +++ b/api/derive.ts @@ -83,15 +83,34 @@ const PLACEHOLDER_QUALITY = 50; * 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, + * `web-480` is BOTH a rung and the floor: a phone is served it whatever the source, and a source + * smaller than 720 still gets something, * rather than nothing or an upscale. */ const VIDEO_RUNGS = [ - { file: '1080.mp4', preset: 'web-1080', height: 1080 }, - { file: '720.mp4', preset: 'web-720', height: 720 }, + { file: '1080.mp4', preset: 'web-1080', height: 1080, options: undefined }, + { file: '720.mp4', preset: 'web-720', height: 720, options: undefined }, + /* + 480 is a real rung now, not only a floor. + + It was reachable únicamente when the SOURCE was smaller than 720, which is backwards: the rung a + phone wants is the one a 720p master never produced. The hero here is 1280x720 and the only file + that existed for it was 7.8 MB of 720p, served to everything. + + `b:a` overrides the preset's 96k. The audio on a hero loop is ambience — the sound of a tufting + gun — and 64k AAC carries that. It is the same argument as crf 26 for the picture: the rung exists + for a phone on a mobile connection, and every kilobit is one it is paying for. + */ + { file: '480.mp4', preset: 'web-480', height: 480, options: { 'b:a': '64k' } }, ] as const; -const VIDEO_FLOOR = { file: '480.mp4', preset: 'web-480', height: 480 } as const; +/** + * When the source is smaller than every rung. + * + * A 360p upload still needs one servable file. Kept separate from the ladder because it is a fallback + * rather than a choice — it is what happens when the filter above matches nothing. + */ +const VIDEO_FLOOR = { file: '480.mp4', preset: 'web-480', height: 480, options: { 'b:a': '64k' } } 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']); @@ -469,6 +488,9 @@ async function buildVideo( inputPath: sourcePath, outputPath: join(staging, rung.file), preset: rung.preset, + // The preset supplies the options and anything given here overrides it — av-club's contract, + // see sidecar/ffmpeg/run.ts. Used only to lighten the audio on the 480 rung. + ...(rung.options ? { options: rung.options } : {}), }); variants.push({ file: rung.file, width: null, height: null, bytes: result.bytes }); }