Files
flatrender/services/remotion/player/main.tsx
T
2026-06-25 12:46:53 +03:30

129 lines
5.3 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;
}
// Asset-free demo shown when opened with no scene data (so the bare /player/ URL
// renders something real to test the in-browser engine).
const DEFAULT_DEMO = {
scenes: [
{ blockId: "IGIntro", durationSec: 3, props: { badge: "اینستاگرام", headline: "صفحهٔ ما را دنبال کنید", subtitle: "هر روز یک طرح تازه" } },
{ blockId: "IGProfile", durationSec: 5, props: { headline: "صفحهٔ ما را دنبال کنید", handle: "flat.studio", name: "استودیو فلت", category: "هنر و طراحی", bio1: "هر روز یک طرح تازه ✨", bio2: "آموزش، قالب و الهام برای طراحان", bio3: "سفارش و دانلود 👇", link: "flat.studio/shop", posts: "۳۲۰", followers: "۲۴٫۸ هزار", following: "۱۸۰", hi1: "جدید", hi2: "قالب‌ها", hi3: "آموزش", hi4: "نمونه‌کار", followLabel: "دنبال کردن", messageLabel: "پیام" } },
{ blockId: "IGFeed", durationSec: 4, props: { caption: "محتوای ما را ببینید" } },
{ blockId: "IGStats", durationSec: 3, props: { bigValue: "۲۴۸۰۰", bigLabel: "دنبال‌کننده", stat2Value: "۱۲۰۰۰۰۰", stat2Label: "پسند", stat3Value: "۳۲۰", stat3Label: "پست", proofLine: "به جمع هزاران دنبال‌کنندهٔ ما بپیوندید" } },
{ blockId: "IGFollowCTA", durationSec: 3, props: { headline: "همین حالا دنبال کنید", handle: "@flat.studio", buttonLabel: "دنبال کردن", followedLabel: "دنبال شد", footer: "لینک در بایو 👆" } },
],
accentColor: "#dc2743", secondaryColor: "#7c5cff", backgroundColor: "#f7f4fa", textColor: "#15151a", music: "", sfx: false, finish: false,
} as unknown as typeof flexStoryDefaults;
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 ?? DEFAULT_DEMO;
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 />);