mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 04:38:33 +00:00
fix(core): remove 1800s hard cap on timeline duration that silently truncated long compositions (#1114)
The runtime had a maxTimelineDurationSeconds field defaulting to 1800 (30 minutes) that clamped the TransportClock duration. Any seek beyond this cap was silently clamped, so GSAP tweens starting past ~1700s never received their totalTime() call and stayed at their pre-tween state (e.g. opacity:0). The data-duration attribute is the authored source of truth. The loop- inflation guard (timelineLooksLoopInflated) already handles the infinite repeat:-1 case this cap was meant to protect against. Closes #1107
This commit is contained in:
@@ -538,8 +538,7 @@ export function initSandboxRuntimeModular(): void {
|
||||
} else {
|
||||
safeDuration = fallbackDuration;
|
||||
}
|
||||
const hardDurationCap = Math.max(1, Number(state.maxTimelineDurationSeconds) || 1800);
|
||||
return safeDuration > 0 ? Math.max(0, Math.min(safeDuration, hardDurationCap)) : 0;
|
||||
return safeDuration > 0 ? Math.max(0, safeDuration) : 0;
|
||||
};
|
||||
|
||||
const resolveRootTimelineFromDocument = (): TimelineResolution => {
|
||||
@@ -1423,7 +1422,6 @@ export function initSandboxRuntimeModular(): void {
|
||||
bindRootTimelineIfAvailable();
|
||||
const payload = collectRuntimeTimelinePayload({
|
||||
canonicalFps: state.canonicalFps,
|
||||
maxTimelineDurationSeconds: state.maxTimelineDurationSeconds,
|
||||
});
|
||||
window.__clipManifest = payload;
|
||||
postRuntimeMessage(payload);
|
||||
|
||||
@@ -12,7 +12,6 @@ describe("createRuntimeState", () => {
|
||||
expect(state.capturedTimeline).toBeNull();
|
||||
expect(state.rafId).toBeNull();
|
||||
expect(state.tornDown).toBe(false);
|
||||
expect(state.maxTimelineDurationSeconds).toBe(1800);
|
||||
expect(state.parityModeEnabled).toBe(true);
|
||||
});
|
||||
|
||||
|
||||
@@ -72,7 +72,6 @@ export type RuntimeState = {
|
||||
cachedVideoClips: RuntimeMediaClip[];
|
||||
cachedMediaTimelineDurationSeconds: number;
|
||||
tornDown: boolean;
|
||||
maxTimelineDurationSeconds: number;
|
||||
nativeVisualWatchdogTick: number;
|
||||
/**
|
||||
* Single-clock transport. The sole time authority — GSAP is always
|
||||
@@ -115,7 +114,6 @@ export function createRuntimeState(): RuntimeState {
|
||||
cachedVideoClips: [],
|
||||
cachedMediaTimelineDurationSeconds: 0,
|
||||
tornDown: false,
|
||||
maxTimelineDurationSeconds: 1800,
|
||||
nativeVisualWatchdogTick: 0,
|
||||
transportClock: null,
|
||||
transportRafId: null,
|
||||
|
||||
@@ -7,7 +7,7 @@ describe("collectRuntimeTimelinePayload", () => {
|
||||
delete (window as any).__timelines;
|
||||
});
|
||||
|
||||
const defaultParams = { canonicalFps: 30, maxTimelineDurationSeconds: 1800 };
|
||||
const defaultParams = { canonicalFps: 30 };
|
||||
|
||||
it("returns minimal payload for empty document", () => {
|
||||
const result = collectRuntimeTimelinePayload(defaultParams);
|
||||
@@ -214,7 +214,7 @@ describe("collectRuntimeTimelinePayload", () => {
|
||||
expect(result.durationInFrames).toBe(210); // 7s * 30fps
|
||||
});
|
||||
|
||||
it("clamps duration to maxTimelineDurationSeconds", () => {
|
||||
it("respects long composition durations without capping", () => {
|
||||
const root = document.createElement("div");
|
||||
root.setAttribute("data-composition-id", "main");
|
||||
root.setAttribute("data-duration", "5000");
|
||||
@@ -225,11 +225,8 @@ describe("collectRuntimeTimelinePayload", () => {
|
||||
clip.setAttribute("data-duration", "5000");
|
||||
root.appendChild(clip);
|
||||
|
||||
const result = collectRuntimeTimelinePayload({
|
||||
canonicalFps: 30,
|
||||
maxTimelineDurationSeconds: 60,
|
||||
});
|
||||
expect(result.durationInFrames).toBeLessThanOrEqual(60 * 30);
|
||||
const result = collectRuntimeTimelinePayload({ canonicalFps: 30 });
|
||||
expect(result.durationInFrames).toBe(5000 * 30);
|
||||
});
|
||||
|
||||
it("skips script/style/meta nodes", () => {
|
||||
|
||||
@@ -176,7 +176,6 @@ function buildTimelineClipLabel(node: Element, kind: RuntimeTimelineClip["kind"]
|
||||
|
||||
export function collectRuntimeTimelinePayload(params: {
|
||||
canonicalFps: number;
|
||||
maxTimelineDurationSeconds: number;
|
||||
}): RuntimeTimelineMessage {
|
||||
const runtimeWindow = window as Window & {
|
||||
__timelines?: Record<string, RuntimeTimelineLike | undefined>;
|
||||
@@ -347,10 +346,7 @@ export function collectRuntimeTimelinePayload(params: {
|
||||
mediaWindowDurationCandidate,
|
||||
compositionWindowDurationCandidate,
|
||||
));
|
||||
const rootCompositionDuration =
|
||||
preferredRootDuration != null
|
||||
? Math.min(preferredRootDuration, params.maxTimelineDurationSeconds)
|
||||
: null;
|
||||
const rootCompositionDuration = preferredRootDuration ?? null;
|
||||
const rootCompositionEnd =
|
||||
rootCompositionDuration != null ? rootCompositionStart + rootCompositionDuration : null;
|
||||
const timelineWindowEnd =
|
||||
@@ -666,13 +662,7 @@ export function collectRuntimeTimelinePayload(params: {
|
||||
// hide structural/background tracks from the timeline UI; if we collapse the
|
||||
// payload duration down to the last visible clip end, the controls jump even
|
||||
// though playback still runs for the full authored root duration.
|
||||
const safeDuration = Math.max(
|
||||
1,
|
||||
Math.min(
|
||||
Math.max(maxEnd || 1, rootCompositionDuration ?? 0),
|
||||
params.maxTimelineDurationSeconds,
|
||||
),
|
||||
);
|
||||
const safeDuration = Math.max(1, maxEnd || 1, rootCompositionDuration ?? 0);
|
||||
const shouldEmitNonDeterministicInf = timelineLooksLoopInflated && attrDurationCandidate == null;
|
||||
const durationInFrames = shouldEmitNonDeterministicInf
|
||||
? Number.POSITIVE_INFINITY
|
||||
|
||||
Reference in New Issue
Block a user