Merge pull request #2681 from heygen-com/07-21-feat_engine_warn_on_live_map_viewports_at_capture_init

feat(engine): warn when a live map viewport is detected at capture init
This commit is contained in:
Vance Ingalls
2026-07-21 14:07:43 -07:00
committed by GitHub
3 changed files with 75 additions and 1 deletions
@@ -0,0 +1,24 @@
import { describe, expect, it } from "vitest";
import { buildLiveMapWarning } from "./frameCapture.js";
describe("buildLiveMapWarning", () => {
it("names every detected map library in the message and details", () => {
const warning = buildLiveMapWarning(["Leaflet", "MapLibre GL"]);
expect(warning.code).toBe("live_map_detected");
expect(warning.message).toContain("Leaflet, MapLibre GL");
expect(warning.details?.sources).toEqual(["Leaflet", "MapLibre GL"]);
});
it("points the author at the bake pipeline instead of just describing the failure", () => {
const warning = buildLiveMapWarning(["Leaflet"]);
expect(warning.message).toContain("bake-basemap.mjs");
expect(warning.message).toContain("deterministic-render");
});
it("copies the libraries array so later mutation cannot alter the recorded warning", () => {
const libraries = ["Leaflet"];
const warning = buildLiveMapWarning(libraries);
libraries.push("Mapbox GL");
expect(warning.details?.sources).toEqual(["Leaflet"]);
});
});
@@ -1716,6 +1716,53 @@ function recordSubTimelineWarning(session: CaptureSession, timeoutMs: number): v
]);
}
/**
* Runtime-mounted map-viewport markers, keyed by the CSS selector each map
* library stamps on its live container. Selector presence means an actual map
* INSTANCE is rendering in the page — not merely that a map library script
* loaded — which keeps false positives low (a baked map video carries none of
* these).
*/
const LIVE_MAP_MARKERS: ReadonlyArray<{ selector: string; library: string }> = [
{ selector: ".leaflet-container", library: "Leaflet" },
{ selector: ".maplibregl-map", library: "MapLibre GL" },
{ selector: ".mapboxgl-map", library: "Mapbox GL" },
{ selector: ".gm-style", library: "Google Maps" },
{ selector: ".ol-viewport", library: "OpenLayers" },
];
/**
* Build the `live_map_detected` warning for the detected map libraries.
* A live tile map violates the deterministic-render contract (tiles are
* render-time network fetches): none of the readiness polls cover late-added
* tile images or map canvases, so frames captured before tiles arrive ship a
* blank/partial map with no error — the render exits success. Wild signature:
* PRINFRA-300 (blank hook scene, zero diagnostics, "fixed" by re-render).
*/
export function buildLiveMapWarning(libraries: readonly string[]): CaptureWarning {
return {
code: "live_map_detected",
message:
`Live map viewport(s) detected in the composition (${libraries.join(", ")}). ` +
`Map tiles load over the network at render time, which the deterministic-render ` +
`contract forbids — frames captured before tiles arrive ship a blank or partial map ` +
`with no error. Bake the map to a video first (see the motion-graphics maps skill / ` +
`bake-basemap.mjs) and use the baked file as the imagery layer.`,
details: { sources: [...libraries] },
};
}
/** Detect live map viewports in the page and record the warning. */
async function recordLiveMapWarning(session: CaptureSession, page: Page): Promise<void> {
const libraries = (await page.evaluate(
(markers: ReadonlyArray<{ selector: string; library: string }>) =>
markers.filter((m) => document.querySelector(m.selector) !== null).map((m) => m.library),
LIVE_MAP_MARKERS,
)) as string[];
if (libraries.length === 0) return;
recordCaptureWarnings(session, [buildLiveMapWarning(libraries)]);
}
// Force every successfully-loaded `<img>` to be GPU-uploaded before the first
// frame capture. `naturalWidth > 0` means the bitmap has been decoded into
// CPU memory, but compositor-side GPU upload can still happen lazily on first
@@ -2002,6 +2049,7 @@ export async function initializeSession(session: CaptureSession): Promise<void>
session,
await collectMediaReadinessWarnings(page, skipVideoIds, pageReadyTimeout),
);
await recordLiveMapWarning(session, page);
await recordSessionInitTelemetry(session, initStart);
@@ -2164,6 +2212,7 @@ export async function initializeSession(session: CaptureSession): Promise<void>
session,
await collectMediaReadinessWarnings(page, bfSkipVideoIds, pageReadyTimeout),
);
await recordLiveMapWarning(session, page);
await recordSessionInitTelemetry(session, initStart);
+2 -1
View File
@@ -18,7 +18,8 @@ export type CaptureWarningCode =
| "media_load_failed"
| "audio_processing_failed"
| "sub_timeline_readiness_timeout"
| "sub_timeline_script_failure";
| "sub_timeline_script_failure"
| "live_map_detected";
/** Structured correctness warning produced while preparing a capture session. */
export interface CaptureWarning {