diff --git a/packages/core/src/runtime/timeline.test.ts b/packages/core/src/runtime/timeline.test.ts index bc3a1a093..8ee1eca9c 100644 --- a/packages/core/src/runtime/timeline.test.ts +++ b/packages/core/src/runtime/timeline.test.ts @@ -183,6 +183,21 @@ describe("collectRuntimeTimelinePayload", () => { expect(result.durationInFrames).toBe(300); // 10s * 30fps }); + it("ceil durationInFrames to match render frame count", () => { + const root = document.createElement("div"); + root.setAttribute("data-composition-id", "main"); + root.setAttribute("data-duration", "1.01"); + document.body.appendChild(root); + + const clip = document.createElement("div"); + clip.setAttribute("data-start", "0"); + clip.setAttribute("data-duration", "1.01"); + root.appendChild(clip); + + const result = collectRuntimeTimelinePayload(defaultParams); + expect(result.durationInFrames).toBe(31); // ceil(1.01s * 30fps), same as render. + }); + it("preserves the authored root duration when clips end earlier", () => { const root = document.createElement("div"); root.setAttribute("data-composition-id", "main"); diff --git a/packages/core/src/runtime/timeline.ts b/packages/core/src/runtime/timeline.ts index f8246f2e8..4d598fb04 100644 --- a/packages/core/src/runtime/timeline.ts +++ b/packages/core/src/runtime/timeline.ts @@ -674,7 +674,7 @@ export function collectRuntimeTimelinePayload(params: { const shouldEmitNonDeterministicInf = timelineLooksLoopInflated && attrDurationCandidate == null; const durationInFrames = shouldEmitNonDeterministicInf ? Number.POSITIVE_INFINITY - : Math.max(1, Math.round(safeDuration * Math.max(1, params.canonicalFps))); + : Math.max(1, Math.ceil(safeDuration * Math.max(1, params.canonicalFps))); return { source: "hf-preview", type: "timeline",