@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 +21 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +68 -25
- package/package.json +7 -3
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
|
|
17
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
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.
|
|
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/
|
|
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.
|
|
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"
|