Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions packages/studio/src/components/nle/NLELayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ export const NLELayout = memo(function NLELayout({
togglePlay,
seek,
onIframeLoad: baseOnIframeLoad,
saveSeekPosition,
refreshPlayer,
} = useTimelinePlayer();

// Reset timeline state when the project changes
Expand All @@ -109,13 +109,16 @@ export const NLELayout = memo(function NLELayout({
usePlayerStore.getState().reset();
}

// Save seek position before refresh
// Lightweight reload: change iframe src instead of destroying the Player.
// refreshPlayer() saves the seek position and appends a cache-busting _t
// param, avoiding the full web-component teardown + crossfade that the
// key-based path uses.
const prevRefreshKeyRef = useRef(refreshKey);
useEffect(() => {
if (refreshKey === prevRefreshKeyRef.current) return;
prevRefreshKeyRef.current = refreshKey;
saveSeekPosition();
}, [refreshKey, saveSeekPosition]);
refreshPlayer();
}, [refreshKey, refreshPlayer]);

const onIframeLoad = useCallback(() => {
baseOnIframeLoad();
Expand Down
53 changes: 9 additions & 44 deletions packages/studio/src/components/nle/NLEPreview.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { memo, useCallback, useEffect, useRef, useState, type Ref } from "react";
import { memo, useCallback, useEffect, useRef, type Ref } from "react";
import { Player } from "../../player";
import {
DEFAULT_PREVIEW_ZOOM,
Expand Down Expand Up @@ -53,15 +53,14 @@ export const NLEPreview = memo(function NLEPreview({
onCompositionLoadingChange,
portrait,
directUrl,
refreshKey,
suppressLoadingOverlay,
}: NLEPreviewProps) {
const baseKey = getPreviewPlayerKey({ projectId, directUrl, refreshKey });
const prevRefreshKeyRef = useRef(refreshKey);
// Player key only changes for structural changes (project switch, composition
// drill-down), NOT for content refreshes. Content refreshes use the lighter
// iframe.src reload path handled by NLELayout → refreshPlayer().
const activeKey = getPreviewPlayerKey({ projectId, directUrl });
const viewportRef = useRef<HTMLDivElement>(null);
const stageRef = useRef<HTMLDivElement>(null);
const [retiringKey, setRetiringKey] = useState<string | null>(null);
const retiringTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);

const zoomRef = useRef<PreviewZoomState>(loadInitialZoom());
const hudRef = useRef<HTMLDivElement>(null);
Expand All @@ -80,7 +79,6 @@ export const NLEPreview = memo(function NLEPreview({
return () => {
if (settleTimerRef.current) clearTimeout(settleTimerRef.current);
if (hudTimerRef.current) clearTimeout(hudTimerRef.current);
if (retiringTimerRef.current) clearTimeout(retiringTimerRef.current);
};
}, []);

Expand Down Expand Up @@ -130,31 +128,13 @@ export const NLEPreview = memo(function NLEPreview({
[writeTransform],
);

if (refreshKey !== prevRefreshKeyRef.current) {
const oldKey = `${baseKey}:${prevRefreshKeyRef.current ?? 0}`;
prevRefreshKeyRef.current = refreshKey;
setRetiringKey(oldKey);
}

const activeKey = `${baseKey}:${refreshKey ?? 0}`;

const applyInitialZoom = useCallback(() => {
const z = zoomRef.current;
if (Math.abs(z.zoomPercent - 100) > 0.5 || Math.abs(z.panX) > 0.1 || Math.abs(z.panY) > 0.1) {
writeTransform(z);
}
}, [writeTransform]);

const handleNewPlayerLoad = () => {
onIframeLoad();
applyInitialZoom();
if (retiringTimerRef.current) clearTimeout(retiringTimerRef.current);
retiringTimerRef.current = setTimeout(() => {
setRetiringKey(null);
retiringTimerRef.current = null;
}, 160);
};

useEffect(() => {
const viewport = viewportRef.current;
if (!viewport) return;
Expand Down Expand Up @@ -282,32 +262,17 @@ export const NLEPreview = memo(function NLEPreview({
}}
data-testid="preview-zoom-stage"
>
{retiringKey && (
<Player
key={retiringKey}
projectId={directUrl ? undefined : projectId}
directUrl={directUrl}
onLoad={() => {}}
portrait={portrait}
style={{ position: "absolute", inset: 0, zIndex: 0, opacity: 1 }}
/>
)}
<Player
key={activeKey}
ref={iframeRef}
projectId={directUrl ? undefined : projectId}
directUrl={directUrl}
onLoad={
retiringKey
? handleNewPlayerLoad
: () => {
onIframeLoad();
applyInitialZoom();
}
}
onLoad={() => {
onIframeLoad();
applyInitialZoom();
}}
onCompositionLoadingChange={onCompositionLoadingChange}
portrait={portrait}
style={retiringKey ? { position: "absolute", inset: 0, zIndex: 1 } : undefined}
suppressLoadingOverlay={suppressLoadingOverlay}
/>
</div>
Expand Down
8 changes: 7 additions & 1 deletion packages/studio/src/player/components/Player.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -229,13 +229,19 @@ export const Player = forwardRef<HTMLIFrameElement, PlayerProps>(
// data arrives), but the overlay communicates why the first frame
// or first audio beat may lag.
//
// Skip the overlay on subsequent loads (content refreshes via
// refreshPlayer). The browser has already cached the assets from
// the first load, so they resolve near-instantly and the overlay
// just creates a disruptive flash.
//
// Poll with a 10 s safety cap (100 ticks × 100 ms). If the cap
// trips we hide the overlay so the UI doesn't appear stuck forever,
// but we log a debug warning so the case is diagnosable — a long
// cold video or a broken asset can legitimately exceed 10 s on a
// slow network.
if (assetPollRef.current) clearInterval(assetPollRef.current);
let lastUnloaded = hasUnloadedAssets(iframe, false);
const isContentRefresh = loadCountRef.current > 1;
let lastUnloaded = isContentRefresh ? false : hasUnloadedAssets(iframe, false);
if (lastUnloaded) {
setAssetsLoading(true);
let attempts = 0;
Expand Down
Loading