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>
This commit is contained in:
@@ -64,3 +64,4 @@ services/remotion/out/
|
|||||||
/-w
|
/-w
|
||||||
/.agent-work/
|
/.agent-work/
|
||||||
dist/
|
dist/
|
||||||
|
services/remotion/player-dist/
|
||||||
|
|||||||
Generated
+1402
-1
File diff suppressed because it is too large
Load Diff
@@ -17,6 +17,7 @@
|
|||||||
"@react-three/postprocessing": "^3.0.4",
|
"@react-three/postprocessing": "^3.0.4",
|
||||||
"@remotion/cli": "4.0.290",
|
"@remotion/cli": "4.0.290",
|
||||||
"@remotion/lottie": "^4.0.290",
|
"@remotion/lottie": "^4.0.290",
|
||||||
|
"@remotion/player": "^4.0.290",
|
||||||
"@remotion/three": "^4.0.290",
|
"@remotion/three": "^4.0.290",
|
||||||
"@remotion/zod-types": "4.0.290",
|
"@remotion/zod-types": "4.0.290",
|
||||||
"@types/three": "^0.171.0",
|
"@types/three": "^0.171.0",
|
||||||
@@ -31,6 +32,8 @@
|
|||||||
"@dicebear/collection": "^9.4.2",
|
"@dicebear/collection": "^9.4.2",
|
||||||
"@dicebear/core": "^9.4.2",
|
"@dicebear/core": "^9.4.2",
|
||||||
"@types/react": "19.0.0",
|
"@types/react": "19.0.0",
|
||||||
"typescript": "5.5.4"
|
"@vitejs/plugin-react": "^4.7.0",
|
||||||
|
"typescript": "5.5.4",
|
||||||
|
"vite": "^5.4.21"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>FlatRender Player</title>
|
||||||
|
<style>
|
||||||
|
html, body, #root { margin: 0; height: 100%; background: #000; overflow: hidden; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="./main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
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 />);
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { defineConfig } from "vite";
|
||||||
|
import react from "@vitejs/plugin-react";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds the standalone client-side player (services/remotion/player) into a static
|
||||||
|
* bundle the Next app embeds via <iframe>. Relative base so it can be served from a
|
||||||
|
* sub-path (e.g. /player/). React-19 here, independent of the React-18 host.
|
||||||
|
*/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
root: path.resolve(__dirname, "player"),
|
||||||
|
base: "./",
|
||||||
|
build: {
|
||||||
|
outDir: path.resolve(__dirname, "player-dist"),
|
||||||
|
emptyOutDir: true,
|
||||||
|
chunkSizeWarningLimit: 4000,
|
||||||
|
},
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user