@lalalic/markcut 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/.env.example +27 -0
  2. package/.github/user-steer.md +9 -0
  3. package/.vscode/settings.json +3 -0
  4. package/AGENTS.md +271 -0
  5. package/README.md +219 -0
  6. package/SKILL.md +209 -0
  7. package/docs/dynamic-components.md +191 -0
  8. package/docs/edit-mode.md +220 -0
  9. package/docs/json-descriptive.md +539 -0
  10. package/docs/label-mode.md +110 -0
  11. package/docs/markdown-descriptive.md +751 -0
  12. package/docs/templates.md +52 -0
  13. package/package.json +64 -0
  14. package/remotion.config.ts +5 -0
  15. package/scripts/artlist-dl.mjs +190 -0
  16. package/scripts/build-pipeline.sh +19 -0
  17. package/scripts/build-player.sh +20 -0
  18. package/src/Root.tsx +55 -0
  19. package/src/config.mjs +88 -0
  20. package/src/context/EventContext.tsx +168 -0
  21. package/src/context/index.tsx +42 -0
  22. package/src/descriptive/compiler.test.ts +1135 -0
  23. package/src/descriptive/compiler.ts +1230 -0
  24. package/src/descriptive/dsl.ts +455 -0
  25. package/src/descriptive/markdown.test.ts +866 -0
  26. package/src/descriptive/markdown.ts +674 -0
  27. package/src/descriptive/resolve.test.ts +951 -0
  28. package/src/descriptive/resolve.ts +891 -0
  29. package/src/entry.tsx +163 -0
  30. package/src/index.ts +4 -0
  31. package/src/player/browser.tsx +356 -0
  32. package/src/player/bundle/player.js +60259 -0
  33. package/src/player/bundler.mjs +269 -0
  34. package/src/player/label-server.mjs +599 -0
  35. package/src/player/pipeline.mjs +11123 -0
  36. package/src/player/pipeline.ts +117 -0
  37. package/src/player/server-shared.mjs +144 -0
  38. package/src/player/server.mjs +1006 -0
  39. package/src/render/cli-tools.ts +177 -0
  40. package/src/render/cli.mjs +628 -0
  41. package/src/schema/index.ts +259 -0
  42. package/src/types/Audio.tsx +56 -0
  43. package/src/types/Component.tsx +135 -0
  44. package/src/types/Effect.tsx +88 -0
  45. package/src/types/Folder.tsx +180 -0
  46. package/src/types/FrameSyncStyle.tsx +51 -0
  47. package/src/types/Image.tsx +51 -0
  48. package/src/types/Include.tsx +394 -0
  49. package/src/types/Map.tsx +252 -0
  50. package/src/types/Rhythm.tsx +58 -0
  51. package/src/types/Scene.tsx +42 -0
  52. package/src/types/Subtitle.tsx +218 -0
  53. package/src/types/Video.tsx +70 -0
  54. package/src/types/keyframes.ts +454 -0
  55. package/src/utils/__tests__/vtt.test.ts +129 -0
  56. package/src/utils/index.ts +168 -0
  57. package/src/utils/tween.ts +118 -0
  58. package/src/vision/cli.mjs +1187 -0
  59. package/src/vision/vision_prompts.md +67 -0
  60. package/tests/dsl.test.ts +317 -0
  61. package/tests/fixtures/audio.json +25 -0
  62. package/tests/fixtures/basic.json +27 -0
  63. package/tests/fixtures/component-all.json +38 -0
  64. package/tests/fixtures/components.json +38 -0
  65. package/tests/fixtures/effects.json +64 -0
  66. package/tests/fixtures/full.json +51 -0
  67. package/tests/fixtures/map.json +23 -0
  68. package/tests/fixtures/md/all-nodes.md +28 -0
  69. package/tests/fixtures/md/basic.md +6 -0
  70. package/tests/fixtures/md/component-imports.md +20 -0
  71. package/tests/fixtures/md/edge-cases.md +33 -0
  72. package/tests/fixtures/md/effects.md +17 -0
  73. package/tests/fixtures/md/frontmatter.md +20 -0
  74. package/tests/fixtures/md/full-feature.md +58 -0
  75. package/tests/fixtures/md/imports-block.md +19 -0
  76. package/tests/fixtures/md/include-main.md +11 -0
  77. package/tests/fixtures/md/include-sub.md +25 -0
  78. package/tests/fixtures/md/jsx-code-fence.md +21 -0
  79. package/tests/fixtures/md/map.md +11 -0
  80. package/tests/fixtures/md/nested-scenes.md +25 -0
  81. package/tests/fixtures/md/rhythm.md +17 -0
  82. package/tests/fixtures/md/scenes.md +16 -0
  83. package/tests/fixtures/md/tween.md +11 -0
  84. package/tests/fixtures/md/vars-test.md +6 -0
  85. package/tests/fixtures/scenes.json +40 -0
  86. package/tests/fixtures/subtitle.json +59 -0
  87. package/tests/fixtures/subvideo.json +59 -0
  88. package/tests/fixtures/templates/courseware.md +351 -0
  89. package/tests/fixtures/tween-visual.json +28 -0
  90. package/tests/fixtures/video-series.json +54 -0
  91. package/tests/md-descriptive.test.ts +742 -0
  92. package/tests/render.test.ts +985 -0
  93. package/tests/schema.test.ts +68 -0
  94. package/tests/server.test.ts +308 -0
  95. package/tests/utils.ts +391 -0
  96. package/tests/vitest.config.ts +18 -0
  97. package/tests/vitest.integration.config.ts +16 -0
  98. package/tsconfig.json +20 -0
