Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -262,11 +262,19 @@ jobs:

test:
name: Test
needs: changes
if: needs.changes.outputs.code == 'true'
needs: [changes, producer-source-tests]
# Keep the existing required `Test` context authoritative for producer
# failures too. The dedicated producer matrix remains parallel and legible,
# while this job fails closed if either lane fails or is cancelled.
if: always() && needs.changes.outputs.code == 'true'
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Require producer source tests
if: needs.producer-source-tests.result != 'success'
run: |
echo "::error::Producer unit/integration tests did not succeed."
exit 1
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
lfs: true
Expand Down
2 changes: 2 additions & 0 deletions packages/engine/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -264,9 +264,11 @@ export {
extractVideoMetadata,
extractFinalVideoFrameTimestamp,
extractAudioMetadata,
probeMediaProfile,
analyzeKeyframeIntervals,
type VideoMetadata,
type AudioMetadata,
type MediaProbeProfile,
type KeyframeAnalysis,
} from "./utils/ffprobe.js";

Expand Down
290 changes: 288 additions & 2 deletions packages/engine/src/utils/ffprobe.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// fallow-ignore-file code-duplication
import { EventEmitter } from "events";
import { readFileSync } from "fs";
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 {
Expand Down Expand Up @@ -113,6 +115,272 @@ describe("extractPngMetadataFromBuffer", () => {
);
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 {
Expand Down Expand Up @@ -222,6 +490,23 @@ describe("ffprobe missing-binary fallback", () => {
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
Expand Down Expand Up @@ -715,11 +1000,12 @@ describe("extractPngMetadataFromBuffer cICP ordering", () => {
it("does not emit color space until IHDR provides width and height", () => {
const ihdr = pngChunk("IHDR", [0, 0, 0, 1, 0, 0, 0, 1, 16, 2, 0, 0, 0]);
const cicp = pngChunk("cICP", [9, 16, 0, 1]);
const idat = pngChunk("IDAT", [0x78, 0x9c, 0x03, 0x00, 0x00, 0x00, 0x00, 0x01]);
const iend = pngChunk("IEND", []);

// cICP before IHDR is invalid PNG ordering; make sure we don't return
// zero-sized metadata in that case.
const malformed = buildPngWithChunks([cicp, ihdr, iend]);
const malformed = buildPngWithChunks([cicp, ihdr, idat, iend]);
expect(extractPngMetadataFromBuffer(malformed)).toEqual({
width: 1,
height: 1,
Expand Down
Loading
Loading