mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-10 22:20:14 +00:00
fix(engine): keep BeginFrame time monotonic across frame rates (#3011)
Fixes #3012 ## What Fixes `HeadlessExperimental.beginFrame` hanging at 60fps and other high frame rates. ## Why Warmup always advances Chrome in 33ms steps, but the first capture timestamp was calculated from the output fps. At 60fps, the last warmup tick is 1947ms, while the next commit tick used to jump back to about 1067ms. Chrome sees time moving backwards and `beginFrame` can stall. ## How Keep the old timestamps when they are already safe. If the commit tick would go backwards, move the capture baseline past warmup first. The init commit and both producer probes now use the same timestamp helpers, so they cannot calculate different values. ## Test plan - [x] Added tests for 24, 30, 31, 32, 33, 60, 120, 240, and 59.94fps - [x] Added coverage for the actual session baseline, commit parameters, and both producer probe paths - [x] Engine tests: 1383 passed, 3 skipped - [x] Producer unit tests passed - [x] Engine and producer typechecks and builds passed - [ ] Manual render test
This commit is contained in:
@@ -51,6 +51,7 @@ import {
|
||||
createCaptureSession,
|
||||
createFrameLookupTable,
|
||||
createVideoFrameInjector,
|
||||
deriveBeginFrameProbeTimeTicks,
|
||||
type EngineConfig,
|
||||
type ExtractedFrames,
|
||||
type FrameLookupTable,
|
||||
@@ -316,7 +317,10 @@ export async function beginFrameSessionNeedsScreenshotFallback(
|
||||
Number(process.env.PRODUCER_BEGINFRAME_PROBE_TIMEOUT_MS) > 0
|
||||
? Number(process.env.PRODUCER_BEGINFRAME_PROBE_TIMEOUT_MS)
|
||||
: 30_000;
|
||||
const probeTick = Math.max(0, session.beginFrameTimeTicks - 5 * session.beginFrameIntervalMs);
|
||||
const probeTick = deriveBeginFrameProbeTimeTicks(
|
||||
session.beginFrameTimeTicks,
|
||||
session.beginFrameIntervalMs,
|
||||
);
|
||||
return !(await probe(session.page, timeoutMs, probeTick, session.beginFrameIntervalMs));
|
||||
}
|
||||
|
||||
|
||||
@@ -46,6 +46,12 @@ let createSessionFailUntilAttempt = 0;
|
||||
let createSessionError: Error | null = null;
|
||||
let closeCaptureSessionCallCount = 0;
|
||||
let probeBeginFrameAlive = true;
|
||||
const beginFrameProbeCalls: Array<{
|
||||
page: unknown;
|
||||
timeoutMs: number;
|
||||
frameTimeTicks: number;
|
||||
intervalMs: number;
|
||||
}> = [];
|
||||
const createdSessions: MockSession[] = [];
|
||||
const closedSessions: MockSession[] = [];
|
||||
const durationProbeSessions: MockSession[] = [];
|
||||
@@ -59,6 +65,7 @@ function resetRetryMocks() {
|
||||
createSessionError = null;
|
||||
closeCaptureSessionCallCount = 0;
|
||||
probeBeginFrameAlive = true;
|
||||
beginFrameProbeCalls.length = 0;
|
||||
createdSessions.length = 0;
|
||||
closedSessions.length = 0;
|
||||
durationProbeSessions.length = 0;
|
||||
@@ -137,7 +144,15 @@ mock.module("@hyperframes/engine", () => ({
|
||||
closeCaptureSessionCallCount++;
|
||||
closedSessions.push(session);
|
||||
},
|
||||
probeBeginFrameLiveness: async () => probeBeginFrameAlive,
|
||||
probeBeginFrameLiveness: async (
|
||||
page: unknown,
|
||||
timeoutMs: number,
|
||||
frameTimeTicks: number,
|
||||
intervalMs: number,
|
||||
) => {
|
||||
beginFrameProbeCalls.push({ page, timeoutMs, frameTimeTicks, intervalMs });
|
||||
return probeBeginFrameAlive;
|
||||
},
|
||||
// Mirror of the real engine classifier. Canonical tests + pattern list
|
||||
// live in frameCapture-transientErrors.test.ts — update both if patterns change.
|
||||
isTransientBrowserError: (error: unknown) => {
|
||||
@@ -678,6 +693,14 @@ describe("runProbeStage — transient browser error retry (#1687)", () => {
|
||||
expect(createSessionCallCount).toBe(2);
|
||||
expect(closedSessions).toEqual([createdSessions[0]]);
|
||||
expect(capturedCfgs[1]).toMatchObject({ forceScreenshot: true });
|
||||
expect(beginFrameProbeCalls).toEqual([
|
||||
{
|
||||
page: createdSessions[0]?.page,
|
||||
timeoutMs: 30_000,
|
||||
frameTimeTicks: 95,
|
||||
intervalMs: 1,
|
||||
},
|
||||
]);
|
||||
expect(durationProbeSessions).toEqual([createdSessions[1]]);
|
||||
expect(result.probeSession).toBe(createdSessions[1]);
|
||||
expect(result.beginFrameStalled).toBe(true);
|
||||
|
||||
@@ -36,6 +36,7 @@ import {
|
||||
type EngineConfig,
|
||||
closeCaptureSession,
|
||||
createCaptureSession,
|
||||
deriveBeginFrameProbeTimeTicks,
|
||||
getCompositionDuration,
|
||||
initializeSession,
|
||||
isTransientBrowserError,
|
||||
@@ -388,9 +389,9 @@ export async function runProbeStage(input: ProbeStageInput): Promise<ProbeStageR
|
||||
const livenessStart = Date.now();
|
||||
// Tick inside the post-warmup cushion: warmup < probe < first capture
|
||||
// keeps the session's BeginFrame frameTimeTicks monotonic.
|
||||
const probeTick = Math.max(
|
||||
0,
|
||||
probeSession.beginFrameTimeTicks - 5 * probeSession.beginFrameIntervalMs,
|
||||
const probeTick = deriveBeginFrameProbeTimeTicks(
|
||||
probeSession.beginFrameTimeTicks,
|
||||
probeSession.beginFrameIntervalMs,
|
||||
);
|
||||
const alive = await probeBeginFrameLiveness(
|
||||
probeSession.page,
|
||||
|
||||
Reference in New Issue
Block a user