Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
3 changes: 2 additions & 1 deletion packages/studio/src/components/nle/NLEPreview.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import React, { act, createRef } from "react";
import { createRoot } from "react-dom/client";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { useTimelinePlayer } from "../../player/hooks/useTimelinePlayer";
import { NLEPreview, getPreviewPlayerKey, resolvePreviewStageSize } from "./NLEPreview";
import { NLEPreview, getPreviewPlayerKey } from "./NLEPreview";
import { resolvePreviewStageSize } from "./previewStage";
import { readPreviewComplexity } from "../../player/hooks/usePreviewFirstFrameTelemetry";

globalThis.IS_REACT_ACT_ENVIRONMENT = true;
Expand Down
50 changes: 5 additions & 45 deletions packages/studio/src/components/nle/NLEPreview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
} from "./previewZoom";
import { RULER_GUTTER_PX, usePreviewGuidesStore } from "../editor/previewGuidesStore";
import { PreviewZoomOverlay, usePreviewNavigator } from "./PreviewZoomOverlay";
import { PREVIEW_STAGE_INSET_PX, resolvePreviewStageSize, SHADOW_IFRAME_STYLE } from "./previewStage";
import { usePreviewFirstFrameTelemetry } from "../../player/hooks/usePreviewFirstFrameTelemetry";
import { PreviewPoster, usePreviewPoster } from "./PreviewPoster";
interface NLEPreviewProps extends Pick<
Expand All @@ -46,6 +47,7 @@ interface NLEPreviewProps extends Pick<
portrait?: boolean;
directUrl?: string;
suppressLoadingOverlay?: boolean;
loadingUi?: "studio" | "host";
onStageRef?: (ref: React.RefObject<HTMLDivElement | null>) => void;
/** Reports the authored composition size measured from the loaded preview. */
onCompositionSizeChange?: (size: PreviewCompositionSize | null) => void;
Expand All @@ -66,51 +68,7 @@ export function getPreviewPlayerKey({

const ZOOM_HUD_TIMEOUT_MS = 1200;
const ZOOM_SETTLE_MS = 200;
const PREVIEW_STAGE_INSET_PX = 8;

// clip-path as well as visibility: the player's loading overlay sets its own
// visibility:visible and would otherwise paint over the live frame.
const SHADOW_IFRAME_STYLE: React.CSSProperties = {
position: "absolute",
inset: 0,
visibility: "hidden",
clipPath: "inset(100%)",
pointerEvents: "none",
};

export function resolvePreviewStageSize(
viewportWidth: number,
viewportHeight: number,
compositionSize: PreviewCompositionSize | null,
portrait: boolean | undefined,
gutterPx = 0,
insetPx = PREVIEW_STAGE_INSET_PX,
): { width: number; height: number } {
const availableWidth = Math.max(0, viewportWidth - 2 * (insetPx + gutterPx));
const availableHeight = Math.max(0, viewportHeight - 2 * (insetPx + gutterPx));
const aspectRatio =
compositionSize && compositionSize.width > 0 && compositionSize.height > 0
? compositionSize.width / compositionSize.height
: portrait
? 9 / 16
: 16 / 9;

if (availableWidth === 0 || availableHeight === 0) {
return { width: 0, height: 0 };
}

let width = availableWidth;
let height = width / aspectRatio;
if (height > availableHeight) {
height = availableHeight;
width = height * aspectRatio;
}

return {
width: toDomPrecision(width),
height: toDomPrecision(height),
};
}


export const NLEPreview = memo(function NLEPreview({
projectId,
Expand All @@ -126,6 +84,7 @@ export const NLEPreview = memo(function NLEPreview({
portrait,
directUrl,
suppressLoadingOverlay,
loadingUi,
onStageRef,
onCompositionSizeChange,
fillBox,
Expand Down Expand Up @@ -544,6 +503,7 @@ export const NLEPreview = memo(function NLEPreview({
onPainted={(details) => reportPreviewFirstFrame(slot, details)}
portrait={portrait}
suppressLoadingOverlay={suppressLoadingOverlay}
loadingUi={loadingUi}
style={
directUrl?.includes("/components/")
? { position: "absolute", inset: 0, zIndex: 1 }
Expand Down
49 changes: 49 additions & 0 deletions packages/studio/src/components/nle/previewStage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import type { CSSProperties } from "react";
import type { PreviewCompositionSize } from "../../utils/previewCompositionSize";
import { toDomPrecision } from "./previewZoom";

export const PREVIEW_STAGE_INSET_PX = 8;

// clip-path as well as visibility: the player's loading overlay sets its own
// visibility:visible and would otherwise paint over the live frame.
export const SHADOW_IFRAME_STYLE: CSSProperties = {
position: "absolute",
inset: 0,
visibility: "hidden",
clipPath: "inset(100%)",
pointerEvents: "none",
};

export function resolvePreviewStageSize(
viewportWidth: number,
viewportHeight: number,
compositionSize: PreviewCompositionSize | null,
portrait: boolean | undefined,
gutterPx = 0,
insetPx = PREVIEW_STAGE_INSET_PX,
): { width: number; height: number } {
const availableWidth = Math.max(0, viewportWidth - 2 * (insetPx + gutterPx));
const availableHeight = Math.max(0, viewportHeight - 2 * (insetPx + gutterPx));
const aspectRatio =
compositionSize && compositionSize.width > 0 && compositionSize.height > 0
? compositionSize.width / compositionSize.height
: portrait
? 9 / 16
: 16 / 9;

if (availableWidth === 0 || availableHeight === 0) {
return { width: 0, height: 0 };
}

let width = availableWidth;
let height = width / aspectRatio;
if (height > availableHeight) {
height = availableHeight;
width = height * aspectRatio;
}

return {
width: toDomPrecision(width),
height: toDomPrecision(height),
};
}
33 changes: 33 additions & 0 deletions packages/studio/src/player/components/Player.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,39 @@ describe("ready to show", () => {
expect(usePlayerStore.getState().previewBooted).toBe(true);
});

/** A first load whose one audio has no data yet: Studio's asset wait. */
async function loadWithUnloadedMedia(props: Partial<PlayerProps>) {
const { host, player } = await mountPlayer(props);
const el = player as TestHyperframesPlayer;
document.body.appendChild(el.iframeElement);
const doc = el.iframeElement.contentDocument!;
const audio = doc.createElement("audio");
Object.defineProperty(audio, "readyState", { value: 0, configurable: true });
Object.defineProperty(audio, "networkState", { value: 2, configurable: true });
doc.body.appendChild(audio);
loadAndReady(el);
painted(el);
await flushEffects();
return host;
}

it("keeps its asset wait on a later load that only skips the composition cover", async () => {
const host = await loadWithUnloadedMedia({ suppressLoadingOverlay: true });
expect(host.textContent).toContain("Preparing preview assets");
});

it("draws no loader of its own when the host draws one, and still says the preview is loading", async () => {
const onCompositionLoadingChange = vi.fn();
const host = await loadWithUnloadedMedia({
suppressLoadingOverlay: false,
loadingUi: "host",
onCompositionLoadingChange,
});
expect(host.textContent).not.toContain("Preparing preview assets");
expect(host.textContent).not.toContain("Loading composition");
expect(onCompositionLoadingChange).toHaveBeenLastCalledWith(true);
});

it("stops deferring editing work when the preview never shows", async () => {
vi.useFakeTimers();
try {
Expand Down
17 changes: 12 additions & 5 deletions packages/studio/src/player/components/Player.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ interface PlayerProps {
portrait?: boolean;
style?: React.CSSProperties;
suppressLoadingOverlay?: boolean;
loadingUi?: "studio" | "host";
noLoadTimeout?: boolean;
}

Expand Down Expand Up @@ -88,6 +89,9 @@ function enableInteractiveIframe(player: HyperframesPlayerElement): void {
iframe?.style.setProperty("pointer-events", "auto");
}

const studioDraws = (wait: boolean, loadingUi: PlayerProps["loadingUi"]) =>
wait && loadingUi !== "host";

function isPreviewMediaElement(el: Element): el is HTMLMediaElement {
const tagName = el.tagName.toLowerCase();
return tagName === "video" || tagName === "audio";
Expand Down Expand Up @@ -156,6 +160,7 @@ export const Player = forwardRef<HTMLIFrameElement, PlayerProps>(
style,
suppressLoadingOverlay,
noLoadTimeout,
...host
},
ref,
) => {
Expand Down Expand Up @@ -470,16 +475,18 @@ export const Player = forwardRef<HTMLIFrameElement, PlayerProps>(
return () => clearTimeout(timer);
}, [projectId]);

const showCompositionOverlay =
const compositionWaitShown =
!suppressLoadingOverlay &&
!compositionOverlayDeferred &&
shouldShowCompositionLoadingOverlay(compositionLoading);
const showAssetOverlay =
assetOverlayVisible && !shaderTransitionLoading && !showCompositionOverlay && !previewError;
const assetWaitShown =
assetOverlayVisible && !shaderTransitionLoading && !compositionWaitShown && !previewError;
const showCompositionOverlay = studioDraws(compositionWaitShown, host.loadingUi);
const showAssetOverlay = studioDraws(assetWaitShown, host.loadingUi);

useEffect(() => {
onCompositionLoadingChange?.(showCompositionOverlay || showAssetOverlay);
}, [onCompositionLoadingChange, showCompositionOverlay, showAssetOverlay]);
onCompositionLoadingChange?.(compositionWaitShown || assetWaitShown);
}, [onCompositionLoadingChange, compositionWaitShown, assetWaitShown]);

return (
<div
Expand Down
Loading