mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-18 21:02:50 +03:00
refactor(video): restore the contact-sheet per-frame byte cap via the shared estimator
This commit is contained in:
@@ -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({
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user