mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-12 15:20:13 +00:00
feat(engine): frame-capture core — fast-capture routing, worker-encode, dedup extension (#1919)
* feat(engine): drawElementImage capture service * chore(ci): ignore drawElementService exports pending upstack consumers Fallow's per-PR audit diffs against the merge base, so the bottom of the fast-capture stack (#1917) sees drawElementService's exports as unused — their consumers (frameCapture) land in #1919, two PRs upstack. ignoreExports entry documents this and can be dropped once #1919 merges. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(engine): 3D projection + compositor-effect risk gate * fix(engine): gate filter drop-shadow wherever blur gates (review) detectCssEffectRisk documented drop-shadow as a ~29dB damage case but only detected blur( in its three scan paths — a drop-shadow comp stayed on the fast path despite the gate's own correctness contract. Detect drop-shadow( in computed styles, stylesheet rules, and GSAP tween vars, pinned by a focused test that runs the real page-side closure against a DOM shim (computed / stylesheet / tween coverage + blur regression + effect-free null). Addresses miguel-heygen's blocker on #1918. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(engine): frame-capture core — fast-capture routing, worker-encode, dedup extension # Conflicts: # packages/engine/src/services/screenshotService.ts * fix(engine): document HF_FORCE_DRAWELEMENT as diagnostic-only; make armStaticDedup idempotent (review) Addresses miguel-heygen's blockers on #1919: - HF_FORCE_DRAWELEMENT promoted from a stale "SCRATCH/Uncommitted" comment to a documented diagnostic flag: it exists for upstream-Chromium repro work (gate-vs-API isolation, crbug 521861819 149-vs-151) and R&D on gated effect classes; renders under it may be damaged BY DESIGN since it bypasses gates whose thresholds encode measured damage. Never production; the safety-net blank guard also stands down under it so diagnostic frames arrive unmodified. - armStaticDedup is now idempotent: the drawElement init path arms dedup before canvas injection, then initializeSession called it again — the second run overwrote the armed state with skipReason="capture_mode" (captureMode is "drawelement" by then), producing contradictory telemetry (armed frames + a skip reason), and re-ran the verification seeks on the fallback path. It now no-ops once staticFrames or a skip decision exists. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
4749fe5716
commit
0e58344dca
@@ -1,3 +1,4 @@
|
||||
// fallow-ignore-file code-duplication complexity
|
||||
/**
|
||||
* Screenshot Service
|
||||
*
|
||||
@@ -43,6 +44,55 @@ export interface BeginFrameResult {
|
||||
hasDamage: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Issue a single no-output BeginFrame and race it against `timeoutMs`.
|
||||
*
|
||||
* On SwiftShader, compositions with many promoted layers (multi-group nested
|
||||
* opacity caption animations) can stall the FIRST BeginFrame indefinitely —
|
||||
* tested to 30 minutes without completion (style-7/8/10/15-prod). The
|
||||
* auto-worker calibration path catches this with its own capped protocol
|
||||
* timeout, but renders with an explicit `--workers N` skip calibration and
|
||||
* would hang for the full protocol timeout (and never succeed). This probe
|
||||
* gives the producer a cheap liveness signal right after session init:
|
||||
* `false` means route the render through screenshot capture instead.
|
||||
*
|
||||
* Healthy comps complete the probe in well under a second on GPU and within
|
||||
* a few seconds on SwiftShader. A protocol error also resolves `false` —
|
||||
* the safe direction (screenshot capture always works).
|
||||
*/
|
||||
export async function probeBeginFrameLiveness(
|
||||
page: Page,
|
||||
timeoutMs: number,
|
||||
// BeginFrame frameTimeTicks must be monotonic per session. The capture loop
|
||||
// sends `session.beginFrameTimeTicks + frameIndex * interval`, where the
|
||||
// base carries a 10-interval cushion above the warmup loop's last tick —
|
||||
// callers probing an initialized session should pass a tick INSIDE that
|
||||
// cushion (e.g. base − 5·interval) so warmup < probe < first capture stays
|
||||
// monotonic. Omit both params only for a session that will not issue
|
||||
// further BeginFrames.
|
||||
frameTimeTicks?: number,
|
||||
intervalMs?: number,
|
||||
): Promise<boolean> {
|
||||
const client = await getCdpSession(page);
|
||||
const params: { frameTimeTicks?: number; interval?: number } = {};
|
||||
if (typeof frameTimeTicks === "number") params.frameTimeTicks = frameTimeTicks;
|
||||
if (typeof intervalMs === "number") params.interval = intervalMs;
|
||||
let timer: ReturnType<typeof setTimeout> | undefined;
|
||||
try {
|
||||
return await Promise.race([
|
||||
client
|
||||
.send("HeadlessExperimental.beginFrame", params)
|
||||
.then(() => true)
|
||||
.catch(() => false),
|
||||
new Promise<boolean>((resolve) => {
|
||||
timer = setTimeout(() => resolve(false), timeoutMs);
|
||||
}),
|
||||
]);
|
||||
} finally {
|
||||
if (timer) clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Capture a frame using HeadlessExperimental.beginFrame.
|
||||
*
|
||||
@@ -545,6 +595,7 @@ export async function injectVideoFramesBatch(
|
||||
// shorter than the host's authored data-duration, where the runtime
|
||||
// truncates visibility but the replacement <img> must hold its last
|
||||
// frame) — those must NOT be skipped here.
|
||||
// fallow-ignore-next-line code-duplication
|
||||
const isVisualAncestorHidden = (el: HTMLElement): boolean => {
|
||||
let parent = el.parentElement;
|
||||
while (parent !== null && parent !== document.documentElement) {
|
||||
|
||||
Reference in New Issue
Block a user