// fallow-ignore-file code-duplication import { EventEmitter } from "events"; import { spawnSync } from "child_process"; import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "fs"; import { tmpdir } from "os"; import { basename, resolve } from "path"; import { afterEach, describe, expect, it, vi } from "vitest"; import { extractMediaMetadata, extractPngMetadataFromBuffer, parseFrameRate, pixelFormatHasAlpha, } from "./ffprobe.js"; function crc32(buf: Buffer): number { let crc = 0xffffffff; for (let i = 0; i < buf.length; i++) { crc ^= buf[i] ?? 0; for (let bit = 0; bit < 8; bit++) { const mask = -(crc & 1); crc = (crc >>> 1) ^ (0xedb88320 & mask); } } return (crc ^ 0xffffffff) >>> 0; } function pngChunk(type: string, data: number[]): Buffer { const chunkData = Buffer.from(data); const header = Buffer.alloc(8); header.writeUInt32BE(chunkData.length, 0); header.write(type, 4, 4, "ascii"); const crc = Buffer.alloc(4); crc.writeUInt32BE(crc32(Buffer.concat([Buffer.from(type, "ascii"), chunkData])), 0); return Buffer.concat([header, chunkData, crc]); } function buildPngWithChunks(chunks: Buffer[]): Buffer { return Buffer.concat([Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]), ...chunks]); } function buildMinimalPng(options?: { cIcpAfterIdat?: boolean; invalidCrc?: boolean; longCicp?: boolean; }) { const ihdr = pngChunk("IHDR", [0, 0, 0, 1, 0, 0, 0, 1, 16, 2, 0, 0, 0]); const cicpData = options?.longCicp ? [9, 16, 0, 1, 255] : [9, 16, 0, 1]; let cicp = pngChunk("cICP", cicpData); if (options?.invalidCrc) { cicp = Buffer.from(cicp); cicp[cicp.length - 1] ^= 0xff; } const idat = pngChunk( "IDAT", [0x78, 0x9c, 0x63, 0x60, 0x60, 0x60, 0x00, 0x00, 0x00, 0x04, 0x00, 0x01], ); const iend = pngChunk("IEND", []); return options?.cIcpAfterIdat ? buildPngWithChunks([ihdr, idat, cicp, iend]) : buildPngWithChunks([ihdr, cicp, idat, iend]); } describe("extractMediaMetadata", () => { it("reads HDR PNG cICP metadata when ffprobe color fields are absent", async () => { const fixturePath = resolve( __dirname, "../../../producer/tests/hdr-regression/src/hdr-photo-pq.png", ); const metadata = await extractMediaMetadata(fixturePath); expect(metadata.colorSpace).toEqual({ colorPrimaries: "bt2020", colorTransfer: "smpte2084", colorSpace: "gbr", }); }); }); describe("extractPngMetadataFromBuffer", () => { it("accepts a valid cICP chunk before IDAT", () => { const metadata = extractPngMetadataFromBuffer(buildMinimalPng()); expect(metadata?.colorSpace).toEqual({ colorPrimaries: "bt2020", colorTransfer: "smpte2084", colorSpace: "gbr", }); }); it("rejects cICP chunks after IDAT", () => { const metadata = extractPngMetadataFromBuffer(buildMinimalPng({ cIcpAfterIdat: true })); expect(metadata).toEqual({ width: 1, height: 1, colorSpace: null, }); }); it("rejects cICP chunks with invalid CRC", () => { expect(extractPngMetadataFromBuffer(buildMinimalPng({ invalidCrc: true }))).toBeNull(); }); it("rejects cICP chunks whose payload is not exactly four bytes", () => { const metadata = extractPngMetadataFromBuffer(buildMinimalPng({ longCicp: true })); expect(metadata).toEqual({ width: 1, height: 1, colorSpace: null, }); }); it("continues to parse the checked-in HDR PNG fixture", () => { const fixture = readFileSync( resolve(__dirname, "../../../producer/tests/hdr-regression/src/hdr-photo-pq.png"), ); expect(extractPngMetadataFromBuffer(fixture)?.colorSpace?.colorTransfer).toBe("smpte2084"); }); it("keeps metadata fallback independent from full PNG integrity validation", () => { const ihdr = pngChunk("IHDR", [0, 0, 0, 1, 0, 0, 0, 1, 16, 2, 0, 0, 0]); expect(extractPngMetadataFromBuffer(buildPngWithChunks([ihdr]))).toMatchObject({ width: 1, height: 1, }); }); }); describe("probeMediaProfile", () => { afterEach(() => { vi.resetModules(); vi.doUnmock("child_process"); }); it("classifies still, moving, audio-only, and mixed streams from probe data", async () => { const { spawn } = createSpawnSpy([ { kind: "exit", code: 0, stdout: JSON.stringify({ streams: [{ codec_type: "video", codec_name: "png" }], format: { format_name: "png_pipe" }, }), }, { kind: "exit", code: 0, stdout: JSON.stringify({ streams: [{ codec_type: "video", codec_name: "h264" }], format: { format_name: "mov,mp4,m4a,3gp,3g2,mj2" }, }), }, { kind: "exit", code: 0, stdout: JSON.stringify({ streams: [{ codec_type: "audio", codec_name: "mp3" }], format: { format_name: "mp3" }, }), }, { kind: "exit", code: 0, stdout: JSON.stringify({ streams: [{ codec_type: "video" }, { codec_type: "audio" }], format: { format_name: "matroska,webm" }, }), }, ]); vi.resetModules(); vi.doMock("child_process", () => ({ spawn })); const { probeMediaProfile } = await import("./ffprobe.js"); const validPngPath = resolve( __dirname, "../../../producer/tests/hdr-regression/src/hdr-photo-pq.png", ); await expect(probeMediaProfile(validPngPath)).resolves.toEqual({ hasVideoStream: true, hasAudioStream: false, visualKind: "still", }); await expect(probeMediaProfile("/tmp/extensionless-video")).resolves.toEqual({ hasVideoStream: true, hasAudioStream: false, visualKind: "moving", }); await expect(probeMediaProfile("/tmp/extensionless-audio")).resolves.toEqual({ hasVideoStream: false, hasAudioStream: true, visualKind: "none", }); await expect(probeMediaProfile("/tmp/mixed-av")).resolves.toEqual({ hasVideoStream: true, hasAudioStream: true, visualKind: "moving", }); }); it("does not treat attached cover art in an audio container as an image asset", async () => { const { spawn } = createSpawnSpy([ { kind: "exit", code: 0, stdout: JSON.stringify({ streams: [ { codec_type: "video", disposition: { attached_pic: 1 } }, { codec_type: "audio" }, ], format: { format_name: "mp3" }, }), }, ]); vi.resetModules(); vi.doMock("child_process", () => ({ spawn })); const { probeMediaProfile } = await import("./ffprobe.js"); await expect(probeMediaProfile("/tmp/audio-with-cover")).resolves.toMatchObject({ hasAudioStream: true, visualKind: "none", }); }); it("deduplicates probes within one cancellation scope without sharing across scopes", async () => { const fixtureDir = mkdtempSync(resolve(tmpdir(), "hf-media-probe-cache-")); const fixturePath = resolve(fixtureDir, "asset"); writeFileSync(fixturePath, "cache identity only"); const outcome = { kind: "exit" as const, code: 0, stdout: JSON.stringify({ streams: [{ codec_type: "video", codec_name: "h264" }], format: { format_name: "mov,mp4,m4a,3gp,3g2,mj2" }, }), }; const { spawn, calls } = createSpawnSpy([outcome, outcome]); vi.resetModules(); vi.doMock("child_process", () => ({ spawn })); const { probeMediaProfile } = await import("./ffprobe.js"); const firstSignal = new AbortController().signal; const secondSignal = new AbortController().signal; try { await Promise.all([ probeMediaProfile(fixturePath, { signal: firstSignal }), probeMediaProfile(fixturePath, { signal: firstSignal }), ]); expect(calls).toHaveLength(1); await probeMediaProfile(fixturePath, { signal: secondSignal }); expect(calls).toHaveLength(2); } finally { rmSync(fixtureDir, { recursive: true, force: true }); } }); it("bounds the process-scoped probe cache", async () => { const fixtureDir = mkdtempSync(resolve(tmpdir(), "hf-media-probe-lru-")); const fixturePaths = Array.from({ length: 129 }, (_, index) => resolve(fixtureDir, `asset-${index}`), ); for (const fixturePath of fixturePaths) writeFileSync(fixturePath, "probe identity"); const outcome = { kind: "exit" as const, code: 0, stdout: JSON.stringify({ streams: [{ codec_type: "audio", codec_name: "aac" }], format: { format_name: "aac" }, }), }; const { spawn, calls } = createSpawnSpy([outcome]); vi.resetModules(); vi.doMock("child_process", () => ({ spawn })); const { probeMediaProfile } = await import("./ffprobe.js"); try { for (const fixturePath of fixturePaths) await probeMediaProfile(fixturePath); await probeMediaProfile(fixturePaths[0]!); expect(calls).toHaveLength(130); } finally { rmSync(fixtureDir, { recursive: true, force: true }); } }); it("classifies extensionless AVIF from its ISO-BMFF brand instead of the generic mov demuxer", async () => { const fixtureDir = mkdtempSync(resolve(tmpdir(), "hf-avif-profile-")); const fixturePath = resolve(fixtureDir, "asset"); const ftyp = Buffer.alloc(24); ftyp.writeUInt32BE(24, 0); ftyp.write("ftyp", 4, 4, "ascii"); ftyp.write("avif", 8, 4, "ascii"); ftyp.writeUInt32BE(0, 12); ftyp.write("mif1", 16, 4, "ascii"); ftyp.write("avif", 20, 4, "ascii"); writeFileSync(fixturePath, ftyp); const { spawn } = createSpawnSpy([ { kind: "exit", code: 0, stdout: JSON.stringify({ streams: [{ codec_type: "video", codec_name: "av1" }], format: { format_name: "mov,mp4,m4a,3gp,3g2,mj2" }, }), }, ]); vi.resetModules(); vi.doMock("child_process", () => ({ spawn })); const { probeMediaProfile } = await import("./ffprobe.js"); try { await expect(probeMediaProfile(fixturePath)).resolves.toMatchObject({ hasVideoStream: true, hasAudioStream: false, visualKind: "still", }); } finally { rmSync(fixtureDir, { recursive: true, force: true }); } }); it("uses any non-attached video stream when cover art precedes moving video", async () => { const { spawn } = createSpawnSpy([ { kind: "exit", code: 0, stdout: JSON.stringify({ streams: [ { codec_type: "video", disposition: { attached_pic: 1 } }, { codec_type: "video", codec_name: "h264" }, { codec_type: "audio" }, ], format: { format_name: "mov,mp4,m4a,3gp,3g2,mj2" }, }), }, ]); vi.resetModules(); vi.doMock("child_process", () => ({ spawn })); const { probeMediaProfile } = await import("./ffprobe.js"); await expect(probeMediaProfile("/tmp/video-with-cover")).resolves.toMatchObject({ visualKind: "moving", }); }); it.skipIf(spawnSync("ffprobe", ["-version"]).status !== 0)( "rejects an IHDR-only truncated PNG even when ffprobe accepts png_pipe", async () => { vi.resetModules(); vi.doUnmock("child_process"); const fixtureDir = mkdtempSync(resolve(tmpdir(), "hf-truncated-png-profile-")); const fixturePath = resolve(fixtureDir, "asset"); const ihdr = pngChunk("IHDR", [0, 0, 0, 1, 0, 0, 0, 1, 16, 2, 0, 0, 0]); writeFileSync(fixturePath, buildPngWithChunks([ihdr])); const { probeMediaProfile } = await import("./ffprobe.js"); try { await expect(probeMediaProfile(fixturePath)).rejects.toThrow(); } finally { rmSync(fixtureDir, { recursive: true, force: true }); } }, ); it("does not turn an aborted PNG probe into a successful metadata fallback", async () => { type KillableFakeProc = FakeProc & { kill: (signal?: NodeJS.Signals) => boolean }; const spawn = () => { const proc = new EventEmitter() as KillableFakeProc; proc.stdout = new EventEmitter(); proc.stderr = new EventEmitter(); proc.kill = vi.fn(() => { process.nextTick(() => proc.emit("close", null, "SIGTERM")); return true; }); process.nextTick(() => proc.emit("spawn")); return proc; }; const fixtureDir = mkdtempSync(resolve(tmpdir(), "hf-aborted-png-profile-")); const fixturePath = resolve(fixtureDir, "asset"); writeFileSync(fixturePath, buildMinimalPng()); vi.resetModules(); vi.doMock("child_process", () => ({ spawn })); const { probeMediaProfile } = await import("./ffprobe.js"); const controller = new AbortController(); try { const pending = probeMediaProfile(fixturePath, { signal: controller.signal }); controller.abort(new Error("render cancelled")); await expect(pending).rejects.toThrow("render cancelled"); } finally { rmSync(fixtureDir, { recursive: true, force: true }); } }); }); interface SpawnCall { command: string; args: readonly string[]; } interface FakeProc extends EventEmitter { stdout: EventEmitter; stderr: EventEmitter; } type SpawnOutcome = | { kind: "missing" } | { kind: "error"; message: string; code?: string } | { kind: "exit"; code: number; stdout?: string; stderr?: string; /** Emit stdout as these exact byte chunks, to exercise a multi-byte * character split across a pipe-chunk boundary. */ stdoutChunks?: Buffer[]; }; function createSpawnSpy(outcomes: SpawnOutcome[]): { spawn: (command: string, args: readonly string[]) => FakeProc; calls: SpawnCall[]; } { const calls: SpawnCall[] = []; let invocation = 0; const spawn = (command: string, args: readonly string[]): FakeProc => { calls.push({ command, args }); const outcome = outcomes[invocation] ?? outcomes[outcomes.length - 1]; invocation += 1; const proc = new EventEmitter() as FakeProc; proc.stdout = new EventEmitter(); proc.stderr = new EventEmitter(); process.nextTick(() => { if (!outcome) return; if (outcome.kind === "missing") { const err = new Error("spawn ffprobe ENOENT") as NodeJS.ErrnoException; err.code = "ENOENT"; proc.emit("error", err); return; } if (outcome.kind === "error") { const err = new Error(outcome.message) as NodeJS.ErrnoException; if (outcome.code) err.code = outcome.code; proc.emit("error", err); return; } if (outcome.stdoutChunks) { for (const chunk of outcome.stdoutChunks) proc.stdout.emit("data", chunk); } else if (outcome.stdout) proc.stdout.emit("data", Buffer.from(outcome.stdout)); if (outcome.stderr) proc.stderr.emit("data", Buffer.from(outcome.stderr)); proc.emit("close", outcome.code); }); return proc; }; return { spawn, calls }; } describe("ffprobe missing-binary fallback", () => { const originalFfprobePath = process.env.HYPERFRAMES_FFPROBE_PATH; const originalPath = process.env.PATH; function hidePathBinaries(): void { process.env.PATH = ""; } afterEach(() => { vi.resetModules(); vi.doUnmock("child_process"); if (originalFfprobePath === undefined) delete process.env.HYPERFRAMES_FFPROBE_PATH; else process.env.HYPERFRAMES_FFPROBE_PATH = originalFfprobePath; if (originalPath === undefined) delete process.env.PATH; else process.env.PATH = originalPath; }); it("spawns the configured absolute FFprobe path when HYPERFRAMES_FFPROBE_PATH is set", async () => { process.env.HYPERFRAMES_FFPROBE_PATH = "/tools/ffprobe.exe"; const successfulStderr = "recoverable diagnostic on a successful probe"; const { spawn, calls } = createSpawnSpy([ { kind: "exit", code: 0, stdout: JSON.stringify({ streams: [{ codec_type: "audio", codec_name: "aac", sample_rate: "48000", channels: 2 }], format: { duration: "1.25", bit_rate: "128000" }, }), stderr: successfulStderr, }, ]); vi.resetModules(); vi.doMock("child_process", () => ({ spawn })); const { extractAudioMetadata } = await import("./ffprobe.js"); const meta = await extractAudioMetadata("/tmp/uses-configured-ffprobe.wav"); expect(meta.durationSeconds).toBe(1.25); expect(JSON.stringify(meta)).not.toContain(successfulStderr); expect(calls[0]?.command).toBe(resolve("/tools/ffprobe.exe")); expect(calls[0]?.args.slice(0, 2)).toEqual(["-v", "error"]); }); it("does not accept an incomplete PNG through the missing-binary fallback", async () => { const fixtureDir = mkdtempSync(resolve(tmpdir(), "hf-truncated-png-fallback-")); const fixturePath = resolve(fixtureDir, "asset"); const ihdr = pngChunk("IHDR", [0, 0, 0, 1, 0, 0, 0, 1, 16, 2, 0, 0, 0]); writeFileSync(fixturePath, buildPngWithChunks([ihdr])); const { spawn } = createSpawnSpy([{ kind: "missing" }]); hidePathBinaries(); vi.resetModules(); vi.doMock("child_process", () => ({ spawn })); const { probeMediaProfile } = await import("./ffprobe.js"); try { await expect(probeMediaProfile(fixturePath)).rejects.toThrow(/ffprobe/i); } finally { rmSync(fixtureDir, { recursive: true, force: true }); } }); // `profile` matters now: the packet refinement is an allowlist on AAC-LC, // because the 1024-sample formula is wrong for LD/ELD/HE and unverified for // the rest. An unprofiled "aac" stream deliberately keeps its container // duration rather than being refined on an assumption. it.each([ { name: "non-AAC metadata", codec: "mp3", profile: undefined, packets: undefined, expected: 1.25, calls: 1, }, { name: "unprofiled AAC", codec: "aac", profile: undefined, packets: "783", expected: 1.25, calls: 1, }, { name: "valid AAC-LC packet count", codec: "aac", profile: "LC", packets: "783", expected: 16.704, calls: 2, }, { name: "missing AAC packet count", codec: "aac", profile: "LC", packets: undefined, expected: 1.25, calls: 2, }, { name: "zero AAC packet count", codec: "aac", profile: "LC", packets: "0", expected: 1.25, calls: 2, }, { name: "invalid AAC packet count", codec: "aac", profile: "LC", packets: "invalid", expected: 1.25, calls: 2, }, ])( "derives audio duration for $name", async ({ codec, profile, packets, expected, calls: expectedCalls }) => { const outcomes: SpawnOutcome[] = [ { kind: "exit", code: 0, stdout: JSON.stringify({ streams: [ { codec_type: "audio", codec_name: codec, sample_rate: "48000", channels: 2, profile, }, ], format: { duration: "1.25", bit_rate: "128000" }, }), }, ]; if (codec === "aac" && profile === "LC") { outcomes.push({ kind: "exit", code: 0, stdout: JSON.stringify({ streams: [{ nb_read_packets: packets }], format: {} }), }); } const { spawn, calls } = createSpawnSpy(outcomes); vi.resetModules(); vi.doMock("child_process", () => ({ spawn })); const { extractAudioMetadata } = await import("./ffprobe.js"); const meta = await extractAudioMetadata(`/tmp/${codec}-${packets ?? "none"}.audio`); expect(meta.durationSeconds).toBeCloseTo(expected, 6); expect(calls).toHaveLength(expectedCalls); }, ); it("extractMediaMetadata falls back to PNG cICP metadata when ffprobe is missing", async () => { const { spawn, calls } = createSpawnSpy([{ kind: "missing" }]); hidePathBinaries(); vi.resetModules(); vi.doMock("child_process", () => ({ spawn })); const { extractMediaMetadata: extractMediaMetadataMocked } = await import("./ffprobe.js"); const fixture = resolve( __dirname, "../../../producer/tests/hdr-regression/src/hdr-photo-pq.png", ); const meta = await extractMediaMetadataMocked(fixture); expect(calls.length).toBe(1); expect(basename(calls[0]?.command ?? "")).toMatch(/^ffprobe(?:\.exe)?$/); expect(meta.videoCodec).toBe("png"); expect(meta.durationSeconds).toBe(0); expect(meta.fps).toBe(0); expect(meta.hasAudio).toBe(false); expect(meta.isVFR).toBe(false); expect(meta.hasAlpha).toBe(false); expect(meta.colorSpace?.colorTransfer).toBe("smpte2084"); expect(meta.colorSpace?.colorPrimaries).toBe("bt2020"); }); it("extractMediaMetadata detects VP9 alpha_mode streams", async () => { const { spawn } = createSpawnSpy([ { kind: "exit", code: 0, stdout: JSON.stringify({ streams: [ { codec_type: "video", codec_name: "vp9", width: 320, height: 180, r_frame_rate: "30/1", avg_frame_rate: "30/1", pix_fmt: "yuv420p", tags: { alpha_mode: "1" }, }, ], format: { duration: "1.5" }, }), }, ]); vi.resetModules(); vi.doMock("child_process", () => ({ spawn })); const { extractMediaMetadata: extractMediaMetadataMocked } = await import("./ffprobe.js"); const meta = await extractMediaMetadataMocked("/tmp/alpha.webm"); expect(meta.videoCodec).toBe("vp9"); expect(meta.hasAlpha).toBe(true); }); it("normalizes omitted video color components to empty strings", async () => { const { spawn } = createSpawnSpy([ { kind: "exit", code: 0, stdout: JSON.stringify({ streams: [ { codec_type: "video", codec_name: "h264", width: 64, height: 64, r_frame_rate: "30/1", avg_frame_rate: "30/1", pix_fmt: "yuv420p", color_space: "bt709", }, ], format: { duration: "1" }, }), }, ]); vi.resetModules(); vi.doMock("child_process", () => ({ spawn })); const { extractMediaMetadata: extractMediaMetadataMocked } = await import("./ffprobe.js"); const metadata = await extractMediaMetadataMocked("/tmp/partial-color.mp4"); expect(metadata.colorSpace).toEqual({ colorPrimaries: "", colorTransfer: "", colorSpace: "bt709", }); }); // Regression: newer libavformat builds (and the output of `hyperframes // remove-background` itself) write the VP9-alpha sidecar tag as // `ALPHA_MODE` (uppercase). The lowercase-only check classified those // files as having no alpha, the producer extracted them as JPGs, and // the injected overlays were fully opaque rectangles that hid // every static element below them on the z-stack. The bug was silent — // studio preview rendered correctly via native