Files
flatrender/services/remotion/player/main.tsx
T

116 lines
3.6 KiB
TypeScript
Raw Normal View History

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 />);