diff --git a/.filesize-allowlist b/.filesize-allowlist
index b4dbc51b4..c4845eb51 100644
--- a/.filesize-allowlist
+++ b/.filesize-allowlist
@@ -2,4 +2,5 @@ packages/studio/src/player/hooks/useTimelinePlayer.ts
packages/studio/src/hooks/useManifestPersistence.ts
packages/studio/src/player/components/PlayerControls.tsx
packages/studio/src/components/editor/manualEdits.test.ts
+packages/studio/src/player/hooks/useTimelinePlayer.test.ts
packages/studio/src/components/editor/manualEditsDom.ts
diff --git a/packages/studio/src/player/hooks/useTimelinePlayer.test.ts b/packages/studio/src/player/hooks/useTimelinePlayer.test.ts
index 672bc576a..f83ea6ef4 100644
--- a/packages/studio/src/player/hooks/useTimelinePlayer.test.ts
+++ b/packages/studio/src/player/hooks/useTimelinePlayer.test.ts
@@ -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(`
+
+ `);
+
+ expect(readTimelineDurationFromDocument(doc)).toBe(70);
+ });
+
+ it("picks the larger of data-duration and data-hf-authored-duration children", () => {
+ const doc = createDocument(`
+
+ `);
+
+ 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", () => {
diff --git a/packages/studio/src/player/hooks/useTimelinePlayer.ts b/packages/studio/src/player/hooks/useTimelinePlayer.ts
index 4614b3936..3c9db97d6 100644
--- a/packages/studio/src/player/hooks/useTimelinePlayer.ts
+++ b/packages/studio/src/player/hooks/useTimelinePlayer.ts
@@ -59,7 +59,7 @@ export function useTimelinePlayer() {
const iframeShortcutCleanupRef = useRef<(() => void) | null>(null);
const lastTimelineMessageRef = useRef(0);
const staticSeekAdapterRef = useRef<{
- player: RuntimePlaybackAdapter;
+ player: RuntimePlaybackAdapter | PlaybackAdapter;
duration: number;
adapter: PlaybackAdapter;
} | null>(null);
@@ -125,10 +125,12 @@ export function useTimelinePlayer() {
return playerAdapter;
}
+ let timelineAdapter: PlaybackAdapter | null = null;
if (win.__timeline) {
const adapter = wrapTimeline(win.__timeline);
const dur = getAdapterDuration(adapter);
if (dur > 0 && docDuration <= dur) return adapter;
+ if (dur > 0) timelineAdapter ??= adapter;
}
if (win.__timelines) {
@@ -145,40 +147,44 @@ export function useTimelinePlayer() {
const adapter = wrapTimeline(win.__timelines[key]);
const dur = getAdapterDuration(adapter);
if (dur > 0 && docDuration <= dur) return adapter;
+ if (dur > 0) timelineAdapter ??= adapter;
}
}
+ // 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,
docDuration,
adapterDur,
);
- const baseAdapter = playerAdapter;
if (
- baseAdapter &&
+ bestAdapter &&
effectiveDuration > 0 &&
- (typeof baseAdapter.renderSeek === "function" || typeof baseAdapter.seek === "function")
+ ("renderSeek" in bestAdapter || typeof bestAdapter.seek === "function")
) {
const cached = staticSeekAdapterRef.current;
- if (cached?.player === baseAdapter && cached.duration === effectiveDuration) {
+ if (cached?.player === bestAdapter && cached.duration === effectiveDuration) {
return cached.adapter;
}
cached?.adapter.pause();
const adapter = createStaticSeekPlaybackAdapter(
- baseAdapter,
+ bestAdapter,
effectiveDuration,
getDefaultStaticSeekPlaybackClock(win),
() => usePlayerStore.getState().playbackRate,
);
staticSeekAdapterRef.current = {
- player: baseAdapter,
+ 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;