Compare commits

...

5 Commits

Author SHA1 Message Date
diegosouzapw
96c2f303fc refactor(video): unify the drill-down JPEG data-URI prefix with the shared contract
Drops videoBridgeDrilldown's local JPEG_DATA_URI_PREFIX constant in favor of the
exported JPEG_FRAME_DATA_URI_PREFIX from videoBridgeFrameContract, matching the
contact-sheet and dedup call sites. Literal value and all validation semantics
(decodeCanonicalJpeg, the chars bound, timestamp/cap/ordering rules) are unchanged.
2026-09-01 12:16:14 -03:00
diegosouzapw
5f43300659 refactor(video): restore the contact-sheet per-frame byte cap via the shared estimator 2026-09-01 09:47:26 -03:00
diegosouzapw
bc39595ec6 refactor(video): route contact-sheet and dedup frame decoding through the shared contract 2026-09-01 09:23:20 -03:00
diegosouzapw
d7fc935514 refactor(video): derive the JPEG frame pattern from the exported prefix 2026-09-01 08:01:00 -03:00
diegosouzapw
630dc172a2 refactor(video): add the shared JPEG frame data-URI contract 2026-09-01 07:48:23 -03:00
6 changed files with 109 additions and 21 deletions

View File

@@ -1,3 +1,6 @@
import { decodeJpegFrameDataUri, estimateJpegFrameBytes } from "./videoBridgeFrameContract";
import { VIDEO_FRAME_MAX_BYTES } from "./videoBridgeRuntime";
export interface ContactSheetFrame {
dataUri: string;
timestampSeconds: number;
@@ -35,12 +38,6 @@ function fallback(frames: readonly ContactSheetFrame[]): VideoContactSheetResult
};
}
function decodeFrame(dataUri: string): Buffer {
const match = /^data:image\/jpeg;base64,([A-Za-z0-9+/=]{4,5592408})$/i.exec(dataUri);
if (!match) throw new Error("Contact sheet requires JPEG data URIs");
return Buffer.from(match[1], "base64");
}
function formatContactSheetTimestamp(timestampSeconds: number): string {
const totalMilliseconds = Math.max(0, Math.round(timestampSeconds * 1000));
const minutes = Math.floor(totalMilliseconds / 60_000);
@@ -89,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(decodeFrame(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

@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
import sharp from "sharp";
import { JPEG_FRAME_DATA_URI_PREFIX } from "./videoBridgeFrameContract";
import { resolveVideoFocusWindow, type VideoFocusWindow } from "./videoBridgeRuntime";
export interface VideoDrilldownFrameInput {
@@ -101,9 +102,8 @@ export const VIDEO_DRILLDOWN_MAX_FRAME_BYTES = 4 * 1024 * 1024;
export const VIDEO_DRILLDOWN_MAX_ENTRY_BYTES = 32 * 1024 * 1024;
const MAX_DURATION_SECONDS = 600;
const MAX_FRAME_DIMENSION = 8192;
const JPEG_DATA_URI_PREFIX = "data:image/jpeg;base64,";
export const VIDEO_DRILLDOWN_MAX_FRAME_DATA_URI_CHARS =
JPEG_DATA_URI_PREFIX.length + Math.ceil(VIDEO_DRILLDOWN_MAX_FRAME_BYTES / 3) * 4;
JPEG_FRAME_DATA_URI_PREFIX.length + Math.ceil(VIDEO_DRILLDOWN_MAX_FRAME_BYTES / 3) * 4;
function validationFailure(message: string): never {
throw new VideoDrilldownValidationError(message);
@@ -270,10 +270,10 @@ async function decodeCanonicalJpeg(
resolution: { height: number; width: number };
}> {
throwIfAborted(signal);
if (!dataUri.startsWith(JPEG_DATA_URI_PREFIX)) {
if (!dataUri.startsWith(JPEG_FRAME_DATA_URI_PREFIX)) {
validationFailure("Invalid drill-down JPEG frame");
}
const encoded = dataUri.slice(JPEG_DATA_URI_PREFIX.length);
const encoded = dataUri.slice(JPEG_FRAME_DATA_URI_PREFIX.length);
if (dataUri.length > VIDEO_DRILLDOWN_MAX_FRAME_DATA_URI_CHARS) {
validationFailure("Drill-down frame byte limit exceeded");
}
@@ -622,7 +622,7 @@ export class VideoDrilldownCache {
)
.slice(0, frameCount)
.map((frame) => ({
dataUri: `${JPEG_DATA_URI_PREFIX}${frame.data.toString("base64")}`,
dataUri: `${JPEG_FRAME_DATA_URI_PREFIX}${frame.data.toString("base64")}`,
height: frame.height,
timestampSeconds: frame.timestampSeconds,
width: frame.width,

View File

@@ -0,0 +1,28 @@
export const JPEG_FRAME_DATA_URI_PREFIX = "data:image/jpeg;base64,";
// Derived from the exported prefix so the two can never drift apart.
const JPEG_FRAME_DATA_URI_PATTERN = new RegExp(
`^${JPEG_FRAME_DATA_URI_PREFIX.replace(/[.*+?^${}()|[\]\\/]/g, "\\$&")}([A-Za-z0-9+/=]+)$`,
"i"
);
function matchJpegFrame(dataUri: string): string {
const match = JPEG_FRAME_DATA_URI_PATTERN.exec(dataUri);
if (!match) throw new Error("Video frame is not a JPEG data URI");
return match[1];
}
/**
* Throws on any non-JPEG or base64-invalid input; the single frame decode used by every
* video module.
*/
export function decodeJpegFrameDataUri(dataUri: string): Buffer {
return Buffer.from(matchJpegFrame(dataUri), "base64");
}
/** Decoded-byte estimate without materializing the buffer (validation/budget paths). */
export function estimateJpegFrameBytes(dataUri: string): number {
const encoded = matchJpegFrame(dataUri);
const padding = encoded.endsWith("==") ? 2 : encoded.endsWith("=") ? 1 : 0;
return Math.floor((encoded.length * 3) / 4) - padding;
}

View File

@@ -10,6 +10,7 @@ import {
type BrokerExtractionOptions,
type BrokerExtractionResult,
} from "./videoBridgeBrokerClient";
import { decodeJpegFrameDataUri } from "./videoBridgeFrameContract";
import {
resolveVideoFocusWindow,
type VideoFocusWindow,
@@ -306,16 +307,15 @@ export async function compareVideoFramesByGrayscale(
signal?: AbortSignal
): Promise<number> {
throwIfVideoDedupAborted(signal);
const decode = (dataUri: string): Buffer => {
const match = /^data:image\/jpeg;base64,([A-Za-z0-9+/=]+)$/i.exec(dataUri);
if (!match) throw new Error("Video frame is not a JPEG data URI");
return Buffer.from(match[1], "base64");
};
const { default: sharp } = await import("sharp");
throwIfVideoDedupAborted(signal);
const [left, right] = await Promise.all(
[previous, current].map((frame) =>
sharp(decode(frame.dataUri)).resize(16, 16, { fit: "fill" }).greyscale().raw().toBuffer()
sharp(decodeJpegFrameDataUri(frame.dataUri))
.resize(16, 16, { fit: "fill" })
.greyscale()
.raw()
.toBuffer()
)
);
throwIfVideoDedupAborted(signal);

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();

View File

@@ -0,0 +1,35 @@
import assert from "node:assert/strict";
import test from "node:test";
import {
JPEG_FRAME_DATA_URI_PREFIX,
decodeJpegFrameDataUri,
estimateJpegFrameBytes,
} from "../../../src/lib/guardrails/videoBridgeFrameContract";
test("decodes a valid JPEG data URI case-insensitively", () => {
const bytes = Buffer.from("abc");
const uri = `data:image/JPEG;base64,${bytes.toString("base64")}`;
assert.deepEqual(decodeJpegFrameDataUri(uri), bytes);
assert.equal(JPEG_FRAME_DATA_URI_PREFIX, "data:image/jpeg;base64,");
});
test("rejects non-JPEG and malformed URIs with a stable message", () => {
for (const bad of [
"data:image/png;base64,QQ==",
"data:image/jpeg;base64,@@invalid@@",
"data:image/jpeg,plain",
"https://example.com/frame.jpg",
"",
]) {
assert.throws(() => decodeJpegFrameDataUri(bad), /not a JPEG data URI/i);
assert.throws(() => estimateJpegFrameBytes(bad), /not a JPEG data URI/i);
}
});
test("estimates decoded bytes without decoding, accounting for padding", () => {
for (const source of ["a", "ab", "abc", "abcd", "x".repeat(3000)]) {
const uri = `data:image/jpeg;base64,${Buffer.from(source).toString("base64")}`;
assert.equal(estimateJpegFrameBytes(uri), Buffer.byteLength(source));
}
});