package/src/entry.tsx ADDED
@@ -0,0 +1,163 @@
1
+ import * as React from "react";
2
+ import { AbsoluteFill, continueRender, delayRender } from "remotion";
3
+ import { ComposeContext, EventProvider, type ComposeContextValue } from "./context/index";
4
+
5
+ import { FolderLeaf } from "./types/Folder";
6
+ import { SubtitleOverlay } from "./types/Subtitle";
7
+ import { getDurationInSeconds } from "./utils/index";
8
+ import { root as rootSchema, type Root } from "./schema/index";
9
+ import {
10
+ compileDescriptiveRoot,
11
+ type CompileOptions,
12
+ type DescriptiveRoot,
13
+ } from "./descriptive/compiler";
14
+
15
+ export interface MarkCutProps {
16
+ /** Stream tree. Will be parsed by zod (defaults applied). */
17
+ root: unknown;
18
+ /** Optional host-provided Container + components registry. */
19
+ compose?: Partial<ComposeContextValue>;
20
+ /** Background of the canvas. Defaults to black. */
21
+ background?: string;
22
+ }
23
+
24
+ export interface DescriptiveCompositionProps extends Omit<MarkCutProps, "root"> {
25
+ root: DescriptiveRoot;
26
+ compileOptions?: CompileOptions;
27
+ }
28
+
29
+ const DefaultContainer: ComposeContextValue["Container"] = ({ children, style, className }) => (
30
+ <div className={className} style={{ position: "absolute", inset: 0, ...style }}>
31
+ {children}
32
+ </div>
33
+ );
34
+
35
+ /**
36
+ * Load the component registry from root.imports.
37
+ * - If string: import it as an ESM module (pre-bundled by server)
38
+ * - If object: use it directly (programmatic API)
39
+ */
40
+ function useComponentRegistry(imports: unknown): Record<string, React.ComponentType<any>> | null {
41
+ const [registry, setRegistry] = React.useState<Record<string, React.ComponentType<any>> | null>(null);
42
+ // delayRender() returns a numeric handle that continueRender() consumes.
43
+ const handleRef = React.useRef<number | null>(null);
44
+
45
+ React.useEffect(() => {
46
+ if (!imports) {
47
+ // No registry to load — stay null so ComposeContext falls back to
48
+ // `compose.components` (programmatic API). Returning `{}` here would
49
+ // shadow the host-provided registry with an empty object.
50
+ setRegistry(null);
51
+ return;
52
+ }
53
+ if (typeof imports === "object" && imports !== null && !Array.isArray(imports)) {
54
+ setRegistry(imports as Record<string, React.ComponentType<any>>);
55
+ return;
56
+ }
57
+ if (typeof imports === "string") {
58
+ if (!handleRef.current) {
59
+ handleRef.current = delayRender("Loading component registry: " + imports);
60
+ }
61
+ import(/* webpackIgnore: true */ imports)
62
+ .then((mod: any) => {
63
+ // The bundle exports all components as named exports
64
+ setRegistry(mod.default ?? mod);
65
+ if (handleRef.current) {
66
+ continueRender(handleRef.current);
67
+ handleRef.current = null;
68
+ }
69
+ })
70
+ .catch((err: Error) => {
71
+ console.error("Failed to load component registry:", err);
72
+ setRegistry(null);
73
+ if (handleRef.current) {
74
+ continueRender(handleRef.current);
75
+ handleRef.current = null;
76
+ }
77
+ });
78
+ return;
79
+ }
80
+ setRegistry(null);
81
+ }, [imports]);
82
+
83
+ return registry;
84
+ }
85
+
86
+ export function MarkCut({ root, compose, background = "#000" }: MarkCutProps) {
87
+ const parsed = React.useMemo<Root>(() => {
88
+ if (!root) {
89
+ // Return a minimal valid root when no data is provided (e.g. studio placeholder)
90
+ return {
91
+ id: "root",
92
+ type: "root",
93
+ width: 1080,
94
+ height: 1920,
95
+ fps: 30,
96
+ visible: true,
97
+ isSeries: false,
98
+ children: [],
99
+ durationInSeconds: 0.1,
100
+ } as unknown as Root;
101
+ }
102
+ return rootSchema.parse(root) as unknown as Root;
103
+ }, [root]);
104
+
105
+ // Load component registry (bundle URL or inline map)
106
+ const registry = useComponentRegistry(parsed.imports);
107
+
108
+ // engine pre-pass: stamp durationInSeconds onto every node
109
+ React.useMemo(() => getDurationInSeconds(parsed as any, true), [parsed]);
110
+
111
+ const value = React.useMemo<ComposeContextValue>(
112
+ () => ({
113
+ Container: compose?.Container ?? DefaultContainer,
114
+ onError: compose?.onError,
115
+ components: registry ?? compose?.components,
116
+ }),
117
+ [compose, registry],
118
+ );
119
+
120
+ return (
121
+ <ComposeContext.Provider value={value}>
122
+ <EventProvider>
123
+ <AbsoluteFill style={{ background }}>
124
+ {parsed.stylesheet && <style>{parsed.stylesheet}</style>}
125
+ <FolderLeaf stream={parsed as any} />
126
+ {parsed.subtitle && <SubtitleOverlay subtitle={parsed.subtitle} />}
127
+ </AbsoluteFill>
128
+ </EventProvider>
129
+ </ComposeContext.Provider>
130
+ );
131
+ }
132
+
133
+ export function DescriptiveComposition({
134
+ root,
135
+ compose,
136
+ background = "#000",
137
+ compileOptions,
138
+ }: DescriptiveCompositionProps) {
139
+ const compiled = React.useMemo(
140
+ () => compileDescriptiveRoot(root, compileOptions),
141
+ [root, compileOptions],
142
+ );
143
+
144
+ return (
145
+ <MarkCut
146
+ root={compiled}
147
+ compose={compose}
148
+ background={background}
149
+ />
150
+ );
151
+ }
152
+
153
+ export { rootSchema, FolderLeaf };
154
+ export * from "./schema/index";
155
+ export * from "./context/index";
156
+ export * from "./descriptive/compiler";
157
+ export * from "./descriptive/markdown";
158
+ export { getDurationInSeconds } from "./utils/index";
159
+ export { builtinAnimations } from "./types/keyframes";
160
+ // Resolve functions (TTS, STT, media probe) are available via
161
+ // import from "markcut/descriptive-resolve" for CLI use.
162
+ // Not re-exported from entry to avoid bundling Node.js modules
163
+ // (node:child_process, node:fs, etc.) into the browser-side render bundle.
package/src/index.ts ADDED
@@ -0,0 +1,4 @@
1
+ import { registerRoot } from "remotion";
2
+ import { RemotionRoot } from "./Root";
3
+
4
+ registerRoot(RemotionRoot);
@@ -0,0 +1,356 @@
1
+ /**
2
+ * Browser player entry point.
3
+ * Bundled with esbuild and served by the player server.
4
+ * Renders stream tree JSON using @remotion/player with MarkCut.
5
+ */
6
+ import * as React from "react";
7
+ import { createRoot } from "react-dom/client";
8
+ import * as ReactDOM from "react-dom";
9
+ import * as Remotion from "remotion";
10
+ import { Player } from "@remotion/player";
11
+ import { MarkCut, getDurationInSeconds } from "../entry";
12
+
13
+ /**
14
+ * Register all player-bundled packages on a global registry so the import map
15
+ * shim (below) can re-export them to dynamically-loaded component bundles.
16
+ *
17
+ * Any package added here must also be listed as --external in bundler.mjs's
18
+ * `getSharedExternals()` so esbuild leaves its import specifier as a bare
19
+ * import in the component bundle output. The import map then resolves that
20
+ * bare import back to this same module instance, avoiding duplicate React,
21
+ * Remotion, or other runtime singleton issues.
22
+ */
23
+ if (typeof window !== "undefined") {
24
+ (globalThis as any).__remotionShared = {
25
+ "react": React,
26
+ "react-dom": ReactDOM,
27
+ "remotion": Remotion,
28
+ "@remotion/player": { Player },
29
+ };
30
+
31
+ // Stub for node:module — some component deps (e.g. @remotion/tailwind-v4)
32
+ // import from node:module at module level for build-time helpers that are
33
+ // never actually called during rendering. Provide a harmless stub so the
34
+ // import doesn't fail in the browser.
35
+ if (!(globalThis as any).__nodeModuleStub) {
36
+ const nodeModuleStubCode = `export function createRequire() { return () => ({ resolve: () => { throw new Error("node:module.createRequire is not available in the browser"); } }); }`;
37
+ const nodeModuleBlob = new Blob([nodeModuleStubCode], { type: "application/javascript" });
38
+ (globalThis as any).__nodeModuleStub = URL.createObjectURL(nodeModuleBlob);
39
+ }
40
+
41
+ // Generate import map shim dynamically from the shared registry.
42
+ // Each entry creates a blob URL that re-exports everything from the global,
43
+ // so component bundles can import("react"), import("remotion"), etc. and
44
+ // receive the same instances already loaded in the main player bundle.
45
+ if (!document.querySelector('#rmtr-import-map')) {
46
+ try {
47
+ const imports: Record<string, string> = {};
48
+ // Add node:* stubs
49
+ imports["node:module"] = (globalThis as any).__nodeModuleStub;
50
+ imports["node:fs"] = (globalThis as any).__nodeModuleStub;
51
+ imports["node:path"] = (globalThis as any).__nodeModuleStub;
52
+ imports["node:os"] = (globalThis as any).__nodeModuleStub;
53
+ const shared = (globalThis as any).__remotionShared;
54
+
55
+ for (const [specifier, mod] of Object.entries(shared) as [string, any][]) {
56
+ const exportNames = Object.keys(mod);
57
+ const hasDefault = 'default' in mod;
58
+ const lines: string[] = [
59
+ `const _mod = globalThis.__remotionShared[${JSON.stringify(specifier)}];`,
60
+ ];
61
+ for (const name of exportNames) {
62
+ if (name === 'default') continue;
63
+ lines.push(
64
+ `const __${name} = _mod[${JSON.stringify(name)}];`,
65
+ `export { __${name} as ${name} };`
66
+ );
67
+ }
68
+ if (hasDefault) {
69
+ lines.push('export default _mod.default;');
70
+ }
71
+ const blob = new Blob([lines.join('\n')], { type: "application/javascript" });
72
+ imports[specifier] = URL.createObjectURL(blob);
73
+ }
74
+
75
+ // react/jsx-runtime — built manually because it's not a package import
76
+ // but rather a sub-path of react that React.createElement handles directly.
77
+ const blobJsx = new Blob([`
78
+ const R = globalThis.__remotionShared["react"];
79
+ const { createElement, Fragment } = R;
80
+ export { Fragment };
81
+ export function jsx(type, props, key) {
82
+ return createElement(type, key != null ? { ...props, key } : props);
83
+ }
84
+ export function jsxs(type, props, key) {
85
+ return createElement(type, key != null ? { ...props, key } : props);
86
+ }
87
+ export function jsxDEV(type, props, key, isStaticChildren, source, self) {
88
+ return createElement(type, key != null ? { ...props, key } : props);
89
+ }
90
+ `], { type: "application/javascript" });
91
+ imports["react/jsx-runtime"] = URL.createObjectURL(blobJsx);
92
+
93
+ const script = document.createElement("script");
94
+ script.type = "importmap";
95
+ script.id = "rmtr-import-map";
96
+ script.textContent = JSON.stringify({ imports });
97
+ document.head.appendChild(script);
98
+ } catch (e) {
99
+ console.warn("Failed to create shared module import map:", e);
100
+ }
101
+ }
102
+ }
103
+
104
+ function PlayerApp() {
105
+ const playerRef = React.useRef<any>(null);
106
+ const [ready, setReady] = React.useState(false);
107
+ const [error, setError] = React.useState<string | null>(null);
108
+ const [data, setData] = React.useState<any>(null);
109
+ const [refreshKey, setRefreshKey] = React.useState(0);
110
+ const [muted, setMuted] = React.useState(false);
111
+ const [volume, setVolume] = React.useState(1);
112
+ const seekAttemptedRef = React.useRef(false);
113
+ const mountedRef = React.useRef(true);
114
+
115
+ // Parse URL params
116
+ const urlParams = new URLSearchParams(typeof window !== "undefined" ? window.location.search : "");
117
+ const autoPlay = urlParams.get("autoplay") === "true";
118
+ const startAt = parseFloat(urlParams.get("start") || urlParams.get("t") || "0") || 0;
119
+
120
+ // Derive player config from data early so effects can reference them safely.
121
+ // These are computed on every render but only meaningful when data is set.
122
+ const fps = data?.fps ?? 30;
123
+ const durationInSeconds = data ? (getDurationInSeconds(data, true) || 5) : 5;
124
+ const durationInFrames = Math.max(1, Math.ceil(durationInSeconds * fps));
125
+
126
+ const loadData = React.useCallback(() => {
127
+ setReady(false);
128
+ const variant = (window as any).VARIANT || "default";
129
+ const url = variant !== "default" ? `/api/video-data?variant=${variant}` : "/api/video-data";
130
+ fetch(url)
131
+ .then((r) => r.json())
132
+ .then((json) => {
133
+ const root = json.root || json;
134
+ setData(root);
135
+ setReady(true);
136
+ })
137
+ .catch((e) => setError(e.message));
138
+ }, []);
139
+
140
+ React.useEffect(() => {
141
+ loadData();
142
+ const handler = () => { setRefreshKey(k => k + 1); };
143
+ window.addEventListener("refresh-player", handler);
144
+ return () => { mountedRef.current = false; window.removeEventListener("refresh-player", handler); };
145
+ }, [loadData]);
146
+
147
+ React.useEffect(() => {
148
+ if (refreshKey > 0) loadData();
149
+ }, [refreshKey, loadData]);
150
+
151
+ // Seek to startAt once player is mounted
152
+ React.useEffect(() => {
153
+ if (!ready || !data || seekAttemptedRef.current) return;
154
+ if (startAt > 0 && playerRef.current) {
155
+ // Small delay to let the player fully initialize
156
+ const timer = setTimeout(() => {
157
+ if (!mountedRef.current || !playerRef.current) return;
158
+ const frame = Math.round(startAt * fps);
159
+ playerRef.current.seekTo(frame);
160
+ seekAttemptedRef.current = true;
161
+ }, 100);
162
+ return () => clearTimeout(timer);
163
+ }
164
+ seekAttemptedRef.current = true;
165
+ }, [ready, data, startAt, fps]);
166
+
167
+ // Keyboard shortcuts
168
+ React.useEffect(() => {
169
+ if (!ready || !playerRef.current) return;
170
+
171
+ const FWD_SECONDS = 5;
172
+ const BACK_SECONDS = 5;
173
+ const VOLUME_STEP = 0.1;
174
+
175
+ function seekRelative(deltaSec: number) {
176
+ const p = playerRef.current;
177
+ if (!p) return;
178
+ const frame = p.getCurrentFrame() + Math.round(deltaSec * fps);
179
+ p.seekTo(Math.max(0, frame));
180
+ }
181
+
182
+ function seekPercent(pct: number) {
183
+ const p = playerRef.current;
184
+ if (!p) return;
185
+ p.seekTo(Math.round(pct * durationInFrames));
186
+ }
187
+
188
+ function showHelp() {
189
+ // eslint-disable-next-line no-console
190
+ console.log(`%c🎬 MarkCut Player Shortcuts
191
+ ━━━━━━━━━━━━━━━━━━━━━
192
+ Space / K Play / Pause
193
+ ← / → Seek -${BACK_SECONDS}s / +${FWD_SECONDS}s
194
+ Shift+←/→ Seek -1 / +1 frame
195
+ J Rewind 2×
196
+ L Forward 2×
197
+ ↑ / ↓ Volume +${Math.round(VOLUME_STEP * 100)}% / -${Math.round(VOLUME_STEP * 100)}%
198
+ M Mute toggle
199
+ F Fullscreen toggle
200
+ 0-9 Seek to 0%-90%
201
+ ? Show this help`, "font-size:14px;");
202
+ }
203
+
204
+ function onKey(e: KeyboardEvent) {
205
+ // Ignore if focus is inside a form element
206
+ const tag = (e.target as HTMLElement).tagName;
207
+ if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return;
208
+
209
+ const p = playerRef.current;
210
+ if (!p) return;
211
+
212
+ switch (e.key) {
213
+ case " ":
214
+ case "k":
215
+ case "K":
216
+ e.preventDefault();
217
+ p.toggle();
218
+ break;
219
+
220
+ case "ArrowLeft":
221
+ e.preventDefault();
222
+ seekRelative(e.shiftKey ? -1 / fps : -BACK_SECONDS);
223
+ break;
224
+
225
+ case "ArrowRight":
226
+ e.preventDefault();
227
+ seekRelative(e.shiftKey ? 1 / fps : FWD_SECONDS);
228
+ break;
229
+
230
+ case "ArrowUp":
231
+ e.preventDefault();
232
+ setVolume(v => {
233
+ const nv = Math.min(1, v + VOLUME_STEP);
234
+ if (typeof p.setVolume === "function") p.setVolume(nv);
235
+ return nv;
236
+ });
237
+ break;
238
+
239
+ case "ArrowDown":
240
+ e.preventDefault();
241
+ setVolume(v => {
242
+ const nv = Math.max(0, v - VOLUME_STEP);
243
+ if (typeof p.setVolume === "function") p.setVolume(nv);
244
+ return nv;
245
+ });
246
+ break;
247
+
248
+ case "m":
249
+ case "M":
250
+ e.preventDefault();
251
+ setMuted(m => {
252
+ const nm = !m;
253
+ if (typeof p.setVolume === "function") p.setVolume(nm ? 0 : volume);
254
+ return nm;
255
+ });
256
+ break;
257
+
258
+ case "f":
259
+ case "F":
260
+ e.preventDefault();
261
+ if (typeof p.requestFullscreen === "function") {
262
+ p.requestFullscreen();
263
+ } else {
264
+ document.fullscreenElement
265
+ ? document.exitFullscreen()
266
+ : document.documentElement.requestFullscreen();
267
+ }
268
+ break;
269
+
270
+ case "j":
271
+ case "J":
272
+ e.preventDefault();
273
+ // Toggle between normal and 2× reverse
274
+ p.playbackRate = p.playbackRate === -2 ? 1 : -2;
275
+ break;
276
+
277
+ case "l":
278
+ case "L":
279
+ e.preventDefault();
280
+ // Toggle between normal and 2× forward
281
+ p.playbackRate = p.playbackRate === 2 ? 1 : 2;
282
+ break;
283
+
284
+ case "/":
285
+ case "?":
286
+ e.preventDefault();
287
+ showHelp();
288
+ break;
289
+
290
+ default:
291
+ if (e.key >= "0" && e.key <= "9") {
292
+ e.preventDefault();
293
+ seekPercent(parseInt(e.key) / 10);
294
+ }
295
+ break;
296
+ }
297
+ }
298
+
299
+ window.addEventListener("keydown", onKey);
300
+ return () => window.removeEventListener("keydown", onKey);
301
+ }, [ready, data, fps, durationInFrames, volume]);
302
+
303
+ // Expose seek API for external scripts
304
+ React.useEffect(() => {
305
+ if (!data) return;
306
+ (window as any).__remotionSeekTo = (timeInSeconds: number) => {
307
+ const frame = Math.round(timeInSeconds * fps);
308
+ playerRef.current?.seekTo(frame);
309
+ };
310
+ });
311
+
312
+ if (error) {
313
+ return React.createElement("div", {
314
+ style: { color: "red", padding: 40, fontFamily: "sans-serif" },
315
+ }, "Error: " + error);
316
+ }
317
+
318
+ if (!ready) {
319
+ return React.createElement("div", {
320
+ style: { color: "#888", padding: 40, fontFamily: "sans-serif" },
321
+ }, "Loading...");
322
+ }
323
+
324
+ const width = data.width || 1080;
325
+ const height = data.height || 1920;
326
+
327
+ return React.createElement("div", {
328
+ style: { width: "100%", height: "100%", background: "#000" },
329
+ },
330
+ React.createElement(Player, {
331
+ ref: playerRef,
332
+ component: MarkCut,
333
+ inputProps: {
334
+ root: data,
335
+ compose: {},
336
+ },
337
+ durationInFrames,
338
+ fps,
339
+ compositionWidth: width,
340
+ compositionHeight: height,
341
+ style: { width: "100%", height: "100%" },
342
+ controls: true,
343
+ showPlaybackRateControl: true,
344
+ allowFullscreen: true,
345
+ clickToPlay: false,
346
+ doubleClickToFullscreen: true,
347
+ autoPlay: autoPlay,
348
+ })
349
+ );
350
+ }
351
+
352
+ const container = document.getElementById("root");
353
+ if (container) {
354
+ const root = createRoot(container);
355
+ root.render(React.createElement(PlayerApp));
356
+ }