mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 04:38:33 +00:00
fix(cli): detect video motion in sweep guard (#2308)
* fix(cli): detect video motion in sweep guard * docs(cli): clarify iframe fingerprint scope
This commit is contained in:
@@ -942,22 +942,26 @@
|
||||
// actually moved anything and the whole audit run is unreliable. Deliberately
|
||||
// a single opaque string (not a structured array) since Node only ever needs
|
||||
// equality, not per-element diffing.
|
||||
// Pixel-only canvas motion (a 2D/WebGL canvas repainting without any element
|
||||
// moving) is invisible to a geometry+opacity fingerprint and false-positived
|
||||
// sweep_static (wild report: 4K WebGL comp needed a transparent moving DOM
|
||||
// sentinel to pass). Downsample each visible canvas to 8x8 and fold its
|
||||
// pixels into the fingerprint. Tainted/zero-sized/unreadable canvases hash
|
||||
// to a constant — no worse than today, never a new false NEGATIVE for
|
||||
// DOM-motion comps.
|
||||
function canvasPixelHash(canvas) {
|
||||
// Pixel-only media motion (a 2D/WebGL canvas repainting or a playing video
|
||||
// without any element moving) is invisible to a geometry+opacity fingerprint
|
||||
// and false-positives sweep_static. Downsample each visible canvas/video to
|
||||
// 8x8 and fold its pixels into the fingerprint. Tainted, zero-sized, or
|
||||
// unreadable media hashes to a constant — no worse than geometry-only
|
||||
// detection and never a new false negative for DOM-motion compositions.
|
||||
// Media inside iframes is intentionally outside this fingerprint: it lives
|
||||
// in a separate document, and cross-origin frames are inaccessible under SOP.
|
||||
function mediaPixelHash(element) {
|
||||
try {
|
||||
if (!canvas.width || !canvas.height) return "x";
|
||||
const rect = element.getBoundingClientRect();
|
||||
const sourceWidth = element.videoWidth || element.width || rect.width;
|
||||
const sourceHeight = element.videoHeight || element.height || rect.height;
|
||||
if (!sourceWidth || !sourceHeight) return "x";
|
||||
const off = document.createElement("canvas");
|
||||
off.width = 8;
|
||||
off.height = 8;
|
||||
const ctx = off.getContext("2d");
|
||||
if (!ctx) return "x";
|
||||
ctx.drawImage(canvas, 0, 0, 8, 8);
|
||||
ctx.drawImage(element, 0, 0, 8, 8);
|
||||
const data = ctx.getImageData(0, 0, 8, 8).data;
|
||||
let hash = 0;
|
||||
for (let i = 0; i < data.length; i++) hash = (hash * 31 + data[i]) >>> 0;
|
||||
@@ -980,9 +984,9 @@
|
||||
const opacity = round(opacityChain(element));
|
||||
return `${rect.left},${rect.top},${rect.width},${rect.height},${opacity}`;
|
||||
});
|
||||
for (const canvas of root.querySelectorAll("canvas")) {
|
||||
if (!isVisibleElement(canvas)) continue;
|
||||
parts.push(`c:${canvasPixelHash(canvas)}`);
|
||||
for (const media of root.querySelectorAll("canvas, video")) {
|
||||
if (!isVisibleElement(media)) continue;
|
||||
parts.push(`p:${mediaPixelHash(media)}`);
|
||||
}
|
||||
return parts.join("|");
|
||||
};
|
||||
|
||||
@@ -31,6 +31,38 @@ describe("layout-audit.browser", () => {
|
||||
clearGeometryCollector();
|
||||
});
|
||||
|
||||
it("changes the sweep fingerprint when visible video pixels advance", () => {
|
||||
document.body.innerHTML = `
|
||||
<div id="root" data-composition-id="main" data-width="640" data-height="360">
|
||||
<video id="footage"></video>
|
||||
</div>
|
||||
`;
|
||||
installGeometry({
|
||||
root: rect({ left: 0, top: 0, width: 640, height: 360 }),
|
||||
footage: rect({ left: 0, top: 0, width: 640, height: 360 }),
|
||||
});
|
||||
|
||||
let pixelValue = 20;
|
||||
const getContextSpy = vi.spyOn(HTMLCanvasElement.prototype, "getContext") as unknown as {
|
||||
mockReturnValue(value: CanvasRenderingContext2D): void;
|
||||
};
|
||||
getContextSpy.mockReturnValue({
|
||||
drawImage() {},
|
||||
getImageData() {
|
||||
return { data: new Uint8ClampedArray(8 * 8 * 4).fill(pixelValue) };
|
||||
},
|
||||
} as unknown as CanvasRenderingContext2D);
|
||||
|
||||
installAuditScript();
|
||||
const collect = (window as unknown as { __hyperframesLayoutGeometry: () => string })
|
||||
.__hyperframesLayoutGeometry;
|
||||
const before = collect();
|
||||
pixelValue = 220;
|
||||
const after = collect();
|
||||
|
||||
expect(after).not.toBe(before);
|
||||
});
|
||||
|
||||
it("uses authored canvas dimensions when the root bounding rect is degenerate", () => {
|
||||
document.body.innerHTML = `
|
||||
<div id="root" data-composition-id="main" data-width="640" data-height="360">
|
||||
|
||||
Reference in New Issue
Block a user