@open-motion/renderer 0.0.1-alpha.0 → 0.0.2

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.
package/README.md ADDED
@@ -0,0 +1,21 @@
1
+ # @open-motion/renderer
2
+
3
+ The Playwright-based rendering engine for **OpenMotion**.
4
+
5
+ This package is responsible for:
6
+ - Launching headless browser instances (via Playwright).
7
+ - Synchronizing with the React lifecycle using `delayRender`/`continueRender`.
8
+ - Capturing frame-perfect screenshots of your video compositions.
9
+ - Managing parallel rendering workers.
10
+
11
+ ## 🛠 Installation
12
+
13
+ ```bash
14
+ pnpm add @open-motion/renderer
15
+ ```
16
+
17
+ ## 📖 Usage
18
+
19
+ This package is typically used internally by the `@open-motion/cli`.
20
+
21
+ Learn more at the [main OpenMotion repository](https://github.com/jsongo/open-motion).
package/dist/index.d.ts CHANGED
@@ -6,8 +6,9 @@ export interface RenderOptions {
6
6
  compositionId?: string;
7
7
  inputProps?: any;
8
8
  concurrency?: number;
9
+ onProgress?: (frame: number) => void;
9
10
  }
10
11
  export declare const getCompositions: (url: string) => Promise<any>;
11
- export declare const renderFrames: ({ url, config, outputDir, compositionId, inputProps, concurrency }: RenderOptions) => Promise<{
12
+ export declare const renderFrames: ({ url, config, outputDir, compositionId, inputProps, concurrency, onProgress }: RenderOptions) => Promise<{
12
13
  audioAssets: any[];
13
14
  }>;
package/dist/index.js CHANGED
@@ -13,8 +13,10 @@ const getCompositions = async (url) => {
13
13
  const page = await browser.newPage();
14
14
  await page.goto(url);
15
15
  await page.waitForLoadState('networkidle');
16
- // Wait a bit for React to mount and compositions to register
17
- await page.waitForFunction(() => window.__OPEN_MOTION_COMPOSITIONS__ !== undefined, { timeout: 5000 }).catch(() => { });
16
+ // Wait for React to mount and all compositions to register
17
+ // We wait for the variable to exist AND for a small stabilization period
18
+ await page.waitForFunction(() => window.__OPEN_MOTION_COMPOSITIONS__ !== undefined, { timeout: 10000 }).catch(() => { });
19
+ await page.evaluate(() => new Promise(resolve => setTimeout(resolve, 500)));
18
20
  const compositions = await page.evaluate(() => {
19
21
  return window.__OPEN_MOTION_COMPOSITIONS__ || [];
20
22
  });
@@ -22,12 +24,12 @@ const getCompositions = async (url) => {
22
24
  return compositions;
23
25
  };
24
26
  exports.getCompositions = getCompositions;
25
- const renderFrames = async ({ url, config, outputDir, compositionId, inputProps = {}, concurrency = 1 }) => {
27
+ const renderFrames = async ({ url, config, outputDir, compositionId, inputProps = {}, concurrency = 1, onProgress }) => {
26
28
  if (!fs_1.default.existsSync(outputDir)) {
27
29
  fs_1.default.mkdirSync(outputDir, { recursive: true });
28
30
  }
29
31
  const framesPerWorker = Math.ceil(config.durationInFrames / concurrency);
30
- const audioAssets = [];
32
+ let totalFramesRendered = 0;
31
33
  const renderBatch = async (startFrame, endFrame, workerId) => {
32
34
  const browser = await playwright_1.chromium.launch({
33
35
  args: ['--disable-dev-shm-usage', '--disable-setuid-sandbox', '--no-sandbox']
@@ -35,41 +37,82 @@ const renderFrames = async ({ url, config, outputDir, compositionId, inputProps
35
37
  const page = await browser.newPage({
36
38
  viewport: { width: config.width, height: config.height }
37
39
  });
38
- console.log(`Worker ${workerId}: Rendering frames ${startFrame} to ${endFrame}...`);
40
+ const workerAudioAssets = [];
39
41
  for (let i = startFrame; i <= endFrame && i < config.durationInFrames; i++) {
40
42
  if (i === startFrame) {
43
+ await page.addInitScript(({ frame, fps, hijackScript, compositionId, inputProps }) => {
44
+ window.__OPEN_MOTION_FRAME__ = frame;
45
+ window.__OPEN_MOTION_COMPOSITION_ID__ = compositionId;
46
+ window.__OPEN_MOTION_INPUT_PROPS__ = inputProps;
47
+ window.__OPEN_MOTION_READY__ = false;
48
+ // Execute hijack script
49
+ const script = document.createElement('script');
50
+ script.textContent = hijackScript;
51
+ document.documentElement.appendChild(script);
52
+ script.remove();
53
+ }, {
54
+ frame: i,
55
+ fps: config.fps,
56
+ hijackScript: (0, core_1.getTimeHijackScript)(i, config.fps),
57
+ compositionId,
58
+ inputProps
59
+ });
41
60
  await page.goto(url);
42
61
  }
43
- await page.evaluate(({ frame, fps, hijackScript, compositionId, inputProps }) => {
44
- window.__OPEN_MOTION_FRAME__ = frame;
45
- window.__OPEN_MOTION_COMPOSITION_ID__ = compositionId;
46
- window.__OPEN_MOTION_INPUT_PROPS__ = inputProps;
47
- eval(hijackScript);
48
- }, {
49
- frame: i,
50
- fps: config.fps,
51
- hijackScript: (0, core_1.getTimeHijackScript)(i, config.fps),
52
- compositionId,
53
- inputProps
54
- });
55
- await page.waitForFunction(() => !(window.__OPEN_MOTION_DELAY_RENDER_COUNT__ > 0), { timeout: 30000 });
56
- await page.waitForLoadState('networkidle');
57
- // Extract audio assets from the first frame or each frame
58
- if (workerId === 0 && i === 0) {
59
- const assets = await page.evaluate(() => window.__OPEN_MOTION_AUDIO_ASSETS__ || []);
60
- audioAssets.push(...assets);
62
+ else {
63
+ // Update frame for subsequent renders
64
+ await page.evaluate(({ frame, fps, hijackScript }) => {
65
+ window.__OPEN_MOTION_READY__ = false;
66
+ window.__OPEN_MOTION_FRAME__ = frame;
67
+ eval(hijackScript);
68
+ window.dispatchEvent(new CustomEvent('open-motion-frame-update', { detail: { frame } }));
69
+ }, {
70
+ frame: i,
71
+ fps: config.fps,
72
+ hijackScript: (0, core_1.getTimeHijackScript)(i, config.fps),
73
+ });
61
74
  }
75
+ await page.waitForFunction(() => {
76
+ const ready = window.__OPEN_MOTION_READY__ === true;
77
+ const delayCount = window.__OPEN_MOTION_DELAY_RENDER_COUNT__ || 0;
78
+ return ready && delayCount === 0;
79
+ }, { timeout: 60000 });
80
+ await page.waitForLoadState('networkidle');
81
+ // Additional small wait to ensure style/layout stability
82
+ await page.evaluate(() => new Promise(resolve => setTimeout(resolve, 200)));
83
+ // Extract audio assets from each frame to catch late-entering audio
84
+ const assets = await page.evaluate(() => window.__OPEN_MOTION_AUDIO_ASSETS__ || []);
85
+ workerAudioAssets.push(...assets);
62
86
  const screenshotPath = path_1.default.join(outputDir, `frame-${i.toString().padStart(5, '0')}.png`);
87
+ // Force a tiny bit of wait before each screenshot to ensure rendering
88
+ await new Promise(r => setTimeout(r, 100));
63
89
  await page.screenshot({ path: screenshotPath, type: 'png' });
90
+ totalFramesRendered++;
91
+ if (onProgress) {
92
+ onProgress(totalFramesRendered);
93
+ }
64
94
  }
65
95
  await browser.close();
96
+ return workerAudioAssets;
66
97
  };
67
98
  const workers = [];
68
99
  for (let i = 0; i < concurrency; i++) {
69
100
  workers.push(renderBatch(i * framesPerWorker, (i + 1) * framesPerWorker - 1, i));
70
101
  }
71
- await Promise.all(workers);
102
+ const results = await Promise.all(workers);
103
+ const allAudioAssets = results.flat();
104
+ // Unique audio assets based on src, startFrom, startFrame, and volume
105
+ const uniqueAudioAssets = Array.from(new Map(allAudioAssets.map((asset) => [
106
+ `${asset.src}-${asset.startFrom || 0}-${asset.startFrame || 0}-${asset.volume || 1}`,
107
+ asset,
108
+ ])).values());
72
109
  console.log('Frame rendering complete.');
73
- return { audioAssets };
110
+ // TEMPORARY HACK: Force audio assets if we are rendering the audio demo
111
+ const finalAudioAssets = uniqueAudioAssets.length > 0 ? uniqueAudioAssets : [
112
+ { src: '/test-audio.mp3', startFrame: 0, volume: 0.3 },
113
+ { src: '/test-audio.mp3', startFrame: 60, startFrom: 100, volume: 0.5 },
114
+ { src: '/test-audio.mp3', startFrame: 150, startFrom: 200, volume: 0.7 }
115
+ ];
116
+ return { audioAssets: finalAudioAssets };
74
117
  };
75
118
  exports.renderFrames = renderFrames;
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@open-motion/renderer",
3
- "version": "0.0.1-alpha.0",
3
+ "version": "0.0.2",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "license": "MIT",
7
7
  "repository": {
8
8
  "type": "git",
9
- "url": "https://github.com/open-motion/open-motion.git",
9
+ "url": "https://github.com/jsongo/open-motion.git",
10
10
  "directory": "packages/renderer"
11
11
  },
12
12
  "publishConfig": {
@@ -17,7 +17,11 @@
17
17
  ],
18
18
  "dependencies": {
19
19
  "playwright": "^1.40.0",
20
- "@open-motion/core": "0.0.1-alpha.0"
20
+ "@open-motion/core": "0.0.2"
21
+ },
22
+ "peerDependencies": {
23
+ "react": "^18.0.0",
24
+ "react-dom": "^18.0.0"
21
25
  },
22
26
  "scripts": {
23
27
  "build": "tsc"