refactor(video): restore the contact-sheet per-frame byte cap via the shared estimator

This commit is contained in:
diegosouzapw
2026-09-01 09:47:26 -03:00
parent bc39595ec6
commit 5f43300659
2 changed files with 34 additions and 5 deletions

View File

@@ -1,4 +1,5 @@
import { decodeJpegFrameDataUri } from "./videoBridgeFrameContract";
import { decodeJpegFrameDataUri, estimateJpegFrameBytes } from "./videoBridgeFrameContract";
import { VIDEO_FRAME_MAX_BYTES } from "./videoBridgeRuntime";
export interface ContactSheetFrame {
dataUri: string;
@@ -85,13 +86,18 @@ export async function buildVideoContactSheet(
const { default: sharp } = await import("sharp");
if (signal.aborted) throw new Error("Video contact sheet was aborted");
const tiles = await Promise.all(
frames.map(async (frame) =>
sharp(decodeJpegFrameDataUri(frame.dataUri))
frames.map(async (frame) => {
// Reject before decoding: an oversized frame must never reach sharp() just to be
// discovered later — estimateJpegFrameBytes reads the encoded length only.
if (estimateJpegFrameBytes(frame.dataUri) > VIDEO_FRAME_MAX_BYTES) {
throw new Error("Contact sheet frame exceeds the maximum per-frame size");
}
return sharp(decodeJpegFrameDataUri(frame.dataUri))
.resize(TILE_SIZE, TILE_SIZE, { fit: "contain", background: "#000000" })
.composite([{ input: buildTimestampLabel(frame.timestampSeconds), left: 0, top: 0 }])
.jpeg({ quality: 80 })
.toBuffer()
)
.toBuffer();
})
);
if (signal.aborted) throw new Error("Video contact sheet was aborted");
const output = await sharp({

View File

@@ -97,6 +97,29 @@ test("contact sheet falls back to individual frames when decoding fails", async
assert.deepEqual(result.frames, frames);
});
test("contact sheet falls back to individual frames when a frame exceeds the per-frame byte cap", async () => {
const validJpegBytes = await sharp({
create: { background: "red", channels: 3, height: 24, width: 32 },
})
.jpeg()
.toBuffer();
// A technically-decodable JPEG prefix followed by zero-filled padding past
// VIDEO_FRAME_MAX_BYTES (4 MiB): without a pre-decode size guard, sharp decodes the
// leading valid JPEG and ignores the trailing bytes after EOI, so an oversized frame
// would otherwise sail through the contact-sheet path undetected (used: true).
const oversizedBytes = Buffer.concat([validJpegBytes, Buffer.alloc(5 * 1024 * 1024, 0)]);
const frames = [
{
dataUri: `data:image/jpeg;base64,${oversizedBytes.toString("base64")}`,
timestampSeconds: 2,
},
];
const result = await buildVideoContactSheet(frames);
assert.equal(result.used, false);
assert.equal(result.fallbackReason, "CONTACT_SHEET_UNAVAILABLE");
assert.deepEqual(result.frames, frames);
});
test("contact sheet respects the parent abort signal", async () => {
const controller = new AbortController();
controller.abort();