diff --git a/packages/producer/src/services/render/stages/probeStage.test.ts b/packages/producer/src/services/render/stages/probeStage.test.ts index ebab7bd907..8c2084b0ed 100644 --- a/packages/producer/src/services/render/stages/probeStage.test.ts +++ b/packages/producer/src/services/render/stages/probeStage.test.ts @@ -279,6 +279,30 @@ describe("runProbeStage — forceScreenshot threading", () => { }); }); +describe("runProbeStage — decimal duration frame count", () => { + it("does not add a frame for a six-decimal duration rounded from an exact frame boundary", async () => { + const { runProbeStage } = await import("./probeStage.js"); + const input = makeProbeInput({}); + input.composition.duration = 32.866667; + input.compiled.staticDuration = 32.866667; + + const result = await runProbeStage(input); + + expect(result.totalFrames).toBe(986); + }); + + it("still ceilings a duration that genuinely extends into the next frame", async () => { + const { runProbeStage } = await import("./probeStage.js"); + const input = makeProbeInput({}); + input.composition.duration = 32.867; + input.compiled.staticDuration = 32.867; + + const result = await runProbeStage(input); + + expect(result.totalFrames).toBe(987); + }); +}); + describe("runProbeStage — transient browser error retry (#1687)", () => { it("retries once on a transient 'Navigating frame was detached' error and succeeds", async () => { resetRetryMocks(); diff --git a/packages/producer/src/services/render/stages/probeStage.ts b/packages/producer/src/services/render/stages/probeStage.ts index 273c960136..8d31298dbe 100644 --- a/packages/producer/src/services/render/stages/probeStage.ts +++ b/packages/producer/src/services/render/stages/probeStage.ts @@ -83,6 +83,16 @@ export interface ProbeStageInput { deviceScaleFactor: number; } +const FRAME_BOUNDARY_EPSILON = 1e-3; + +function durationToFrameCount(duration: number, fps: number): number { + const rawFrameCount = duration * fps; + const nearestFrame = Math.round(rawFrameCount); + return Math.abs(rawFrameCount - nearestFrame) <= FRAME_BOUNDARY_EPSILON + ? nearestFrame + : Math.ceil(rawFrameCount); +} + export interface ProbeStageResult { /** May be reassigned from `recompileWithResolutions`. */ compiled: CompiledComposition; @@ -536,7 +546,7 @@ export async function runProbeStage(input: ProbeStageInput): Promise