mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 12:54:29 +00:00
fix(studio): seek slider respects full document timeline duration
getAdapter() returned the runtime player or GSAP timeline adapter directly when its duration was > 0, even when the document's timeline (computed from sub-composition data-start + data-hf-authored-duration attributes) extended beyond that duration. This capped the seek slider, seek clamping, and sub-composition visibility at the adapter's shorter value. Now each adapter path checks whether the document duration exceeds the adapter's own duration. When it does, the adapter falls through to createStaticSeekPlaybackAdapter which wraps the runtime player with the correct effective duration, allowing seeking and preview across the full timeline range.
This commit is contained in:
@@ -235,9 +235,11 @@ export function StudioApp() {
|
||||
openSourceForSelection: fileManager.openSourceForSelection,
|
||||
selectSidebarTab: (tab: SidebarTab) => leftSidebarRef.current?.selectTab(tab),
|
||||
});
|
||||
|
||||
domEditSelectionBridgeRef.current = domEditSession.domEditSelection;
|
||||
clearDomSelectionRef.current = domEditSession.clearDomSelection;
|
||||
handleDomEditElementDeleteRef.current = domEditSession.handleDomEditElementDelete;
|
||||
|
||||
useCaptionDetection({
|
||||
projectId,
|
||||
activeCompPath,
|
||||
@@ -269,8 +271,10 @@ export function StudioApp() {
|
||||
setConsoleErrors,
|
||||
resetErrors: resetConsoleErrors,
|
||||
} = useConsoleErrorCapture(previewIframe);
|
||||
|
||||
const [globalDragOver, setGlobalDragOver] = useState(false);
|
||||
const dragCounterRef = useRef(0);
|
||||
|
||||
const { syncPreviewTimelineHotkey, syncPreviewHistoryHotkey } = appHotkeys;
|
||||
const handlePreviewIframeRef = useCallback(
|
||||
(iframe: HTMLIFrameElement | null) => {
|
||||
|
||||
@@ -55,11 +55,7 @@ export const PlayerControls = memo(function PlayerControls({
|
||||
}: PlayerControlsProps) {
|
||||
// Subscribe to only the fields we render — each selector prevents cascading re-renders
|
||||
const isPlaying = usePlayerStore((s) => s.isPlaying);
|
||||
const duration = usePlayerStore((s) => {
|
||||
if (s.elements.length === 0) return s.duration;
|
||||
const maxEnd = Math.max(...s.elements.map((el) => el.start + el.duration));
|
||||
return Math.max(s.duration, maxEnd);
|
||||
});
|
||||
const duration = usePlayerStore((s) => s.duration);
|
||||
const timelineReady = usePlayerStore((s) => s.timelineReady);
|
||||
const playbackRate = usePlayerStore((s) => s.playbackRate);
|
||||
const audioMuted = usePlayerStore((s) => s.audioMuted);
|
||||
|
||||
@@ -43,27 +43,6 @@ import {
|
||||
shouldMutePreviewAudio,
|
||||
} from "../lib/timelineIframeHelpers";
|
||||
|
||||
function patchRuntimeClockDuration(win: IframeWindow, targetDuration: number): void {
|
||||
try {
|
||||
const rootEl = win.document?.querySelector("[data-composition-id]");
|
||||
const rootId = rootEl?.getAttribute("data-composition-id");
|
||||
const tl = rootId && win.__timelines?.[rootId];
|
||||
if (tl && typeof tl.duration === "function" && tl.duration() < targetDuration) {
|
||||
const tlWithTo = tl as typeof tl & {
|
||||
to?: (t: object, v: { duration: number }, p: number) => void;
|
||||
};
|
||||
if (typeof tlWithTo.to === "function") {
|
||||
tlWithTo.to({}, { duration: 0 }, targetDuration);
|
||||
}
|
||||
}
|
||||
if (typeof win.__hfForceTimelineRebind === "function") {
|
||||
win.__hfForceTimelineRebind();
|
||||
}
|
||||
} catch {
|
||||
// cross-origin or missing runtime — non-fatal
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Hook
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -120,7 +99,7 @@ export function useTimelinePlayer() {
|
||||
if (
|
||||
Number.isFinite(nextDuration) &&
|
||||
(nextDuration ?? 0) > 0 &&
|
||||
(nextDuration ?? 0) > state.duration
|
||||
nextDuration !== state.duration
|
||||
) {
|
||||
setDuration(nextDuration ?? 0);
|
||||
}
|
||||
@@ -139,58 +118,57 @@ export function useTimelinePlayer() {
|
||||
|
||||
const playerAdapter =
|
||||
win.__player && typeof win.__player.play === "function" ? win.__player : null;
|
||||
if (getAdapterDuration(playerAdapter) > 0) {
|
||||
const docDur = readTimelineDurationFromDocument(iframe?.contentDocument);
|
||||
if (docDur > 0 && docDur > playerAdapter!.getDuration()) {
|
||||
patchRuntimeClockDuration(win, docDur);
|
||||
}
|
||||
const docDuration = readTimelineDurationFromDocument(iframe.contentDocument);
|
||||
const adapterDur = getAdapterDuration(playerAdapter);
|
||||
|
||||
if (adapterDur > 0 && docDuration <= adapterDur) {
|
||||
return playerAdapter;
|
||||
}
|
||||
|
||||
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 (win.__timelines) {
|
||||
const keys = Object.keys(win.__timelines);
|
||||
if (keys.length > 0) {
|
||||
// Resolve the root composition id from the DOM — the outermost
|
||||
// `[data-composition-id]` element is the master. Without this,
|
||||
// Object.keys() order would let a sub-composition's timeline
|
||||
// hijack play/pause/seek and the duration readout.
|
||||
const rootId = iframe?.contentDocument
|
||||
?.querySelector("[data-composition-id]")
|
||||
?.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;
|
||||
}
|
||||
}
|
||||
|
||||
const fallbackDuration = Math.max(
|
||||
const effectiveDuration = Math.max(
|
||||
usePlayerStore.getState().duration,
|
||||
readTimelineDurationFromDocument(iframe.contentDocument),
|
||||
docDuration,
|
||||
adapterDur,
|
||||
);
|
||||
const baseAdapter = playerAdapter;
|
||||
if (
|
||||
playerAdapter &&
|
||||
fallbackDuration > 0 &&
|
||||
(typeof playerAdapter.renderSeek === "function" || typeof playerAdapter.seek === "function")
|
||||
baseAdapter &&
|
||||
effectiveDuration > 0 &&
|
||||
(typeof baseAdapter.renderSeek === "function" || typeof baseAdapter.seek === "function")
|
||||
) {
|
||||
const cached = staticSeekAdapterRef.current;
|
||||
if (cached?.player === playerAdapter && cached.duration === fallbackDuration) {
|
||||
if (cached?.player === baseAdapter && cached.duration === effectiveDuration) {
|
||||
return cached.adapter;
|
||||
}
|
||||
cached?.adapter.pause();
|
||||
const adapter = createStaticSeekPlaybackAdapter(
|
||||
playerAdapter,
|
||||
fallbackDuration,
|
||||
baseAdapter,
|
||||
effectiveDuration,
|
||||
getDefaultStaticSeekPlaybackClock(win),
|
||||
() => usePlayerStore.getState().playbackRate,
|
||||
);
|
||||
staticSeekAdapterRef.current = {
|
||||
player: playerAdapter,
|
||||
duration: fallbackDuration,
|
||||
player: baseAdapter,
|
||||
duration: effectiveDuration,
|
||||
adapter,
|
||||
};
|
||||
return adapter;
|
||||
@@ -382,13 +360,7 @@ export function useTimelinePlayer() {
|
||||
pendingSeekRef.current = Math.max(0, time);
|
||||
return false;
|
||||
}
|
||||
const adapterDur = adapter.getDuration();
|
||||
const state = usePlayerStore.getState();
|
||||
const maxEnd =
|
||||
state.elements.length > 0
|
||||
? Math.max(...state.elements.map((el) => el.start + el.duration))
|
||||
: 0;
|
||||
const duration = Math.max(0, adapterDur, maxEnd);
|
||||
const duration = Math.max(0, adapter.getDuration());
|
||||
const nextTime = Math.max(0, duration > 0 ? Math.min(duration, time) : time);
|
||||
adapter.seek(nextTime, options);
|
||||
liveTime.notify(nextTime); // Direct DOM updates (playhead, timecode, progress) — no re-render
|
||||
|
||||
@@ -163,12 +163,11 @@ export function useTimelineSyncCallbacks({
|
||||
// with the initial adapter seek on iframe load.
|
||||
liveTime.notify(startTime);
|
||||
const adapterDur = adapter.getDuration();
|
||||
const storeDur = usePlayerStore.getState().duration;
|
||||
if (
|
||||
Number.isFinite(adapterDur) &&
|
||||
adapterDur > 0 &&
|
||||
adapterDur < 7200 &&
|
||||
adapterDur > storeDur
|
||||
adapterDur !== usePlayerStore.getState().duration
|
||||
) {
|
||||
setDuration(adapterDur);
|
||||
}
|
||||
|
||||
@@ -57,5 +57,4 @@ export type IframeWindow = Window & {
|
||||
__timeline?: TimelineLike;
|
||||
__timelines?: Record<string, TimelineLike>;
|
||||
__clipManifest?: ClipManifest;
|
||||
__hfForceTimelineRebind?: () => void;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user