Merge pull request #940 from heygen-com/worktree-fix+seek-slider-duration

fix(studio): seek slider respects effective timeline duration with appended blocks
This commit is contained in:
Miguel Ángel
2026-05-19 00:39:13 +02:00
committed by GitHub
3 changed files with 84 additions and 15 deletions
@@ -106,6 +106,28 @@ describe("readTimelineDurationFromDocument", () => {
expect(readTimelineDurationFromDocument(doc)).toBe(5.5);
});
it("reads data-hf-authored-duration when data-duration is stripped", () => {
const doc = createDocument(`
<div data-composition-id="main">
<div data-composition-id="sub-a" data-start="0" data-hf-authored-duration="8"></div>
<div data-composition-id="sub-b" data-start="60" data-hf-authored-duration="10"></div>
</div>
`);
expect(readTimelineDurationFromDocument(doc)).toBe(70);
});
it("picks the larger of data-duration and data-hf-authored-duration children", () => {
const doc = createDocument(`
<div data-composition-id="main">
<div data-start="0" data-duration="5"></div>
<div data-composition-id="ext" data-start="74" data-hf-authored-duration="8"></div>
</div>
`);
expect(readTimelineDurationFromDocument(doc)).toBe(82);
});
});
describe("createStaticSeekPlaybackAdapter", () => {
@@ -153,6 +175,39 @@ describe("createStaticSeekPlaybackAdapter", () => {
expect(renderedTimes).toEqual([2]);
expect(adapter.getTime()).toBe(2);
});
it("works with a seek-only adapter (no renderSeek)", () => {
const clock = createManualAnimationClock();
const seekedTimes: number[] = [];
const adapter = createStaticSeekPlaybackAdapter(
{
getTime: () => 0,
seek: (time: number) => {
seekedTimes.push(time);
},
},
82,
clock,
);
adapter.seek(77);
expect(seekedTimes).toEqual([77]);
expect(adapter.getTime()).toBe(77);
expect(adapter.getDuration()).toBe(82);
});
it("pauses old adapter before replacing with new duration", () => {
const clock = createManualAnimationClock();
const adapter = createStaticSeekPlaybackAdapter(
{ getTime: () => 0, renderSeek: () => {} },
10,
clock,
);
adapter.play();
expect(adapter.isPlaying()).toBe(true);
adapter.pause();
expect(adapter.isPlaying()).toBe(false);
});
});
describe("buildStandaloneRootTimelineElement", () => {
@@ -59,7 +59,7 @@ export function useTimelinePlayer() {
const iframeShortcutCleanupRef = useRef<(() => void) | null>(null);
const lastTimelineMessageRef = useRef<number>(0);
const staticSeekAdapterRef = useRef<{
player: RuntimePlaybackAdapter;
player: RuntimePlaybackAdapter | PlaybackAdapter;
duration: number;
adapter: PlaybackAdapter;
} | null>(null);
@@ -118,13 +118,19 @@ export function useTimelinePlayer() {
const playerAdapter =
win.__player && typeof win.__player.play === "function" ? win.__player : null;
if (getAdapterDuration(playerAdapter) > 0) {
const docDuration = readTimelineDurationFromDocument(iframe.contentDocument);
const adapterDur = getAdapterDuration(playerAdapter);
if (adapterDur > 0 && docDuration <= adapterDur) {
return playerAdapter;
}
let timelineAdapter: PlaybackAdapter | null = null;
if (win.__timeline) {
const adapter = wrapTimeline(win.__timeline);
if (getAdapterDuration(adapter) > 0) return adapter;
const dur = getAdapterDuration(adapter);
if (dur > 0 && docDuration <= dur) return adapter;
if (dur > 0) timelineAdapter ??= adapter;
}
if (win.__timelines) {
@@ -139,39 +145,46 @@ export function useTimelinePlayer() {
?.getAttribute("data-composition-id");
const key = rootId && rootId in win.__timelines ? rootId : keys[keys.length - 1];
const adapter = wrapTimeline(win.__timelines[key]);
if (getAdapterDuration(adapter) > 0) return adapter;
const dur = getAdapterDuration(adapter);
if (dur > 0 && docDuration <= dur) return adapter;
if (dur > 0) timelineAdapter ??= adapter;
}
}
const fallbackDuration = Math.max(
// The document timeline extends past every native adapter's duration.
// Wrap the best available adapter with the effective duration so the
// seek slider, seek clamping, and duration display cover the full range.
const bestAdapter = playerAdapter ?? timelineAdapter;
const effectiveDuration = Math.max(
usePlayerStore.getState().duration,
readTimelineDurationFromDocument(iframe.contentDocument),
docDuration,
adapterDur,
);
if (
playerAdapter &&
fallbackDuration > 0 &&
(typeof playerAdapter.renderSeek === "function" || typeof playerAdapter.seek === "function")
bestAdapter &&
effectiveDuration > 0 &&
("renderSeek" in bestAdapter || typeof bestAdapter.seek === "function")
) {
const cached = staticSeekAdapterRef.current;
if (cached?.player === playerAdapter && cached.duration === fallbackDuration) {
if (cached?.player === bestAdapter && cached.duration === effectiveDuration) {
return cached.adapter;
}
cached?.adapter.pause();
const adapter = createStaticSeekPlaybackAdapter(
playerAdapter,
fallbackDuration,
bestAdapter,
effectiveDuration,
getDefaultStaticSeekPlaybackClock(win),
() => usePlayerStore.getState().playbackRate,
);
staticSeekAdapterRef.current = {
player: playerAdapter,
duration: fallbackDuration,
player: bestAdapter,
duration: effectiveDuration,
adapter,
};
return adapter;
}
return playerAdapter;
return bestAdapter;
} catch (err) {
console.warn("[useTimelinePlayer] Could not get playback adapter (cross-origin)", err);
return null;