vueseq 0.1.2 → 0.2.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.
- package/README.md +115 -52
- package/bin/cli.js +185 -79
- package/package.json +7 -2
- package/src/index.js +3 -2
- package/src/renderer/capture.js +185 -0
- package/src/renderer/encode-optimized.js +317 -0
- package/src/renderer/encode-parallel.js +320 -0
- package/src/renderer/encode.js +285 -63
- package/src/renderer/ffmpeg-encode.js +70 -0
- package/src/renderer/gpu.js +423 -0
- package/src/renderer/render.js +133 -104
package/src/renderer/render.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Frame Renderer
|
|
3
|
-
*
|
|
3
|
+
*
|
|
4
4
|
* The core rendering loop that captures each frame using Playwright.
|
|
5
5
|
* For each frame:
|
|
6
6
|
* 1. Seek GSAP globalTimeline to the exact time
|
|
@@ -12,6 +12,10 @@ import { chromium } from 'playwright'
|
|
|
12
12
|
import { createVideoServer } from '../bundler/vite.js'
|
|
13
13
|
import { mkdir } from 'fs/promises'
|
|
14
14
|
import { join } from 'path'
|
|
15
|
+
import { getOptimalChromiumConfig } from './gpu.js'
|
|
16
|
+
|
|
17
|
+
// GPU configuration is now handled by the gpu.js module
|
|
18
|
+
// which auto-detects the best backend for the current system
|
|
15
19
|
|
|
16
20
|
/**
|
|
17
21
|
* Get the timeline duration from a Vue component
|
|
@@ -22,32 +26,43 @@ import { join } from 'path'
|
|
|
22
26
|
* @returns {Promise<number|null>} Duration in seconds, or null if not detectable
|
|
23
27
|
*/
|
|
24
28
|
export async function getTimelineDuration(options) {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
29
|
+
const { input, width = 1920, height = 1080 } = options
|
|
30
|
+
|
|
31
|
+
const { url, cleanup } = await createVideoServer({ input, width, height })
|
|
32
|
+
|
|
33
|
+
// Get optimal GPU configuration for this system
|
|
34
|
+
const gpuConfig = await getOptimalChromiumConfig()
|
|
35
|
+
|
|
36
|
+
const launchOptions = {
|
|
37
|
+
headless: gpuConfig.headless,
|
|
38
|
+
args: gpuConfig.args,
|
|
39
|
+
}
|
|
40
|
+
if (gpuConfig.channel) {
|
|
41
|
+
launchOptions.channel = gpuConfig.channel
|
|
42
|
+
}
|
|
43
|
+
const browser = await chromium.launch(launchOptions)
|
|
44
|
+
const context = await browser.newContext({
|
|
45
|
+
viewport: { width, height },
|
|
46
|
+
deviceScaleFactor: 1,
|
|
47
|
+
})
|
|
48
|
+
const page = await context.newPage()
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
await page.goto(url, { waitUntil: 'networkidle' })
|
|
52
|
+
await page.waitForFunction(() => window.__VUESEQ_READY__ === true, {
|
|
53
|
+
timeout: 30000,
|
|
33
54
|
})
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const duration = await page.evaluate(() => window.__VUESEQ_GET_DURATION__?.())
|
|
46
|
-
return duration
|
|
47
|
-
} finally {
|
|
48
|
-
await browser.close()
|
|
49
|
-
await cleanup()
|
|
50
|
-
}
|
|
55
|
+
// Give Vue/GSAP a moment to set up timelines
|
|
56
|
+
await page.waitForTimeout(100)
|
|
57
|
+
|
|
58
|
+
const duration = await page.evaluate(() =>
|
|
59
|
+
window.__VUESEQ_GET_DURATION__?.(),
|
|
60
|
+
)
|
|
61
|
+
return duration
|
|
62
|
+
} finally {
|
|
63
|
+
await browser.close()
|
|
64
|
+
await cleanup()
|
|
65
|
+
}
|
|
51
66
|
}
|
|
52
67
|
|
|
53
68
|
/**
|
|
@@ -62,87 +77,101 @@ export async function getTimelineDuration(options) {
|
|
|
62
77
|
* @returns {Promise<{framesDir: string, totalFrames: number, cleanup: () => Promise<void>}>}
|
|
63
78
|
*/
|
|
64
79
|
export async function renderFrames(options) {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
+
let {
|
|
81
|
+
input,
|
|
82
|
+
fps = 30,
|
|
83
|
+
duration,
|
|
84
|
+
width = 1920,
|
|
85
|
+
height = 1080,
|
|
86
|
+
onProgress,
|
|
87
|
+
} = options
|
|
88
|
+
|
|
89
|
+
// Auto-detect duration if not provided
|
|
90
|
+
if (!duration) {
|
|
91
|
+
duration = await getTimelineDuration({ input, width, height })
|
|
92
|
+
if (!duration || duration <= 0) {
|
|
93
|
+
throw new Error(
|
|
94
|
+
'Could not auto-detect duration. Specify -d/--duration manually.',
|
|
95
|
+
)
|
|
80
96
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const totalFrames = Math.ceil(duration * fps)
|
|
100
|
+
|
|
101
|
+
// Start Vite server
|
|
102
|
+
const { url, tempDir, cleanup } = await createVideoServer({
|
|
103
|
+
input,
|
|
104
|
+
width,
|
|
105
|
+
height,
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
const framesDir = join(tempDir, 'frames')
|
|
109
|
+
await mkdir(framesDir, { recursive: true })
|
|
110
|
+
|
|
111
|
+
// Launch headless browser with optimal GPU config
|
|
112
|
+
const gpuConfig = await getOptimalChromiumConfig()
|
|
113
|
+
const launchOptions2 = {
|
|
114
|
+
headless: gpuConfig.headless,
|
|
115
|
+
args: gpuConfig.args,
|
|
116
|
+
}
|
|
117
|
+
if (gpuConfig.channel) {
|
|
118
|
+
launchOptions2.channel = gpuConfig.channel
|
|
119
|
+
}
|
|
120
|
+
const browser = await chromium.launch(launchOptions2)
|
|
121
|
+
|
|
122
|
+
const context = await browser.newContext({
|
|
123
|
+
viewport: { width, height },
|
|
124
|
+
deviceScaleFactor: 1,
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
const page = await context.newPage()
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
// Load the page
|
|
131
|
+
await page.goto(url, { waitUntil: 'networkidle' })
|
|
132
|
+
|
|
133
|
+
// Wait for VueSeq bridge to be ready
|
|
134
|
+
await page.waitForFunction(() => window.__VUESEQ_READY__ === true, {
|
|
135
|
+
timeout: 30000,
|
|
98
136
|
})
|
|
99
137
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
// Progress callback
|
|
134
|
-
if (onProgress) {
|
|
135
|
-
onProgress({
|
|
136
|
-
frame,
|
|
137
|
-
total: totalFrames,
|
|
138
|
-
timeInSeconds,
|
|
139
|
-
percent: Math.round((frame + 1) / totalFrames * 100)
|
|
140
|
-
})
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
} finally {
|
|
144
|
-
await browser.close()
|
|
138
|
+
// Give Vue a moment to mount and GSAP to set up timelines
|
|
139
|
+
await page.waitForTimeout(100)
|
|
140
|
+
|
|
141
|
+
// Render each frame
|
|
142
|
+
for (let frame = 0; frame < totalFrames; frame++) {
|
|
143
|
+
const timeInSeconds = frame / fps
|
|
144
|
+
|
|
145
|
+
// Seek GSAP to exact time and wait for paint
|
|
146
|
+
await page.evaluate(async (t) => {
|
|
147
|
+
window.__VUESEQ_SEEK__(t)
|
|
148
|
+
// Wait for next animation frame to ensure DOM is painted
|
|
149
|
+
await new Promise((resolve) => requestAnimationFrame(resolve))
|
|
150
|
+
}, timeInSeconds)
|
|
151
|
+
|
|
152
|
+
// Take screenshot
|
|
153
|
+
const framePath = join(
|
|
154
|
+
framesDir,
|
|
155
|
+
`frame-${String(frame).padStart(5, '0')}.png`,
|
|
156
|
+
)
|
|
157
|
+
await page.screenshot({
|
|
158
|
+
path: framePath,
|
|
159
|
+
type: 'png',
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
// Progress callback
|
|
163
|
+
if (onProgress) {
|
|
164
|
+
onProgress({
|
|
165
|
+
frame,
|
|
166
|
+
total: totalFrames,
|
|
167
|
+
timeInSeconds,
|
|
168
|
+
percent: Math.round(((frame + 1) / totalFrames) * 100),
|
|
169
|
+
})
|
|
170
|
+
}
|
|
145
171
|
}
|
|
172
|
+
} finally {
|
|
173
|
+
await browser.close()
|
|
174
|
+
}
|
|
146
175
|
|
|
147
|
-
|
|
176
|
+
return { framesDir, totalFrames, cleanup }
|
|
148
177
|
}
|