import { MEDIA_PLATFORM_PROFILES } from "@rakazo/contracts"; import { describe, expect, it } from "vitest"; import type { ExportMeasurement } from "./platform-export.js"; import { BITRATE_TOLERANCE, checkExport, DURATION_TOLERANCE_MS, describeSkipped, durationIssues, LOUDNESS_TOLERANCE_LU, MIN_VIDEO_KBPS, needsLoudnessFix, platformTarget, sizeBudgetKbps, sizeFixKbps, TRUE_PEAK_TOLERANCE_DB, } from "./platform-export.js"; const tiktok = MEDIA_PLATFORM_PROFILES.tiktok; /** A clean 30 s TikTok render; tests override what they examine. */ function measured(overrides: Partial = {}): ExportMeasurement { return { width: 1080, height: 1920, fps: 30, codec: "h264", durationMs: 30_000, sizeBytes: 20_000_000, videoKbps: 5000, loudness: { integratedLufs: -14.2, truePeakDb: -1.3, rangeLu: 4, thresholdLufs: -24.5, offsetLu: 0.1, }, ...overrides, }; } function loudness(integratedLufs: number, truePeakDb = -2) { return { integratedLufs, truePeakDb, rangeLu: 4, thresholdLufs: -30, offsetLu: 0 }; } describe("platform targets", () => { it("renders to the profile and caps the bitrate below its size limit for long videos", () => { expect(platformTarget(tiktok, "blur", 30_000)).toEqual({ width: 1080, height: 1920, fit: "blur", maxFps: 30, maxBitrateKbps: 8000, integratedLufs: -14, truePeakDb: -1, }); // 287 MB over 10 minutes leaves about 3.6 Mbps of video after headroom and audio. const tenMinutes = platformTarget(tiktok, "crop", 600_000).maxBitrateKbps; expect(tenMinutes).toBe(sizeBudgetKbps(tiktok, 600_000)); expect(tenMinutes).toBeGreaterThan(3000); expect(tenMinutes).toBeLessThan(3700); expect(tenMinutes * 600 + 160 * 600).toBeLessThan(287 * 8000); expect(sizeBudgetKbps(tiktok, 1e9)).toBe(MIN_VIDEO_KBPS); }); }); describe("duration limits", () => { it("refuses videos past the limits, with a small tolerance for container rounding", () => { expect(durationIssues(tiktok, 600_000 + DURATION_TOLERANCE_MS)).toEqual([]); expect(durationIssues(tiktok, 720_000)).toEqual([ { code: "too_long", maxMs: 600_000, actualMs: 720_000 }, ]); expect(durationIssues(tiktok, 3000 - DURATION_TOLERANCE_MS)).toEqual([]); expect(durationIssues(tiktok, 2000)).toEqual([ { code: "too_short", minMs: 3000, actualMs: 2000 }, ]); }); it("words a skipped platform for the bot with what to do about it", () => { expect(describeSkipped(tiktok, durationIssues(tiktok, 720_000))).toBe( "Not exported for TikTok: 720 s is longer than its 600 s limit; trim the video first.", ); expect(describeSkipped(tiktok, durationIssues(tiktok, 1500))).toBe( "Not exported for TikTok: 1.5 s is shorter than its 3 s minimum; use a longer video.", ); }); }); describe("platform check", () => { it("passes a render within every tolerance", () => { expect( checkExport( tiktok, measured({ videoKbps: 8000 * (1 + BITRATE_TOLERANCE), fps: 30.2, loudness: loudness(-14 - LOUDNESS_TOLERANCE_LU, -1 + TRUE_PEAK_TOLERANCE_DB), }), ), ).toEqual({ status: "pass", issues: [] }); }); it("warns about what the platform would degrade", () => { expect( checkExport( tiktok, measured({ width: 720, height: 1280, fps: 60, videoKbps: 9500, loudness: loudness(-17.26, -0.2), }), ), ).toEqual({ status: "warn", issues: [ { code: "resolution_mismatch", width: 1080, height: 1920, actualWidth: 720, actualHeight: 1280, }, { code: "fps_mismatch", maxFps: 30, actualFps: 60 }, { code: "bitrate_high", maxKbps: 8000, actualKbps: 9500 }, { code: "loudness_off", targetLufs: -14, actualLufs: -17.3 }, { code: "true_peak_high", maxDb: -1, actualDb: -0.2 }, ], }); }); it("fails what the platform would reject, most severe first", () => { const check = checkExport( tiktok, measured({ codec: "hevc", sizeBytes: 300_456_789, loudness: loudness(-20) }), ); expect(check.status).toBe("fail"); expect(check.issues).toEqual([ { code: "file_too_large", maxMb: 287, actualMb: 300.5 }, { code: "codec_mismatch" }, { code: "loudness_off", targetLufs: -14, actualLufs: -20 }, ]); expect(checkExport(tiktok, measured({ durationMs: 601_000 })).issues[0]).toEqual({ code: "too_long", maxMs: 600_000, actualMs: 601_000, }); }); it("does not judge the loudness of silent or missing audio, or an unknown frame rate", () => { expect(checkExport(tiktok, measured({ loudness: null, fps: 0 })).status).toBe("pass"); expect( checkExport(tiktok, measured({ loudness: loudness(Number.NEGATIVE_INFINITY) })).status, ).toBe("pass"); expect(checkExport(tiktok, measured({ loudness: loudness(-65) })).status).toBe("pass"); }); }); describe("automatic fixes", () => { it("re-encodes an oversized file once at a bitrate scaled from what it averaged", () => { expect(sizeFixKbps(tiktok, measured(), 8000)).toBeNull(); // 330 MB at 4400 kbps average: scale by 287/330 and keep 10% headroom. const lower = sizeFixKbps(tiktok, measured({ sizeBytes: 330_000_000, videoKbps: 4400 }), 8000); expect(lower).toBe(Math.floor(4400 * (287 / 330) * 0.9)); // Without a stream bitrate, the file size and length give the average. expect( sizeFixKbps( tiktok, measured({ sizeBytes: 330_000_000, videoKbps: undefined, durationMs: 600_000 }), 8000, ), ).toBe(Math.floor((4400 - 160) * (287 / 330) * 0.9)); // Nothing lower is left to try. expect( sizeFixKbps(tiktok, measured({ sizeBytes: 330_000_000, videoKbps: 200 }), MIN_VIDEO_KBPS), ).toBeNull(); }); it("re-normalizes audible audio that missed the loudness target by more than the tolerance", () => { expect(needsLoudnessFix(tiktok, measured())).toBe(false); expect(needsLoudnessFix(tiktok, measured({ loudness: loudness(-16) }))).toBe(false); expect(needsLoudnessFix(tiktok, measured({ loudness: loudness(-16.5) }))).toBe(true); expect(needsLoudnessFix(tiktok, measured({ loudness: loudness(-10) }))).toBe(true); expect(needsLoudnessFix(tiktok, measured({ loudness: null }))).toBe(false); expect(needsLoudnessFix(tiktok, measured({ loudness: loudness(-70) }))).toBe(false); }); });