mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 04:38:33 +00:00
fix(producer): suppress GSAP call side effects during render seeks (#2037)
* fix(producer): suppress GSAP call side effects during render seeks * fix(core): preserve GSAP root render nudge safely
This commit is contained in:
@@ -147,4 +147,40 @@ describe("verifyStaticFramesSafe catches drift the old fixed-point density would
|
||||
expect(result?.budgetExhausted).toBe(false);
|
||||
expect(result?.badFrame).toBe(changeAt);
|
||||
});
|
||||
|
||||
it("uses silent verification seeks and restores the playhead to frame zero", async () => {
|
||||
const seekCalls: Array<{ t: number; options?: { suppressEvents?: boolean } }> = [];
|
||||
const page = {
|
||||
evaluate: vi.fn(async (fn: (tt: number) => void, t: number) => {
|
||||
const globalWithWindow = globalThis as typeof globalThis & { window?: unknown };
|
||||
const previousWindow = globalWithWindow.window;
|
||||
globalWithWindow.window = {
|
||||
__hf: {
|
||||
seek: (seekTime: number, options?: { suppressEvents?: boolean }) => {
|
||||
seekCalls.push({ t: seekTime, options });
|
||||
},
|
||||
},
|
||||
};
|
||||
try {
|
||||
fn(t);
|
||||
} finally {
|
||||
if (previousWindow === undefined) delete globalWithWindow.window;
|
||||
else globalWithWindow.window = previousWindow;
|
||||
}
|
||||
}),
|
||||
};
|
||||
vi.mocked(pageScreenshotCapture).mockImplementation(async () => Buffer.from("same"));
|
||||
|
||||
const result = await verifyStaticFramesSafe(
|
||||
{ options: {} } as unknown as CaptureSession,
|
||||
page as unknown as Parameters<typeof verifyStaticFramesSafe>[1],
|
||||
new Set([1, 2]),
|
||||
fps,
|
||||
3,
|
||||
);
|
||||
|
||||
expect(result).toBeNull();
|
||||
expect(seekCalls.map((call) => Math.round(call.t * fps))).toEqual([0, 1, 2, 0]);
|
||||
expect(seekCalls.every((call) => call.options?.suppressEvents === true)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2067,12 +2067,19 @@ export async function verifyStaticFramesSafe(
|
||||
if (last && f === last.b + 1) last.b = f;
|
||||
else runs.push({ a: f, b: f });
|
||||
}
|
||||
const seekCapture = async (frameIdx: number): Promise<Buffer> => {
|
||||
const seekToFrame = async (frameIdx: number): Promise<void> => {
|
||||
const t = quantizeTimeToFrame(frameIdx / fps, fps);
|
||||
await page.evaluate((tt: number) => {
|
||||
const hf = (window as unknown as { __hf?: { seek?: (t: number) => void } }).__hf;
|
||||
if (hf && typeof hf.seek === "function") hf.seek(tt);
|
||||
const hf = (
|
||||
window as unknown as {
|
||||
__hf?: { seek?: (t: number, options?: { suppressEvents?: boolean }) => void };
|
||||
}
|
||||
).__hf;
|
||||
if (hf && typeof hf.seek === "function") hf.seek(tt, { suppressEvents: true });
|
||||
}, t);
|
||||
};
|
||||
const seekCapture = async (frameIdx: number): Promise<Buffer> => {
|
||||
await seekToFrame(frameIdx);
|
||||
return pageScreenshotCapture(page, session.options);
|
||||
};
|
||||
// Verify EVERY run in order (no longest-first truncation that would leave runs armed
|
||||
@@ -2093,23 +2100,27 @@ export async function verifyStaticFramesSafe(
|
||||
400,
|
||||
Math.ceil(frames.length / STATIC_VERIFY_REFERENCE_STRIDE) * 3 + runs.length,
|
||||
);
|
||||
let spent = 0;
|
||||
for (const { a, b } of runs) {
|
||||
const anchor = a - 1;
|
||||
if (anchor < 0) continue;
|
||||
const anchorBuf = await seekCapture(anchor);
|
||||
spent++;
|
||||
for (const f of computeStaticVerificationPoints(a, b, sampleCount)) {
|
||||
const cur = await seekCapture(f);
|
||||
try {
|
||||
let spent = 0;
|
||||
for (const { a, b } of runs) {
|
||||
const anchor = a - 1;
|
||||
if (anchor < 0) continue;
|
||||
const anchorBuf = await seekCapture(anchor);
|
||||
spent++;
|
||||
if (!anchorBuf.equals(cur)) return { badFrame: f, budgetExhausted: false };
|
||||
for (const f of computeStaticVerificationPoints(a, b, sampleCount)) {
|
||||
const cur = await seekCapture(f);
|
||||
spent++;
|
||||
if (!anchorBuf.equals(cur)) return { badFrame: f, budgetExhausted: false };
|
||||
}
|
||||
// Budget exhausted → can't fully verify → disarm, distinct from real drift so a
|
||||
// `verification_budget` spike in telemetry reads as "this composition has a lot
|
||||
// of static material to verify," not "compositions are non-static."
|
||||
if (spent > hardCap) return { badFrame: a, budgetExhausted: true };
|
||||
}
|
||||
// Budget exhausted → can't fully verify → disarm, distinct from real drift so a
|
||||
// `verification_budget` spike in telemetry reads as "this composition has a lot
|
||||
// of static material to verify," not "compositions are non-static."
|
||||
if (spent > hardCap) return { badFrame: a, budgetExhausted: true };
|
||||
return null;
|
||||
} finally {
|
||||
await seekToFrame(0).catch(() => {});
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2985,8 +2996,12 @@ async function captureDeVerificationFrames(
|
||||
const fractions = Array.from({ length: k }, (_, i) => (i + 1) / (k + 1));
|
||||
const seekTo = async (t: number): Promise<void> => {
|
||||
await page.evaluate((tt: number) => {
|
||||
const hf = (window as unknown as { __hf?: { seek?: (x: number) => void } }).__hf;
|
||||
if (hf && typeof hf.seek === "function") hf.seek(tt);
|
||||
const hf = (
|
||||
window as unknown as {
|
||||
__hf?: { seek?: (x: number, options?: { suppressEvents?: boolean }) => void };
|
||||
}
|
||||
).__hf;
|
||||
if (hf && typeof hf.seek === "function") hf.seek(tt, { suppressEvents: true });
|
||||
}, t);
|
||||
};
|
||||
await seekTo(quantizeTimeToFrame(0, fps));
|
||||
|
||||
Reference in New Issue
Block a user