Merge pull request #1826 from heygen-com/fix/studio-recent-issues

fix(studio): resolve timeline keyframe, click-selection, and nested-video sync regressions
This commit is contained in:
Miguel Ángel
2026-07-01 22:01:41 -07:00
committed by GitHub
34 changed files with 1483 additions and 88 deletions
@@ -100,6 +100,18 @@ describe("applySoftReload", () => {
expect(contentWindow.__hfStudioManualEditsApply).toHaveBeenCalled();
});
it("seeks to the caller-supplied currentTime override instead of the iframe's own __player.getTime()", () => {
// Regression: the iframe's raw __player.getTime() (2.0 here, per the mock)
// can desync from the studio's authoritative scrub position — e.g. a
// keyframe-node drag parks the playhead via the store before this reload's
// async commit resolves. The rebuilt timeline must re-seek to the caller's
// value, not the iframe's possibly-stale one.
const { iframe, contentWindow } = buildMockIframe();
const result = applySoftReload(iframe, SCRIPT_TEXT, undefined, 0);
expect(result).toBe("applied");
expect(contentWindow.__player.seek).toHaveBeenCalledWith(0);
});
it("strips a stale inline transform from an orphaned (non-timeline-child) element", () => {
// Repro: an element dragged via gsap.set whose keyframes were then removed is
// no longer a timeline child, so the timeline-children sweep misses it. Its
+15 -2
View File
@@ -175,6 +175,7 @@ export function applySoftReload(
iframe: HTMLIFrameElement | null,
scriptText: string,
onAsyncFailure?: () => void,
currentTimeOverride?: number,
): SoftReloadResult {
if (!iframe || !scriptText) return "cannot-soft-reload";
@@ -210,7 +211,14 @@ export function applySoftReload(
// rather than killing the target timeline and appending an orphan script.
if (gsapScripts.length > 1 && staleScripts.length === 0) return "cannot-soft-reload";
const currentTime = win.__player?.getTime?.() ?? 0;
// Prefer the caller-supplied scrub position (the studio's own authoritative
// currentTime, e.g. usePlayerStore) over the iframe's raw `__player.getTime()`:
// the two can desync (a keyframe-node drag parks the playhead via the store
// BEFORE this reload's async commit resolves, and the iframe's own GSAP clock
// doesn't reliably reflect that yet), which re-seeks the freshly rebuilt
// timeline to the wrong frame and leaves the element (and its overlay)
// rendered at a stale/unrelated position.
const currentTime = currentTimeOverride ?? win.__player?.getTime?.() ?? 0;
// Track whether the MotionPath async path was taken. When it is, the script
// executes inside pluginScript.onload — after applySoftReload has already
@@ -300,8 +308,13 @@ export function applySoftReload(
const s = doc.createElement("script");
s.textContent = `(function(){${scriptText}\n})();`;
doc.body.appendChild(s);
win.__hfForceTimelineRebind?.();
// Seek BEFORE rebind: __hfForceTimelineRebind's own internal force-render
// (see init.ts) renders the freshly-created timeline at whatever the
// runtime's internal scrub position already is, not at whatever we pass
// here afterward — a redundant seek() call after rebind can be a GSAP
// no-op if the timeline already reports being at that time internally.
win.__player?.seek?.(currentTime);
win.__hfForceTimelineRebind?.();
win.__hfStudioManualEditsApply?.();
};