diff --git a/packages/studio/src/App.tsx b/packages/studio/src/App.tsx index 1c6fd8e79..1cf24b2c5 100644 --- a/packages/studio/src/App.tsx +++ b/packages/studio/src/App.tsx @@ -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) => { diff --git a/packages/studio/src/player/components/PlayerControls.tsx b/packages/studio/src/player/components/PlayerControls.tsx index ef57ca457..c528cffb8 100644 --- a/packages/studio/src/player/components/PlayerControls.tsx +++ b/packages/studio/src/player/components/PlayerControls.tsx @@ -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); diff --git a/packages/studio/src/player/hooks/useTimelinePlayer.ts b/packages/studio/src/player/hooks/useTimelinePlayer.ts index ecd923389..5ab8b87e7 100644 --- a/packages/studio/src/player/hooks/useTimelinePlayer.ts +++ b/packages/studio/src/player/hooks/useTimelinePlayer.ts @@ -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 diff --git a/packages/studio/src/player/hooks/useTimelineSyncCallbacks.ts b/packages/studio/src/player/hooks/useTimelineSyncCallbacks.ts index 7a55ec01d..54c28732f 100644 --- a/packages/studio/src/player/hooks/useTimelineSyncCallbacks.ts +++ b/packages/studio/src/player/hooks/useTimelineSyncCallbacks.ts @@ -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); } diff --git a/packages/studio/src/player/lib/playbackTypes.ts b/packages/studio/src/player/lib/playbackTypes.ts index 540933465..e86b67667 100644 --- a/packages/studio/src/player/lib/playbackTypes.ts +++ b/packages/studio/src/player/lib/playbackTypes.ts @@ -57,5 +57,4 @@ export type IframeWindow = Window & { __timeline?: TimelineLike; __timelines?: Record; __clipManifest?: ClipManifest; - __hfForceTimelineRebind?: () => void; };