vueseq 0.1.0 → 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 +148 -53
- package/bin/cli.js +190 -70
- package/package.json +8 -3
- 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 +145 -75
- package/src/runtime/gsap-bridge.js +11 -1
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,96 +12,166 @@ 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
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Get the timeline duration from a Vue component
|
|
22
|
+
* @param {Object} options
|
|
23
|
+
* @param {string} options.input - Absolute path to the Video.vue component
|
|
24
|
+
* @param {number} [options.width=1920] - Video width in pixels
|
|
25
|
+
* @param {number} [options.height=1080] - Video height in pixels
|
|
26
|
+
* @returns {Promise<number|null>} Duration in seconds, or null if not detectable
|
|
27
|
+
*/
|
|
28
|
+
export async function getTimelineDuration(options) {
|
|
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,
|
|
54
|
+
})
|
|
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
|
+
}
|
|
66
|
+
}
|
|
15
67
|
|
|
16
68
|
/**
|
|
17
69
|
* Render frames from a Vue component
|
|
18
70
|
* @param {Object} options
|
|
19
71
|
* @param {string} options.input - Absolute path to the Video.vue component
|
|
20
72
|
* @param {number} [options.fps=30] - Frames per second
|
|
21
|
-
* @param {number} options.duration - Duration in seconds
|
|
73
|
+
* @param {number} [options.duration] - Duration in seconds (auto-detected if not provided)
|
|
22
74
|
* @param {number} [options.width=1920] - Video width in pixels
|
|
23
75
|
* @param {number} [options.height=1080] - Video height in pixels
|
|
24
76
|
* @param {function} [options.onProgress] - Progress callback
|
|
25
77
|
* @returns {Promise<{framesDir: string, totalFrames: number, cleanup: () => Promise<void>}>}
|
|
26
78
|
*/
|
|
27
79
|
export async function renderFrames(options) {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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 })
|
|
37
92
|
if (!duration || duration <= 0) {
|
|
38
|
-
|
|
93
|
+
throw new Error(
|
|
94
|
+
'Could not auto-detect duration. Specify -d/--duration manually.',
|
|
95
|
+
)
|
|
39
96
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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,
|
|
57
136
|
})
|
|
58
137
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
// Progress callback
|
|
93
|
-
if (onProgress) {
|
|
94
|
-
onProgress({
|
|
95
|
-
frame,
|
|
96
|
-
total: totalFrames,
|
|
97
|
-
timeInSeconds,
|
|
98
|
-
percent: Math.round((frame + 1) / totalFrames * 100)
|
|
99
|
-
})
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
} finally {
|
|
103
|
-
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
|
+
}
|
|
104
171
|
}
|
|
172
|
+
} finally {
|
|
173
|
+
await browser.close()
|
|
174
|
+
}
|
|
105
175
|
|
|
106
|
-
|
|
176
|
+
return { framesDir, totalFrames, cleanup }
|
|
107
177
|
}
|
|
@@ -35,7 +35,17 @@ window.__VUESEQ_SET_CONFIG__ = (config) => {
|
|
|
35
35
|
window.__VUESEQ_CONFIG__ = config
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
// 6.
|
|
38
|
+
// 6. Expose timeline duration for auto-detection
|
|
39
|
+
window.__VUESEQ_GET_DURATION__ = () => {
|
|
40
|
+
const duration = gsap.globalTimeline.duration()
|
|
41
|
+
// Return null for infinite timelines (repeat: -1)
|
|
42
|
+
if (duration === Infinity || duration > 3600) {
|
|
43
|
+
return null
|
|
44
|
+
}
|
|
45
|
+
return duration
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 7. Mark as ready after a microtask to ensure Vue is mounted
|
|
39
49
|
queueMicrotask(() => {
|
|
40
50
|
window.__VUESEQ_READY__ = true
|
|
41
51
|
})
|