| 1 | test("getConstantAspectScaleFilter picks one square-pixel ratio for the ladder", () => { |
| 2 | const filter = getConstantAspectScaleFilter({ width: 1920, height: 817 }, 600); |
| 3 | assert.equal(filter, "scale=1410:600"); |
| 4 | }); |
| 5 | |
| 6 | test("getAv1VideoArgs uses constant-aspect scaling for downscaled renditions", () => { |
| 7 | const args = getAv1VideoArgs( |
| 8 | { id: "al", preset: 2, mbitMax: 1.25, crf: 40, maxHeight: 600 }, |
| 9 | { width: 1920, height: 817 }, |
| 10 | ["-i", "/tmp/input.mp4"], |
| 11 | new Path("/tmp/out"), |
| 12 | ); |
| 13 | |
| 14 | assert.equal(args[args.indexOf("-vf") + 1], "scale=1410:600"); |
| 15 | assert.equal(args[args.indexOf("-y")], "-y"); |
| 16 | assert.equal(args.at(-1), "/tmp/out/tmp.av1.mp4"); |
| 17 | }); |
| 18 | |
| 19 | test("getAv1VideoArgs also snaps top-quality renditions onto the same exact ratio", () => { |
| 20 | const args = getAv1VideoArgs( |
| 21 | { id: "au", preset: 1, mbitMax: 30, crf: 28, depth: 10 }, |
| 22 | { width: 1920, height: 817 }, |
| 23 | ["-i", "/tmp/input.mp4"], |
| 24 | new Path("/tmp/out"), |
| 25 | ); |
| 26 | |
| 27 | assert.equal(args[args.indexOf("-vf") + 1], "scale=1880:800"); |
| 28 | }); |
| 29 | |
| 30 | test("getMpegDashArgs keeps all video streams in one adaptation set", () => { |
| 31 | const args = getMpegDashArgs( |
| 32 | [ |
| 33 | new Path("/tmp/av1-1.mp4"), |
| 34 | new Path("/tmp/av1-2.mp4"), |
| 35 | new Path("/tmp/av1-3.mp4"), |
| 36 | ], |
| 37 | [ |
| 38 | new Path("/tmp/audio-1.webm"), |
| 39 | new Path("/tmp/audio-2.webm"), |
| 40 | ], |
| 41 | new Path("/tmp/dash"), |
| 42 | ); |
| 43 | |
| 44 | const adaptationSets = args[args.indexOf("-adaptation_sets") + 1]; |
| 45 | assert.equal(adaptationSets, "id=0,streams=0,1,2 id=1,streams=3,4"); |
| 46 | }); |
| 47 | |
| 48 | test("all AV1 presets share one exact square-pixel ratio across many source sizes", () => { |
| 49 | const sources = [ |
| 50 | { width: 3840, height: 2160 }, |
| 51 | { width: 1920, height: 1080 }, |
| 52 | { width: 1920, height: 817 }, |
| 53 | { width: 1440, height: 1080 }, |
| 54 | { width: 1280, height: 720 }, |
| 55 | { width: 1280, height: 534 }, |
| 56 | { width: 1080, height: 1920 }, |
| 57 | { width: 1080, height: 1350 }, |
| 58 | { width: 1024, height: 768 }, |
| 59 | { width: 853, height: 480 }, |
| 60 | { width: 800, height: 800 }, |
| 61 | { width: 721, height: 405 }, |
| 62 | { width: 640, height: 360 }, |
| 63 | { width: 426, height: 240 }, |
| 64 | ]; |
| 65 | |
| 66 | for (const source of sources) { |
| 67 | let expectedRatio: number | null = null; |
| 68 | for (const preset of videoFormats) { |
| 69 | const args = getAv1VideoArgs( |
| 70 | preset, |
| 71 | source, |
| 72 | ["-i", "/tmp/input.mp4"], |
| 73 | new Path("/tmp/out"), |
| 74 | ); |
| 75 | const vfIndex = args.indexOf("-vf"); |
| 76 | const filter = vfIndex === -1 ? null : args[vfIndex + 1]; |
| 77 | const dims = filter == null |
| 78 | ? source |
| 79 | : parseScaleFilter(assertString(filter)); |
| 80 | |
| 81 | assert.ok(dims.width > 0); |
| 82 | assert.ok(dims.height > 0); |
| 83 | assert.equal(dims.width % 2, 0); |
| 84 | assert.equal(dims.height % 2, 0); |
| 85 | assert.ok(dims.width <= source.width); |
| 86 | assert.ok(dims.height <= source.height); |
| 87 | if (preset.maxHeight != null) { |
| 88 | assert.ok( |
| 89 | dims.height <= preset.maxHeight, |
| 90 | `${source.width}x${source.height} exceeded maxHeight ${preset.maxHeight} with ${dims.width}x${dims.height}`, |
| 91 | ); |
| 92 | } |
| 93 | |
| 94 | const ratio = dims.width / dims.height; |
| 95 | if (expectedRatio == null) { |
| 96 | expectedRatio = ratio; |
| 97 | } else { |
| 98 | assert.equal( |
| 99 | ratio, |
| 100 | expectedRatio, |
| 101 | `${source.width}x${source.height} produced mismatched ladder ratio ${dims.width}x${dims.height}`, |
| 102 | ); |
| 103 | } |
| 104 | } |
| 105 | } |
| 106 | }); |
| 107 | |
| 108 | test("canonical ratio sweep matches the best feasible exact ratio", () => { |
| 109 | for (let width = 320; width <= 4096; width += 113) { |
| 110 | for (let height = 240; height <= 3072; height += 97) { |
| 111 | const source = { width, height }; |
| 112 | const actualDimsByPreset: Array<{ width: number; height: number }> = []; |
| 113 | let referenceDims: { width: number; height: number } | null = null; |
| 114 | |
| 115 | for (const preset of videoFormats) { |
| 116 | const args = getAv1VideoArgs( |
| 117 | preset, |
| 118 | source, |
| 119 | ["-i", "/tmp/input.mp4"], |
| 120 | new Path("/tmp/out"), |
| 121 | ); |
| 122 | const vfIndex = args.indexOf("-vf"); |
| 123 | const filter = vfIndex === -1 ? null : args[vfIndex + 1]; |
| 124 | const dims = filter == null |
| 125 | ? source |
| 126 | : parseScaleFilter(assertString(filter)); |
| 127 | |
| 128 | actualDimsByPreset.push(dims); |
| 129 | |
| 130 | if (referenceDims == null) { |
| 131 | referenceDims = dims; |
| 132 | } else { |
| 133 | assert.equal( |
| 134 | dims.width * referenceDims.height, |
| 135 | referenceDims.width * dims.height, |
| 136 | `${width}x${height} produced inconsistent exact ratios within the ladder`, |
| 137 | ); |
| 138 | } |
| 139 | } |
| 140 | |
| 141 | const actualRatio = reduceRatio(referenceDims!); |
| 142 | const actualScore = getCandidateScore(source, actualRatio); |
| 143 | assert.ok(actualScore, `${width}x${height} produced an infeasible ratio`); |
| 144 | const feasibleCandidates = getFeasibleCandidates(source); |
| 145 | const candidatesWithinThreshold = feasibleCandidates.filter((candidate) => |
| 146 | candidate.error <= EXACT_ERROR_THRESHOLD |
| 147 | ); |
| 148 | |
| 149 | if (candidatesWithinThreshold.length > 0) { |
| 150 | assert.ok( |
| 151 | actualScore.error <= EXACT_ERROR_THRESHOLD, |
| 152 | `${width}x${height} should have found a ratio within the allowed drift threshold`, |
| 153 | ); |
| 154 | } else { |
| 155 | const bestFallbackError = Math.min( |
| 156 | ...feasibleCandidates.map((candidate) => candidate.error), |
| 157 | ); |
| 158 | assert.equal( |
| 159 | actualScore.error, |
| 160 | bestFallbackError, |
| 161 | `${width}x${height} did not pick a minimum-drift fallback ratio`, |
| 162 | ); |
| 163 | } |
| 164 | |
| 165 | for (let i = 0; i < videoFormats.length; i++) { |
| 166 | const expectedDims = getExpectedDimensionsForPreset( |
| 167 | source, |
| 168 | actualRatio, |
| 169 | videoFormats[i].maxHeight, |
| 170 | ); |
| 171 | assert.deepEqual( |
| 172 | actualDimsByPreset[i], |
| 173 | expectedDims, |
| 174 | `${width}x${height} produced unexpected dimensions for preset ${videoFormats[i].id}`, |
| 175 | ); |
| 176 | } |
| 177 | } |
| 178 | } |
| 179 | }); |
| 180 | |
| 181 | test("dense sweep never produces mismatched ladder aspect ratios", () => { |
| 182 | for (let width = 2; width <= 2048; width++) { |
| 183 | for (let height = 2; height <= 2048; height++) { |
| 184 | const source = { width, height }; |
| 185 | let referenceDims: { width: number; height: number } | null = null; |
| 186 | |
| 187 | for (const preset of videoFormats) { |
| 188 | const args = getAv1VideoArgs( |
| 189 | preset, |
| 190 | source, |
| 191 | ["-i", "/tmp/input.mp4"], |
| 192 | new Path("/tmp/out"), |
| 193 | ); |
| 194 | const vfIndex = args.indexOf("-vf"); |
| 195 | const filter = vfIndex === -1 ? null : args[vfIndex + 1]; |
| 196 | const dims = filter == null |
| 197 | ? source |
| 198 | : parseScaleFilter(assertString(filter)); |
| 199 | |
| 200 | assert.ok(dims.width >= 2, `${width}x${height} produced width < 2`); |
| 201 | assert.ok(dims.height >= 2, `${width}x${height} produced height < 2`); |
| 202 | if (filter != null) { |
| 203 | assert.equal(dims.width % 2, 0, `${width}x${height} produced odd scaled width`); |
| 204 | assert.equal(dims.height % 2, 0, `${width}x${height} produced odd scaled height`); |
| 205 | } |
| 206 | assert.ok(dims.width <= source.width, `${width}x${height} exceeded source width`); |
| 207 | assert.ok(dims.height <= source.height, `${width}x${height} exceeded source height`); |
| 208 | if (preset.maxHeight != null) { |
| 209 | assert.ok( |
| 210 | dims.height <= preset.maxHeight, |
| 211 | `${width}x${height} exceeded maxHeight ${preset.maxHeight} with ${dims.width}x${dims.height}`, |
| 212 | ); |
| 213 | } |
| 214 | |
| 215 | if (referenceDims == null) { |
| 216 | referenceDims = dims; |
| 217 | } else { |
| 218 | assert.equal( |
| 219 | dims.width * referenceDims.height, |
| 220 | referenceDims.width * dims.height, |
| 221 | `${width}x${height} produced mismatched exact ratios: ${referenceDims.width}x${referenceDims.height} vs ${dims.width}x${dims.height}`, |
| 222 | ); |
| 223 | } |
| 224 | } |
| 225 | } |
| 226 | } |
| 227 | }); |
| 228 | |
| 229 | import { Path } from "#sitegen/path"; |
| 230 | import assert from "node:assert"; |
| 231 | import { test } from "vitest"; |
| 232 | import { getAv1VideoArgs, getConstantAspectScaleFilter, getMpegDashArgs, videoFormats } from "./transcode-rules.ts"; |
| 233 | |
| 234 | function assertString(value: string | undefined | null): string { |
| 235 | assert.equal(typeof value, "string"); |
| 236 | return value; |
| 237 | } |
| 238 | |
| 239 | function parseScaleFilter(filter: string) { |
| 240 | const match = /^scale=(\d+):(\d+)$/.exec(filter); |
| 241 | assert.ok(match, `unexpected filter ${filter}`); |
| 242 | return { |
| 243 | width: Number(match[1]), |
| 244 | height: Number(match[2]), |
| 245 | }; |
| 246 | } |
| 247 | |
| 248 | function getExpectedDimensionsForPreset( |
| 249 | source: { width: number; height: number }, |
| 250 | ratio: { width: number; height: number }, |
| 251 | maxHeight?: number, |
| 252 | ) { |
| 253 | const heightCap = Math.min(maxHeight ?? source.height, source.height); |
| 254 | const maxScale = Math.floor(Math.min( |
| 255 | source.width / ratio.width, |
| 256 | heightCap / ratio.height, |
| 257 | )); |
| 258 | const scale = maxScale % 2 === 0 ? maxScale : maxScale - 1; |
| 259 | const dims = { |
| 260 | width: ratio.width * scale, |
| 261 | height: ratio.height * scale, |
| 262 | }; |
| 263 | |
| 264 | if (dims.width === source.width && dims.height === source.height) return source; |
| 265 | return dims; |
| 266 | } |
| 267 | |
| 268 | const EXACT_ERROR_THRESHOLD = 0.0005; |
| 269 | |
| 270 | function getFeasibleCandidates(source: { width: number; height: number }) { |
| 271 | const caps = [ |
| 272 | source.height, |
| 273 | ...videoFormats.flatMap((preset) => preset.maxHeight != null ? [Math.min(preset.maxHeight, source.height)] : []), |
| 274 | ]; |
| 275 | const sourceRatio = source.width / source.height; |
| 276 | const maxDenominator = Math.max(1, Math.floor(Math.min(...caps) / 2)); |
| 277 | const seen = new Set<string>(); |
| 278 | const candidates: Array<{ |
| 279 | width: number; |
| 280 | height: number; |
| 281 | error: number; |
| 282 | retainedArea: number; |
| 283 | }> = []; |
| 284 | |
| 285 | for (let denominator = 1; denominator <= maxDenominator; denominator++) { |
| 286 | const numerator = Math.max(1, Math.round(sourceRatio * denominator)); |
| 287 | const divisor = greatestCommonDivisor(numerator, denominator); |
| 288 | const candidate = { |
| 289 | width: numerator / divisor, |
| 290 | height: denominator / divisor, |
| 291 | }; |
| 292 | const key = `${candidate.width}:${candidate.height}`; |
| 293 | if (seen.has(key)) continue; |
| 294 | seen.add(key); |
| 295 | |
| 296 | const dims = videoFormats.map((preset) => getExpectedDimensionsForPreset(source, candidate, preset.maxHeight)); |
| 297 | if (dims.some((value) => value.width <= 0 || value.height <= 0)) continue; |
| 298 | |
| 299 | const retainedArea = dims |
| 300 | .map((value) => value.width * value.height) |
| 301 | .reduce((sum, value) => sum + value, 0); |
| 302 | const error = Math.abs((candidate.width / candidate.height) - sourceRatio) |
| 303 | / sourceRatio; |
| 304 | candidates.push({ ...candidate, error, retainedArea }); |
| 305 | } |
| 306 | |
| 307 | return candidates; |
| 308 | } |
| 309 | |
| 310 | function greatestCommonDivisor(a: number, b: number): number { |
| 311 | let x = Math.abs(a); |
| 312 | let y = Math.abs(b); |
| 313 | while (y !== 0) { |
| 314 | [x, y] = [y, x % y]; |
| 315 | } |
| 316 | return x || 1; |
| 317 | } |
| 318 | |
| 319 | function reduceRatio(dimensions: { width: number; height: number }) { |
| 320 | const divisor = greatestCommonDivisor(dimensions.width, dimensions.height); |
| 321 | return { |
| 322 | width: dimensions.width / divisor, |
| 323 | height: dimensions.height / divisor, |
| 324 | }; |
| 325 | } |
| 326 | |
| 327 | function getCandidateScore( |
| 328 | source: { width: number; height: number }, |
| 329 | ratio: { width: number; height: number }, |
| 330 | ) { |
| 331 | const dims = videoFormats.map((preset) => getExpectedDimensionsForPreset(source, ratio, preset.maxHeight)); |
| 332 | if (dims.some((value) => value.width <= 0 || value.height <= 0)) return null; |
| 333 | const sourceRatio = source.width / source.height; |
| 334 | return { |
| 335 | ...ratio, |
| 336 | error: Math.abs((ratio.width / ratio.height) - sourceRatio) / sourceRatio, |
| 337 | retainedArea: dims |
| 338 | .map((value) => value.width * value.height) |
| 339 | .reduce((sum, value) => sum + value, 0), |
| 340 | }; |
| 341 | } |