Files
flatrender/services/remotion/player/main.tsx
T
soroush.asadi dc5ff09b67 feat(remotion): isolated client-side player (Approach A) — templates render in-browser
Render the React-Three-Fiber-v9 (React 19) templates client-side without touching the
React-18 Next host: a standalone Vite app (services/remotion/player) mounts
@remotion/player with the real FlexStory composition. The studio will embed it via an
iframe and feed scene data (URL hash for first paint, postMessage for live edits).

- player/main.tsx: reads {props, aspect, watermark}, computes duration via
  calcFlexStoryMetadata, renders <Player>. Free tier shows a watermark overlay
  (preview only — clean export stays server-authorized).
- vite.config.player.ts: builds to player-dist/ with relative base (servable at /player/).
- @remotion/player + vite added.

Verified: vite build bundles FlexStory + three.js (672 modules → 1.3MB) and serves
at /player/index.html (200). Browser render to be confirmed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 12:11:27 +03:30

116 lines
3.6 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useEffect, useMemo, useState } from "react";
import { createRoot } from "react-dom/client";
import { Player } from "@remotion/player";
import { FlexStory, calcFlexStoryMetadata, flexStoryDefaults } from "../src/compositions/FlexStory";
/**
* Standalone, isolated client-side player (Approach A). Runs as its own React-19 app
* so the React-Three-Fiber v9 templates render in the browser without touching the
* React-18 Next host. The studio embeds this via an <iframe> and feeds it the
* project's scene data (URL hash for the first paint, postMessage for live edits).
*
* Free tier shows a watermark overlay here (preview only). The clean, no-watermark
* EXPORT is issued server-side — never trust the client to drop the watermark.
*/
const FPS = 30;
const ASPECTS: Record<string, [number, number]> = {
"16:9": [1920, 1080],
"1:1": [1080, 1080],
"9:16": [1080, 1920],
};
interface PlayerInput {
props?: typeof flexStoryDefaults;
aspect?: keyof typeof ASPECTS;
watermark?: boolean;
}
function decodeHash(): PlayerInput | null {
try {
const h = window.location.hash.replace(/^#/, "");
if (!h) return null;
return JSON.parse(decodeURIComponent(escape(window.atob(h))));
} catch {
return null;
}
}
const Watermark: React.FC = () => (
<div
style={{
position: "absolute",
inset: 0,
pointerEvents: "none",
display: "flex",
flexWrap: "wrap",
alignContent: "center",
justifyContent: "center",
gap: "12vmin",
transform: "rotate(-24deg) scale(1.4)",
opacity: 0.16,
mixBlendMode: "overlay",
}}
>
{Array.from({ length: 24 }).map((_, i) => (
<span key={i} style={{ color: "#fff", fontWeight: 800, fontSize: "5vmin", letterSpacing: 2, whiteSpace: "nowrap", fontFamily: "system-ui, sans-serif" }}>
FlatRender نسخهٔ پیشنمایش
</span>
))}
</div>
);
const App: React.FC = () => {
const [input, setInput] = useState<PlayerInput | null>(() => decodeHash());
useEffect(() => {
const onMsg = (e: MessageEvent) => {
if (e.data && e.data.type === "flatrender:props" && e.data.payload) {
setInput(e.data.payload as PlayerInput);
}
};
window.addEventListener("message", onMsg);
// Announce readiness so the studio can push the current props.
try {
window.parent?.postMessage({ type: "flatrender:ready" }, "*");
} catch {
/* not embedded */
}
return () => window.removeEventListener("message", onMsg);
}, []);
const props = input?.props ?? flexStoryDefaults;
const aspect = (input?.aspect ?? "9:16") as keyof typeof ASPECTS;
const [w, h] = ASPECTS[aspect] ?? ASPECTS["9:16"];
const watermark = input?.watermark ?? true;
const durationInFrames = useMemo(() => {
try {
return Math.max(1, calcFlexStoryMetadata({ props }).durationInFrames);
} catch {
return FPS * 10;
}
}, [props]);
return (
<div style={{ position: "fixed", inset: 0, background: "#000", display: "flex", alignItems: "center", justifyContent: "center" }}>
<div style={{ position: "relative", width: "100%", height: "100%" }}>
<Player
component={FlexStory}
inputProps={props}
durationInFrames={durationInFrames}
fps={FPS}
compositionWidth={w}
compositionHeight={h}
style={{ width: "100%", height: "100%" }}
controls
loop
acknowledgeRemotionLicense
/>
{watermark && <Watermark />}
</div>
</div>
);
};
createRoot(document.getElementById("root")!).render(<App />);