mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-17 20:52:15 +03:00
Implements the secure, opt-in Video Bridge for issue #9760, including bounded FFmpeg frame extraction, capability-aware routing, telemetry, settings UI, localization, documentation, and regression coverage.
458 lines
14 KiB
TypeScript
458 lines
14 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { access, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import test from "node:test";
|
|
|
|
import {
|
|
calculateFrameTimestamps,
|
|
extractFramesFromLocalVideo,
|
|
extractVideoFramesFromBytes,
|
|
probeLocalVideo,
|
|
probeVideoRuntime,
|
|
readBoundedExtractedFrames,
|
|
resetVideoRuntimeProbeCacheForTests,
|
|
type VideoCommandRunner,
|
|
} from "../../../src/lib/guardrails/videoBridgeRuntime.ts";
|
|
|
|
test("calculates uniform midpoint timestamps", () => {
|
|
assert.deepEqual(calculateFrameTimestamps(8, 4), [1, 3, 5, 7]);
|
|
assert.deepEqual(calculateFrameTimestamps(0.4, 8), [0.2]);
|
|
});
|
|
|
|
test("probes and extracts a local video using shell-free bounded commands", async () => {
|
|
const calls: Array<{ executable: string; args: string[]; timeoutMs: number }> = [];
|
|
const runner: VideoCommandRunner = async (executable, args, options) => {
|
|
calls.push({ executable, args: [...args], timeoutMs: options.timeoutMs });
|
|
if (executable === "ffprobe") {
|
|
return {
|
|
stdout: JSON.stringify({
|
|
format: { duration: "8.0", format_name: "mov,mp4,m4a,3gp,3g2,mj2" },
|
|
streams: [{ index: 0, codec_type: "video", width: 1920, height: 1080 }],
|
|
}),
|
|
stderr: "",
|
|
};
|
|
}
|
|
return { stdout: "", stderr: "" };
|
|
};
|
|
|
|
const metadata = await probeLocalVideo("/tmp/input.mp4", {
|
|
maxDurationSeconds: 600,
|
|
runner,
|
|
timeoutMs: 5_000,
|
|
});
|
|
const frames = await extractFramesFromLocalVideo("/tmp/input.mp4", "/tmp/frames", {
|
|
durationSeconds: metadata.durationSeconds,
|
|
frameCount: 4,
|
|
runner,
|
|
streamIndex: metadata.streamIndex,
|
|
timeoutMs: 10_000,
|
|
});
|
|
|
|
assert.equal(metadata.durationSeconds, 8);
|
|
assert.deepEqual(
|
|
frames.map((frame) => frame.timestampSeconds),
|
|
[1, 3, 5, 7]
|
|
);
|
|
assert.equal(calls[0].executable, "ffprobe");
|
|
assert.equal(calls[0].timeoutMs, 5_000);
|
|
assert.deepEqual(calls[0].args.slice(-2), ["json", "/tmp/input.mp4"]);
|
|
assert.deepEqual(
|
|
calls[0].args.slice(
|
|
calls[0].args.indexOf("-protocol_whitelist"),
|
|
calls[0].args.indexOf("-protocol_whitelist") + 2
|
|
),
|
|
["-protocol_whitelist", "file"]
|
|
);
|
|
assert.ok(calls[0].args.includes("-format_whitelist"));
|
|
assert.equal(
|
|
calls[0].args[calls[0].args.indexOf("-show_entries") + 1],
|
|
"format=duration,format_name:stream=index,codec_type,width,height:stream_disposition=default,attached_pic"
|
|
);
|
|
assert.equal(
|
|
calls.slice(1).every((call) => call.executable === "ffmpeg"),
|
|
true
|
|
);
|
|
assert.equal(
|
|
calls.slice(1).every((call) => call.args.includes("-nostdin")),
|
|
true
|
|
);
|
|
assert.equal(
|
|
calls.slice(1).every((call) => call.args.includes("-protocol_whitelist")),
|
|
true
|
|
);
|
|
assert.equal(
|
|
calls.slice(1).every((call) => call.args.includes("-format_whitelist")),
|
|
true
|
|
);
|
|
assert.equal(
|
|
calls.slice(1).every((call) => call.args.includes("-threads") && call.args.includes("1")),
|
|
true
|
|
);
|
|
assert.equal(
|
|
calls
|
|
.slice(1)
|
|
.every((call) =>
|
|
call.args.some(
|
|
(arg) =>
|
|
arg.includes("min(1024,iw)") &&
|
|
arg.includes("min(1024,ih)") &&
|
|
arg.includes("force_original_aspect_ratio=decrease")
|
|
)
|
|
),
|
|
true
|
|
);
|
|
assert.equal(
|
|
calls.slice(1).every((call) => !call.args.some((arg) => arg.includes("://"))),
|
|
true
|
|
);
|
|
});
|
|
|
|
test("rejects remote process inputs and videos beyond the duration bound", async () => {
|
|
const runner: VideoCommandRunner = async () => ({
|
|
stdout: JSON.stringify({
|
|
format: { duration: "601", format_name: "mp4" },
|
|
streams: [{ index: 0, codec_type: "video", width: 1280, height: 720 }],
|
|
}),
|
|
stderr: "private upstream details",
|
|
});
|
|
await assert.rejects(
|
|
() => probeLocalVideo("https://example.test/video.mp4", { runner }),
|
|
/local path/
|
|
);
|
|
await assert.rejects(
|
|
() => probeLocalVideo("/tmp/input.mp4", { maxDurationSeconds: 600, runner }),
|
|
/maximum duration/
|
|
);
|
|
});
|
|
|
|
test("rejects reference-bearing formats before extraction and confines both tools to local files", async () => {
|
|
const calls: Array<{ executable: string; args: string[] }> = [];
|
|
const runner: VideoCommandRunner = async (executable, args) => {
|
|
calls.push({ executable, args: [...args] });
|
|
return {
|
|
stdout: JSON.stringify({
|
|
format: { duration: "10", format_name: "hls" },
|
|
streams: [{ index: 0, codec_type: "video", width: 640, height: 360 }],
|
|
}),
|
|
stderr: "http://169.254.169.254/latest/meta-data",
|
|
};
|
|
};
|
|
|
|
await assert.rejects(() => probeLocalVideo("/tmp/malicious.m3u8", { runner }), /format/);
|
|
assert.equal(calls.length, 1, "a rejected manifest must never reach ffmpeg");
|
|
assert.deepEqual(
|
|
calls[0].args.slice(
|
|
calls[0].args.indexOf("-protocol_whitelist"),
|
|
calls[0].args.indexOf("-protocol_whitelist") + 2
|
|
),
|
|
["-protocol_whitelist", "file"]
|
|
);
|
|
assert.equal(
|
|
calls[0].args.some((arg) => arg.includes("169.254.169.254")),
|
|
false
|
|
);
|
|
});
|
|
|
|
test("safe containers may contain URL or traversal-like compressed bytes without false rejection", async () => {
|
|
const calls: string[] = [];
|
|
const runner: VideoCommandRunner = async (executable, args) => {
|
|
calls.push(executable);
|
|
if (executable === "ffprobe") {
|
|
return {
|
|
stdout: JSON.stringify({
|
|
format: { duration: "2", format_name: "mp4" },
|
|
streams: [{ index: 0, codec_type: "video", width: 640, height: 360 }],
|
|
}),
|
|
stderr: "",
|
|
};
|
|
}
|
|
await writeFile(args.at(-1) ?? "", Buffer.from([0xff, 0xd8, 0xff, 0xd9]));
|
|
return { stdout: "", stderr: "" };
|
|
};
|
|
const validContainerBytes = Buffer.concat([
|
|
Buffer.from([0, 0, 0, 24, 0x66, 0x74, 0x79, 0x70]),
|
|
Buffer.from("compressed-chunk:http://127.0.0.1/../not-a-reference"),
|
|
]);
|
|
|
|
const result = await extractVideoFramesFromBytes(validContainerBytes, {
|
|
frameCount: 1,
|
|
maxDurationSeconds: 600,
|
|
runner,
|
|
timeoutMs: 5_000,
|
|
});
|
|
|
|
assert.deepEqual(calls, ["ffprobe", "ffmpeg"]);
|
|
assert.equal(result.frames.length, 1);
|
|
});
|
|
|
|
test("rejects oversized dimensions and pixel counts from sanitized probe metadata", async () => {
|
|
const runner: VideoCommandRunner = async () => ({
|
|
stdout: JSON.stringify({
|
|
format: { duration: "2", format_name: "mp4" },
|
|
streams: [{ index: 0, codec_type: "video", width: 16384, height: 16384 }],
|
|
}),
|
|
stderr: "private path",
|
|
});
|
|
await assert.rejects(() => probeLocalVideo("/tmp/oversized.mp4", { runner }), /dimensions/);
|
|
});
|
|
|
|
test("rejects a container when any video stream exceeds dimension or pixel limits", async () => {
|
|
const runner: VideoCommandRunner = async () => ({
|
|
stdout: JSON.stringify({
|
|
format: { duration: "2", format_name: "mp4" },
|
|
streams: [
|
|
{ index: 0, codec_type: "video", width: 640, height: 360 },
|
|
{ index: 1, codec_type: "video", width: 16384, height: 16384 },
|
|
],
|
|
}),
|
|
stderr: "",
|
|
});
|
|
|
|
await assert.rejects(
|
|
() => probeLocalVideo("/tmp/multiple-streams.mp4", { runner }),
|
|
/dimensions/
|
|
);
|
|
});
|
|
|
|
test("selects the lowest validated video stream index and maps it explicitly in ffmpeg", async () => {
|
|
const calls: Array<{ executable: string; args: string[] }> = [];
|
|
const runner: VideoCommandRunner = async (executable, args) => {
|
|
calls.push({ executable, args: [...args] });
|
|
return executable === "ffprobe"
|
|
? {
|
|
stdout: JSON.stringify({
|
|
format: { duration: "4", format_name: "mp4" },
|
|
streams: [
|
|
{ index: 3, codec_type: "video", width: 1280, height: 720 },
|
|
{ index: 1, codec_type: "video", width: 640, height: 360 },
|
|
],
|
|
}),
|
|
stderr: "",
|
|
}
|
|
: { stdout: "", stderr: "" };
|
|
};
|
|
|
|
const metadata = await probeLocalVideo("/tmp/multiple-safe.mp4", { runner });
|
|
await extractFramesFromLocalVideo("/tmp/multiple-safe.mp4", "/tmp/frames", {
|
|
durationSeconds: metadata.durationSeconds,
|
|
frameCount: 1,
|
|
runner,
|
|
streamIndex: metadata.streamIndex,
|
|
});
|
|
|
|
assert.equal(metadata.streamIndex, 1);
|
|
const ffmpegArgs = calls.find((call) => call.executable === "ffmpeg")?.args ?? [];
|
|
const mapIndex = ffmpegArgs.indexOf("-map");
|
|
assert.deepEqual(ffmpegArgs.slice(mapIndex, mapIndex + 2), ["-map", "0:1"]);
|
|
});
|
|
|
|
test("ignores an attached cover and maps the preferred playable default stream", async () => {
|
|
const calls: Array<{ executable: string; args: string[] }> = [];
|
|
const runner: VideoCommandRunner = async (executable, args) => {
|
|
calls.push({ executable, args: [...args] });
|
|
return executable === "ffprobe"
|
|
? {
|
|
stdout: JSON.stringify({
|
|
format: { duration: "4", format_name: "mp4" },
|
|
streams: [
|
|
{
|
|
index: 0,
|
|
codec_type: "video",
|
|
width: 20000,
|
|
height: 20000,
|
|
disposition: { attached_pic: 1, default: 0 },
|
|
},
|
|
{
|
|
index: 1,
|
|
codec_type: "video",
|
|
width: 640,
|
|
height: 360,
|
|
disposition: { attached_pic: 0, default: 0 },
|
|
},
|
|
{
|
|
index: 2,
|
|
codec_type: "video",
|
|
width: 1280,
|
|
height: 720,
|
|
disposition: { attached_pic: 0, default: 1 },
|
|
},
|
|
],
|
|
}),
|
|
stderr: "",
|
|
}
|
|
: { stdout: "", stderr: "" };
|
|
};
|
|
|
|
const metadata = await probeLocalVideo("/tmp/cover-and-video.mp4", { runner });
|
|
await extractFramesFromLocalVideo("/tmp/cover-and-video.mp4", "/tmp/frames", {
|
|
durationSeconds: metadata.durationSeconds,
|
|
frameCount: 1,
|
|
runner,
|
|
streamIndex: metadata.streamIndex,
|
|
});
|
|
|
|
assert.equal(metadata.streamIndex, 2);
|
|
assert.equal(metadata.width, 1280);
|
|
assert.equal(metadata.height, 720);
|
|
const ffmpegArgs = calls.find((call) => call.executable === "ffmpeg")?.args ?? [];
|
|
const mapIndex = ffmpegArgs.indexOf("-map");
|
|
assert.deepEqual(ffmpegArgs.slice(mapIndex, mapIndex + 2), ["-map", "0:2"]);
|
|
});
|
|
|
|
test("rejects a container whose only video stream is an attached picture", async () => {
|
|
const runner: VideoCommandRunner = async () => ({
|
|
stdout: JSON.stringify({
|
|
format: { duration: "4", format_name: "mp4" },
|
|
streams: [
|
|
{ index: 0, codec_type: "audio" },
|
|
{
|
|
index: 1,
|
|
codec_type: "video",
|
|
width: 600,
|
|
height: 600,
|
|
disposition: { attached_pic: 1, default: 1 },
|
|
},
|
|
],
|
|
}),
|
|
stderr: "",
|
|
});
|
|
|
|
await assert.rejects(
|
|
() => probeLocalVideo("/tmp/audio-with-cover.mp4", { runner }),
|
|
/playable video stream/
|
|
);
|
|
});
|
|
|
|
test("malformed playable stream disposition or index fails closed without selecting a cover", async () => {
|
|
const runner: VideoCommandRunner = async () => ({
|
|
stdout: JSON.stringify({
|
|
format: { duration: "4", format_name: "mp4" },
|
|
streams: [
|
|
{
|
|
index: 0,
|
|
codec_type: "video",
|
|
width: 300,
|
|
height: 300,
|
|
disposition: { attached_pic: "1", default: "not-a-flag" },
|
|
},
|
|
{
|
|
index: "bad",
|
|
codec_type: "video",
|
|
width: 1280,
|
|
height: 720,
|
|
disposition: { attached_pic: 0, default: 1 },
|
|
},
|
|
],
|
|
}),
|
|
stderr: "",
|
|
});
|
|
|
|
await assert.rejects(
|
|
() => probeLocalVideo("/tmp/malformed-stream.mp4", { runner }),
|
|
/dimensions|stream metadata/
|
|
);
|
|
});
|
|
|
|
test("runtime status exposes sanitized versions and a sanitized unavailable reason", async () => {
|
|
resetVideoRuntimeProbeCacheForTests();
|
|
const ready = await probeVideoRuntime({
|
|
cacheTtlMs: 0,
|
|
runner: async (executable) => ({
|
|
stdout:
|
|
executable === "ffmpeg" ? "ffmpeg version 6.1.1 secret" : "ffprobe version 6.1.1 secret",
|
|
stderr: "",
|
|
}),
|
|
});
|
|
assert.deepEqual(ready, {
|
|
available: true,
|
|
ffmpegVersion: "6.1.1",
|
|
ffprobeVersion: "6.1.1",
|
|
});
|
|
|
|
resetVideoRuntimeProbeCacheForTests();
|
|
const unavailable = await probeVideoRuntime({
|
|
cacheTtlMs: 0,
|
|
runner: async () => {
|
|
throw new Error("spawn /private/operator/path ENOENT");
|
|
},
|
|
});
|
|
assert.deepEqual(unavailable, {
|
|
available: false,
|
|
ffmpegVersion: null,
|
|
ffprobeVersion: null,
|
|
reason: "FFmpeg and ffprobe are not available on PATH",
|
|
});
|
|
});
|
|
|
|
test("runtime probe uses its short cache instead of spawning on every status read", async () => {
|
|
resetVideoRuntimeProbeCacheForTests();
|
|
let calls = 0;
|
|
const runner: VideoCommandRunner = async (executable) => {
|
|
calls += 1;
|
|
return {
|
|
stdout: `${executable} version 7.0`,
|
|
stderr: "",
|
|
};
|
|
};
|
|
|
|
const first = await probeVideoRuntime({ cacheTtlMs: 30_000, runner });
|
|
const second = await probeVideoRuntime({ cacheTtlMs: 30_000, runner });
|
|
assert.deepEqual(second, first);
|
|
assert.equal(calls, 2, "one ffmpeg + one ffprobe process should serve both reads");
|
|
});
|
|
|
|
test("checks individual and aggregate frame byte caps before returning broker output", async () => {
|
|
const directory = await mkdtemp(join(tmpdir(), "video-frame-caps-"));
|
|
const first = join(directory, "first.jpg");
|
|
const second = join(directory, "second.jpg");
|
|
await writeFile(first, Buffer.alloc(3));
|
|
await writeFile(second, Buffer.alloc(3));
|
|
const frames = [
|
|
{ path: first, timestampSeconds: 1 },
|
|
{ path: second, timestampSeconds: 2 },
|
|
];
|
|
try {
|
|
await assert.rejects(
|
|
() => readBoundedExtractedFrames(frames, { maxFrameBytes: 2, maxTotalBytes: 8 }),
|
|
/frame byte limit/
|
|
);
|
|
await assert.rejects(
|
|
() => readBoundedExtractedFrames(frames, { maxFrameBytes: 4, maxTotalBytes: 5 }),
|
|
/total frame byte limit/
|
|
);
|
|
const result = await readBoundedExtractedFrames(frames, {
|
|
maxFrameBytes: 4,
|
|
maxTotalBytes: 6,
|
|
});
|
|
assert.equal(result.length, 2);
|
|
assert.equal(
|
|
result.reduce((sum, frame) => sum + frame.byteLength, 0),
|
|
6
|
|
);
|
|
} finally {
|
|
await rm(directory, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("byte extraction removes its private temporary tree after a subprocess failure", async () => {
|
|
let temporaryInput = "";
|
|
const runner: VideoCommandRunner = async (_executable, args) => {
|
|
temporaryInput = args.at(-1) ?? "";
|
|
throw Object.assign(new Error("private ffprobe path"), { code: "ENOENT" });
|
|
};
|
|
|
|
await assert.rejects(
|
|
() =>
|
|
extractVideoFramesFromBytes(Buffer.from("video"), {
|
|
frameCount: 1,
|
|
maxDurationSeconds: 600,
|
|
runner,
|
|
timeoutMs: 5_000,
|
|
}),
|
|
/private ffprobe path/
|
|
);
|
|
assert.notEqual(temporaryInput, "");
|
|
await assert.rejects(() => access(temporaryInput));
|
|
});
|