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/README.md
CHANGED
|
@@ -15,48 +15,97 @@ Render Vue components with GSAP animations to video. Write standard Vue + GSAP c
|
|
|
15
15
|
- ✅ **Simple CLI** - One command to render your video
|
|
16
16
|
- ✅ **Programmatic API** - Integrate into your build pipeline
|
|
17
17
|
- ✅ **Full GSAP Power** - All easing, timelines, and plugins work
|
|
18
|
+
- ✅ **Parallel Rendering** - Distributed capture across CPU cores (20x faster)
|
|
19
|
+
- ✅ **In-Browser Capture** - Zero-copy absolute performance
|
|
20
|
+
- ✅ **Auto-Scaling** - Automatically utilizes all available CPU cores
|
|
21
|
+
- ✅ **WebCodecs Encoding** - Hardware-accelerated H.264 encoding, no FFmpeg required
|
|
18
22
|
|
|
19
23
|
## Demo
|
|
20
24
|
|
|
21
|
-
|
|
22
|
-
|
|
23
25
|
https://github.com/user-attachments/assets/84d01c02-4b4f-4d86-879e-720a7e367967
|
|
24
26
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
*This video was rendered with VueSeq from [examples/HelloWorld.vue](./examples/HelloWorld.vue)*
|
|
27
|
+
_This video was rendered with VueSeq from [examples/Showcase.vue](./examples/Showcase.vue)_
|
|
28
28
|
|
|
29
29
|
## What Can You Build?
|
|
30
30
|
|
|
31
31
|
**Your app. As a video. Using your actual components.**
|
|
32
32
|
|
|
33
|
-
Install VueSeq into your Vue project and create stunning videos that perfectly match your brand—because they
|
|
33
|
+
Install VueSeq into your Vue project and create stunning videos that perfectly match your brand—because they _are_ your brand. No recreating UIs in After Effects. No screen recording artifacts. Just your components, animated with GSAP, rendered to pixel-perfect video.
|
|
34
34
|
|
|
35
35
|
### 🎬 App Showcases & Demos
|
|
36
|
+
|
|
36
37
|
Create promotional videos using your real UI components. Product tours, feature walkthroughs, "What's New in v2.0" announcements—all with your exact design system, your exact colors, your exact components.
|
|
37
38
|
|
|
38
39
|
### 📱 Social Media Content
|
|
40
|
+
|
|
39
41
|
Generate short-form videos for Twitter, LinkedIn, Instagram. Show off a feature in 15 seconds. Your followers see your actual product, not a mockup.
|
|
40
42
|
|
|
41
|
-
### 📚 Documentation & Tutorials
|
|
43
|
+
### 📚 Documentation & Tutorials
|
|
44
|
+
|
|
42
45
|
Animate how your features work. Show state transitions, user flows, component interactions. Embed videos in your docs that never go stale—regenerate them when your UI changes.
|
|
43
46
|
|
|
44
47
|
### 📊 Data Visualizations
|
|
48
|
+
|
|
45
49
|
Animated charts, dashboards, infographics. Watch your bar charts grow, your line graphs draw, your data come alive.
|
|
46
50
|
|
|
51
|
+
### 🎓 Educational Videos
|
|
52
|
+
|
|
53
|
+
Create animated explainers, course content, and training materials. Complex concepts become clear with step-by-step animated infographics and interactive walkthroughs.
|
|
54
|
+
|
|
47
55
|
### 🎨 Design System Demos
|
|
48
|
-
|
|
56
|
+
|
|
57
|
+
Showcase your component library in motion. Let designers and developers _see_ how components animate, transition, and interact.
|
|
49
58
|
|
|
50
59
|
---
|
|
51
60
|
|
|
52
61
|
**The idea is simple:** If you can build it in Vue, you can render it to video. One command. Deterministic output. Every time.
|
|
53
62
|
|
|
63
|
+
## Integration Modes
|
|
64
|
+
|
|
65
|
+
### 🔌 Add to Your Existing App
|
|
66
|
+
|
|
67
|
+
Install VueSeq into your Vue project and create videos using your existing components and design system. Import your buttons, cards, charts—anything you've already built. Your videos will match your app perfectly because they _are_ your app.
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
npm install vueseq
|
|
71
|
+
# Create a video component that imports your existing components
|
|
72
|
+
npx vueseq src/videos/ProductDemo.vue -o demo.mp4
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### 🤖 Programmatic / AI-Generated Videos
|
|
76
|
+
|
|
77
|
+
Use the API to generate videos programmatically. Perfect for:
|
|
78
|
+
|
|
79
|
+
- **AI pipelines**: Generate videos from LLM-created storyboards
|
|
80
|
+
- **Automated content**: Create personalized videos at scale
|
|
81
|
+
- **CI/CD integration**: Regenerate demo videos on every release
|
|
82
|
+
|
|
83
|
+
```javascript
|
|
84
|
+
import { renderToMp4 } from 'vueseq'
|
|
85
|
+
|
|
86
|
+
await renderToMp4({
|
|
87
|
+
input: '/path/to/GeneratedVideo.vue',
|
|
88
|
+
output: 'output.mp4',
|
|
89
|
+
})
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### 📦 Standalone Projects
|
|
93
|
+
|
|
94
|
+
Create a dedicated video project from scratch. Ideal for marketing teams, content creators, or anyone who wants to produce videos without an existing Vue app.
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
mkdir my-video && cd my-video
|
|
98
|
+
npm init -y && npm install vueseq
|
|
99
|
+
# Create your video component and render
|
|
100
|
+
npx vueseq Video.vue -o video.mp4
|
|
101
|
+
```
|
|
54
102
|
|
|
55
103
|
## Philosophy
|
|
56
104
|
|
|
57
|
-
VueSeq is intentionally minimal. We bundle only the essentials: **Vue**, **GSAP**, **Playwright**, and **
|
|
105
|
+
VueSeq is intentionally minimal. We bundle only the essentials: **Vue**, **GSAP**, **Playwright**, **Vite**, and **Mediabunny** (for WebCodecs-based encoding).
|
|
58
106
|
|
|
59
107
|
We don't include CSS frameworks (Tailwind, UnoCSS, etc.) because:
|
|
108
|
+
|
|
60
109
|
- Every developer has their preferred styling approach
|
|
61
110
|
- Your project likely already has styling configured
|
|
62
111
|
- Video components are self-contained—vanilla CSS works great
|
|
@@ -73,10 +122,7 @@ npm install vueseq
|
|
|
73
122
|
### Requirements
|
|
74
123
|
|
|
75
124
|
- Node.js 18+
|
|
76
|
-
-
|
|
77
|
-
- macOS: `brew install ffmpeg`
|
|
78
|
-
- Ubuntu/Debian: `sudo apt install ffmpeg`
|
|
79
|
-
- Windows: Download from [ffmpeg.org](https://ffmpeg.org/download.html)
|
|
125
|
+
- A modern Chromium browser (Playwright will download this automatically)
|
|
80
126
|
|
|
81
127
|
## Quick Start
|
|
82
128
|
|
|
@@ -95,25 +141,29 @@ const textRef = ref(null)
|
|
|
95
141
|
|
|
96
142
|
onMounted(() => {
|
|
97
143
|
const tl = gsap.timeline()
|
|
98
|
-
|
|
99
|
-
tl.from(boxRef.value, {
|
|
100
|
-
x: -200,
|
|
101
|
-
opacity: 0,
|
|
144
|
+
|
|
145
|
+
tl.from(boxRef.value, {
|
|
146
|
+
x: -200,
|
|
147
|
+
opacity: 0,
|
|
102
148
|
duration: 1,
|
|
103
|
-
ease: 'power2.out'
|
|
149
|
+
ease: 'power2.out',
|
|
104
150
|
})
|
|
105
|
-
|
|
106
|
-
tl.from(
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
151
|
+
|
|
152
|
+
tl.from(
|
|
153
|
+
textRef.value,
|
|
154
|
+
{
|
|
155
|
+
y: 50,
|
|
156
|
+
opacity: 0,
|
|
157
|
+
duration: 0.8,
|
|
158
|
+
ease: 'back.out',
|
|
159
|
+
},
|
|
160
|
+
'-=0.3',
|
|
161
|
+
)
|
|
162
|
+
|
|
113
163
|
tl.to(boxRef.value, {
|
|
114
164
|
rotation: 360,
|
|
115
165
|
duration: 2,
|
|
116
|
-
ease: 'elastic.out(1, 0.3)'
|
|
166
|
+
ease: 'elastic.out(1, 0.3)',
|
|
117
167
|
})
|
|
118
168
|
})
|
|
119
169
|
</script>
|
|
@@ -162,6 +212,15 @@ span {
|
|
|
162
212
|
npx vueseq examples/HelloWorld.vue -o examples/hello.mp4
|
|
163
213
|
```
|
|
164
214
|
|
|
215
|
+
### 3. Parallel Rendering (Recommended for Speed)
|
|
216
|
+
|
|
217
|
+
For complex animations or long videos, use parallel rendering to utilize all CPU cores:
|
|
218
|
+
|
|
219
|
+
```bash
|
|
220
|
+
npx vueseq examples/Showcase.vue --parallel
|
|
221
|
+
```
|
|
222
|
+
*Automatically detects CPU cores and scales accordingly (e.g., 60s for a 20min render).*
|
|
223
|
+
|
|
165
224
|
That's it! Duration is auto-detected from your timeline. Your video will be rendered at 1920x1080, 30fps.
|
|
166
225
|
|
|
167
226
|
## CLI Options
|
|
@@ -175,6 +234,12 @@ Options:
|
|
|
175
234
|
-f, --fps Frames per second (default: 30)
|
|
176
235
|
-w, --width Video width in pixels (default: 1920)
|
|
177
236
|
-H, --height Video height in pixels (default: 1080)
|
|
237
|
+
--parallel Use parallel rendering (multi-process) [Recommended]
|
|
238
|
+
--workers Number of workers (default: auto-detected, use with --parallel)
|
|
239
|
+
--optimized Use optimized single-page in-browser capture
|
|
240
|
+
--gpu-backend Force GPU backend: vulkan, metal, d3d11, software (default: auto)
|
|
241
|
+
--monitor-memory Log memory usage
|
|
242
|
+
-v, --version Show version number
|
|
178
243
|
--help Show help message
|
|
179
244
|
```
|
|
180
245
|
|
|
@@ -193,41 +258,39 @@ npx vueseq examples/Showcase.vue -d 10 -o examples/partial.mp4
|
|
|
193
258
|
# 4K at 60fps
|
|
194
259
|
npx vueseq examples/HelloWorld.vue -f 60 -w 3840 -H 2160 -o examples/hello-4k.mp4
|
|
195
260
|
|
|
196
|
-
#
|
|
197
|
-
npx vueseq examples/
|
|
261
|
+
# Max Performance (Parallel + Auto-Scaling)
|
|
262
|
+
npx vueseq examples/Showcase.vue --parallel -o examples/showcase.mp4
|
|
263
|
+
|
|
264
|
+
# Dedicated Optimized Single-Page (Low Specs)
|
|
265
|
+
npx vueseq examples/Showcase.vue --optimized -o examples/showcase.mp4
|
|
266
|
+
|
|
267
|
+
# Force Vulkan Backend
|
|
268
|
+
npx vueseq examples/Showcase.vue --gpu-backend vulkan --parallel
|
|
269
|
+
|
|
270
|
+
# 4K at 60fps
|
|
271
|
+
npx vueseq examples/HelloWorld.vue -f 60 -w 3840 -H 2160 -o examples/hello-4k.mp4
|
|
198
272
|
```
|
|
199
273
|
|
|
200
274
|
## Programmatic API
|
|
201
275
|
|
|
202
276
|
```javascript
|
|
203
|
-
import { renderToMp4,
|
|
277
|
+
import { renderToMp4, isWebCodecsSupported } from 'vueseq'
|
|
278
|
+
|
|
279
|
+
// Check if WebCodecs is supported
|
|
280
|
+
const supported = await isWebCodecsSupported()
|
|
204
281
|
|
|
205
|
-
// Render directly to MP4
|
|
282
|
+
// Render directly to MP4 (WebCodecs encoding)
|
|
206
283
|
await renderToMp4({
|
|
207
284
|
input: '/path/to/MyVideo.vue',
|
|
208
|
-
duration: 5,
|
|
285
|
+
duration: 5, // Optional: auto-detected from timeline if not provided
|
|
209
286
|
fps: 30,
|
|
210
287
|
width: 1920,
|
|
211
288
|
height: 1080,
|
|
212
289
|
output: './output.mp4',
|
|
213
290
|
onProgress: ({ frame, total, percent }) => {
|
|
214
291
|
console.log(`Rendering: ${percent}%`)
|
|
215
|
-
}
|
|
216
|
-
})
|
|
217
|
-
|
|
218
|
-
// Or render frames separately for custom processing
|
|
219
|
-
const { framesDir, totalFrames, cleanup } = await renderFrames({
|
|
220
|
-
input: '/path/to/MyVideo.vue',
|
|
221
|
-
duration: 5,
|
|
222
|
-
fps: 30,
|
|
223
|
-
width: 1920,
|
|
224
|
-
height: 1080
|
|
292
|
+
},
|
|
225
293
|
})
|
|
226
|
-
|
|
227
|
-
// Process frames here...
|
|
228
|
-
|
|
229
|
-
await encodeVideo({ framesDir, output: './output.mp4', fps: 30 })
|
|
230
|
-
await cleanup()
|
|
231
294
|
```
|
|
232
295
|
|
|
233
296
|
## How It Works
|
|
@@ -238,9 +301,9 @@ VueSeq uses GSAP's deterministic timeline control:
|
|
|
238
301
|
2. **Playwright** opens it in headless Chrome
|
|
239
302
|
3. For each frame, GSAP's `globalTimeline.seek(time)` jumps to the exact moment
|
|
240
303
|
4. **Screenshot** captures the frame
|
|
241
|
-
5. **
|
|
304
|
+
5. **WebCodecs API** (via Mediabunny) encodes frames to video with hardware acceleration
|
|
242
305
|
|
|
243
|
-
This is deterministic because `seek()` applies all GSAP values synchronously—given the same time, you get the exact same DOM state every time.
|
|
306
|
+
This is deterministic because `seek()` applies all GSAP values synchronously—given the same time, you get the exact same DOM state every time. The WebCodecs API provides hardware-accelerated H.264 encoding without requiring FFmpeg.
|
|
244
307
|
|
|
245
308
|
## Multi-Scene Videos
|
|
246
309
|
|
|
@@ -249,16 +312,16 @@ For longer videos with multiple scenes, use nested GSAP timelines:
|
|
|
249
312
|
```javascript
|
|
250
313
|
onMounted(() => {
|
|
251
314
|
const master = gsap.timeline()
|
|
252
|
-
|
|
315
|
+
|
|
253
316
|
master.add(createIntro())
|
|
254
|
-
master.add(createMainContent(), '-=0.5')
|
|
317
|
+
master.add(createMainContent(), '-=0.5') // Overlap for smooth transition
|
|
255
318
|
master.add(createOutro())
|
|
256
319
|
})
|
|
257
320
|
|
|
258
321
|
function createIntro() {
|
|
259
322
|
const tl = gsap.timeline()
|
|
260
323
|
tl.from('.title', { opacity: 0, duration: 1 })
|
|
261
|
-
tl.to({}, { duration: 2 })
|
|
324
|
+
tl.to({}, { duration: 2 }) // Hold
|
|
262
325
|
tl.to('.scene-intro', { opacity: 0, duration: 0.5 })
|
|
263
326
|
return tl
|
|
264
327
|
}
|
package/bin/cli.js
CHANGED
|
@@ -2,78 +2,109 @@
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* VueSeq CLI
|
|
5
|
-
*
|
|
5
|
+
*
|
|
6
6
|
* Render Vue + GSAP components to video.
|
|
7
|
-
*
|
|
7
|
+
*
|
|
8
8
|
* Usage:
|
|
9
9
|
* vueseq <Video.vue> [options]
|
|
10
|
-
*
|
|
10
|
+
*
|
|
11
11
|
* Example:
|
|
12
12
|
* vueseq MyAnimation.vue -d 5 -o my-video.mp4
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import { parseArgs } from 'node:util'
|
|
16
16
|
import { resolve, extname } from 'node:path'
|
|
17
|
-
import { existsSync } from 'node:fs'
|
|
17
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
18
18
|
|
|
19
19
|
// Show help text
|
|
20
20
|
function showHelp() {
|
|
21
|
-
|
|
21
|
+
console.log(`
|
|
22
22
|
VueSeq - Render Vue + GSAP components to video
|
|
23
23
|
|
|
24
24
|
USAGE:
|
|
25
25
|
vueseq <Video.vue> [options]
|
|
26
26
|
|
|
27
27
|
OPTIONS:
|
|
28
|
-
-o, --output
|
|
29
|
-
-d, --duration
|
|
30
|
-
-f, --fps
|
|
31
|
-
-w, --width
|
|
32
|
-
-H, --height
|
|
33
|
-
--
|
|
28
|
+
-o, --output Output file (default: ./output.mp4)
|
|
29
|
+
-d, --duration Duration in seconds (auto-detected if not specified)
|
|
30
|
+
-f, --fps Frames per second (default: 30)
|
|
31
|
+
-w, --width Video width in pixels (default: 1920)
|
|
32
|
+
-H, --height Video height in pixels (default: 1080)
|
|
33
|
+
--gpu-backend GPU backend: auto, vulkan, egl, metal, d3d11, software (default: auto)
|
|
34
|
+
--optimized Use optimized in-browser capture (eliminates PNG overhead)
|
|
35
|
+
--parallel Use parallel frame capture with multiple browser pages
|
|
36
|
+
--workers Number of parallel workers (default: auto-detected cores)
|
|
37
|
+
--monitor-memory Log memory usage during rendering
|
|
38
|
+
--benchmark Compare original vs optimized render methods
|
|
39
|
+
-v, --version Show version number
|
|
40
|
+
--help Show this help message
|
|
34
41
|
|
|
35
42
|
EXAMPLE:
|
|
36
43
|
npx vueseq examples/HelloWorld.vue -o examples/hello.mp4
|
|
37
|
-
npx vueseq examples/
|
|
44
|
+
npx vueseq examples/HelloWorld.vue --optimized -o examples/hello.mp4
|
|
45
|
+
npx vueseq examples/Showcase.vue --parallel --workers 4 --monitor-memory
|
|
46
|
+
|
|
47
|
+
GPU DIAGNOSTICS:
|
|
48
|
+
node test-gpu.js # Run full GPU diagnostics
|
|
49
|
+
node test-gpu.js --benchmark # Include render benchmarks
|
|
38
50
|
`)
|
|
39
51
|
}
|
|
40
52
|
|
|
41
53
|
// Parse command line arguments
|
|
42
54
|
const { values, positionals } = parseArgs({
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
55
|
+
allowPositionals: true,
|
|
56
|
+
options: {
|
|
57
|
+
output: { type: 'string', short: 'o', default: './output.mp4' },
|
|
58
|
+
duration: { type: 'string', short: 'd' },
|
|
59
|
+
fps: { type: 'string', short: 'f', default: '30' },
|
|
60
|
+
width: { type: 'string', short: 'w', default: '1920' },
|
|
61
|
+
height: { type: 'string', short: 'H', default: '1080' },
|
|
62
|
+
'gpu-backend': { type: 'string', default: 'auto' },
|
|
63
|
+
optimized: { type: 'boolean', default: false },
|
|
64
|
+
parallel: { type: 'boolean', default: false },
|
|
65
|
+
workers: { type: 'string' },
|
|
66
|
+
'monitor-memory': { type: 'boolean', default: false },
|
|
67
|
+
benchmark: { type: 'boolean', default: false },
|
|
68
|
+
version: { type: 'boolean', short: 'v' },
|
|
69
|
+
help: { type: 'boolean' },
|
|
70
|
+
},
|
|
52
71
|
})
|
|
53
72
|
|
|
73
|
+
// Show version if requested
|
|
74
|
+
if (values.version) {
|
|
75
|
+
try {
|
|
76
|
+
const pkgPath = new URL('../package.json', import.meta.url)
|
|
77
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'))
|
|
78
|
+
console.log(`vueseq v${pkg.version}`)
|
|
79
|
+
} catch (e) {
|
|
80
|
+
console.error('Error reading version:', e.message)
|
|
81
|
+
}
|
|
82
|
+
process.exit(0)
|
|
83
|
+
}
|
|
84
|
+
|
|
54
85
|
// Show help if requested
|
|
55
86
|
if (values.help) {
|
|
56
|
-
|
|
57
|
-
|
|
87
|
+
showHelp()
|
|
88
|
+
process.exit(0)
|
|
58
89
|
}
|
|
59
90
|
|
|
60
91
|
// Validate input file
|
|
61
92
|
const input = positionals[0]
|
|
62
93
|
if (!input) {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
94
|
+
console.error('Error: Please specify a .vue file\n')
|
|
95
|
+
showHelp()
|
|
96
|
+
process.exit(1)
|
|
66
97
|
}
|
|
67
98
|
|
|
68
99
|
const inputPath = resolve(input)
|
|
69
100
|
if (!existsSync(inputPath)) {
|
|
70
|
-
|
|
71
|
-
|
|
101
|
+
console.error(`Error: File not found: ${inputPath}`)
|
|
102
|
+
process.exit(1)
|
|
72
103
|
}
|
|
73
104
|
|
|
74
105
|
if (extname(inputPath) !== '.vue') {
|
|
75
|
-
|
|
76
|
-
|
|
106
|
+
console.error('Error: Input must be a .vue file')
|
|
107
|
+
process.exit(1)
|
|
77
108
|
}
|
|
78
109
|
|
|
79
110
|
// Parse duration if provided
|
|
@@ -81,13 +112,13 @@ let duration = null
|
|
|
81
112
|
let durationAuto = false
|
|
82
113
|
|
|
83
114
|
if (values.duration) {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
115
|
+
duration = parseFloat(values.duration)
|
|
116
|
+
if (isNaN(duration) || duration <= 0) {
|
|
117
|
+
console.error('Error: Duration must be a positive number')
|
|
118
|
+
process.exit(1)
|
|
119
|
+
}
|
|
89
120
|
} else {
|
|
90
|
-
|
|
121
|
+
durationAuto = true
|
|
91
122
|
}
|
|
92
123
|
|
|
93
124
|
// Parse numeric options
|
|
@@ -96,59 +127,134 @@ const width = parseInt(values.width)
|
|
|
96
127
|
const height = parseInt(values.height)
|
|
97
128
|
|
|
98
129
|
if (isNaN(fps) || fps <= 0) {
|
|
99
|
-
|
|
100
|
-
|
|
130
|
+
console.error('Error: FPS must be a positive number')
|
|
131
|
+
process.exit(1)
|
|
101
132
|
}
|
|
102
133
|
|
|
103
134
|
if (isNaN(width) || width <= 0 || isNaN(height) || height <= 0) {
|
|
104
|
-
|
|
105
|
-
|
|
135
|
+
console.error('Error: Width and height must be positive numbers')
|
|
136
|
+
process.exit(1)
|
|
106
137
|
}
|
|
107
138
|
|
|
108
139
|
// Import renderer and start rendering
|
|
109
140
|
try {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
141
|
+
const { renderToMp4, renderToMp4Optimized, benchmarkRenderMethods } =
|
|
142
|
+
await import('../src/renderer/encode.js')
|
|
143
|
+
const { renderToMp4Parallel } = await import(
|
|
144
|
+
'../src/renderer/encode-parallel.js'
|
|
145
|
+
)
|
|
146
|
+
const { getTimelineDuration } = await import('../src/renderer/render.js')
|
|
147
|
+
const { detectBestGPUConfig, clearGPUCache } = await import(
|
|
148
|
+
'../src/renderer/gpu.js'
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
// Get GPU backend preference
|
|
152
|
+
const gpuBackend = values['gpu-backend'] || 'auto'
|
|
153
|
+
|
|
154
|
+
// Clear cache if specific backend requested (force re-detection)
|
|
155
|
+
if (gpuBackend !== 'auto') {
|
|
156
|
+
await clearGPUCache()
|
|
157
|
+
}
|
|
123
158
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
process.stdout.write(`\rRendering: ${percent}% (${frame + 1}/${total} frames)`)
|
|
144
|
-
}
|
|
145
|
-
}
|
|
159
|
+
// Check GPU acceleration status
|
|
160
|
+
const gpuConfig = await detectBestGPUConfig({
|
|
161
|
+
preferBackend: gpuBackend !== 'auto' ? gpuBackend : undefined,
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
const gpuIcon = gpuConfig.isHardwareAccelerated ? '✓' : '○'
|
|
165
|
+
const gpuMode = gpuConfig.isHardwareAccelerated ? 'Hardware' : 'Software'
|
|
166
|
+
console.log(`\nGPU: ${gpuIcon} ${gpuMode}(${gpuConfig.label})`)
|
|
167
|
+
console.log(` Renderer: ${gpuConfig.renderer}`)
|
|
168
|
+
|
|
169
|
+
// Run benchmark mode if requested
|
|
170
|
+
if (values.benchmark) {
|
|
171
|
+
console.log('\n📊 Running render benchmark...')
|
|
172
|
+
await benchmarkRenderMethods({
|
|
173
|
+
input: inputPath,
|
|
174
|
+
duration: duration || 2,
|
|
175
|
+
fps,
|
|
176
|
+
width,
|
|
177
|
+
height,
|
|
146
178
|
})
|
|
179
|
+
process.exit(0)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Auto-detect duration if not provided
|
|
183
|
+
if (durationAuto) {
|
|
184
|
+
console.log(`\nVueSeq - Detecting timeline duration...`)
|
|
185
|
+
duration = await getTimelineDuration({ input: inputPath, width, height })
|
|
186
|
+
if (!duration || duration <= 0) {
|
|
187
|
+
console.error(
|
|
188
|
+
'Error: Could not auto-detect duration. Use -d to specify manually.',
|
|
189
|
+
)
|
|
190
|
+
process.exit(1)
|
|
191
|
+
}
|
|
192
|
+
console.log(` Auto - detected: ${duration.toFixed(2)}s`)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Select renderer based on flags
|
|
196
|
+
const useParallel = values.parallel
|
|
197
|
+
const useOptimized = values.optimized
|
|
198
|
+
const numWorkers = values.workers ? parseInt(values.workers, 10) : undefined
|
|
199
|
+
const monitorMemory = values['monitor-memory']
|
|
200
|
+
|
|
201
|
+
let renderMethod
|
|
202
|
+
if (useParallel) {
|
|
203
|
+
renderMethod = `Parallel(${numWorkers || 'Auto'} workers)`
|
|
204
|
+
} else if (useOptimized) {
|
|
205
|
+
renderMethod = 'Optimized (in-browser capture)'
|
|
206
|
+
} else {
|
|
207
|
+
renderMethod = 'Standard (PNG-based)'
|
|
208
|
+
}
|
|
147
209
|
|
|
148
|
-
|
|
149
|
-
|
|
210
|
+
console.log(`\nVueSeq - Rendering ${input} `)
|
|
211
|
+
console.log(` Method: ${renderMethod} `)
|
|
212
|
+
console.log(
|
|
213
|
+
` Duration: ${duration}s at ${fps} fps(${Math.ceil(duration * fps)} frames)${durationAuto ? ' (auto)' : ''} `,
|
|
214
|
+
)
|
|
215
|
+
console.log(` Resolution: ${width}x${height} `)
|
|
216
|
+
console.log(` Output: ${values.output} \n`)
|
|
150
217
|
|
|
218
|
+
const startTime = Date.now()
|
|
219
|
+
let lastLoggedPercent = -1
|
|
220
|
+
|
|
221
|
+
// Select render function
|
|
222
|
+
let renderFn
|
|
223
|
+
if (useParallel) {
|
|
224
|
+
renderFn = renderToMp4Parallel
|
|
225
|
+
} else if (useOptimized) {
|
|
226
|
+
renderFn = renderToMp4Optimized
|
|
227
|
+
} else {
|
|
228
|
+
renderFn = renderToMp4
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
await renderFn({
|
|
232
|
+
input: inputPath,
|
|
233
|
+
duration,
|
|
234
|
+
fps,
|
|
235
|
+
width,
|
|
236
|
+
height,
|
|
237
|
+
output: values.output,
|
|
238
|
+
workers: numWorkers,
|
|
239
|
+
monitorMemory,
|
|
240
|
+
onProgress: ({ frame, total, percent, workerId }) => {
|
|
241
|
+
// Only log every 5% to reduce noise
|
|
242
|
+
if (percent % 5 === 0 && percent !== lastLoggedPercent) {
|
|
243
|
+
lastLoggedPercent = percent
|
|
244
|
+
const workerInfo = workerId !== undefined ? ` [W${workerId}]` : ''
|
|
245
|
+
process.stdout.write(
|
|
246
|
+
`\rRendering: ${percent}% (${frame + 1}/${total} frames)${workerInfo} `,
|
|
247
|
+
)
|
|
248
|
+
}
|
|
249
|
+
},
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1)
|
|
253
|
+
console.log(`\n\n✓ Video saved to ${values.output} (${elapsed}s)`)
|
|
151
254
|
} catch (error) {
|
|
152
|
-
|
|
153
|
-
|
|
255
|
+
console.error(`\nError: ${error.message} `)
|
|
256
|
+
process.exit(1)
|
|
154
257
|
}
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vueseq",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "A minimal, deterministic video renderer for Vue 3 + GSAP",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -38,10 +38,15 @@
|
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"gsap": "^3.12.0",
|
|
41
|
-
"
|
|
41
|
+
"html2canvas": "^1.4.1",
|
|
42
|
+
"mediabunny": "^1.31.0",
|
|
43
|
+
"playwright": "^1.58.0"
|
|
42
44
|
},
|
|
43
45
|
"devDependencies": {
|
|
46
|
+
"@semantic-release/changelog": "^6.0.3",
|
|
47
|
+
"@semantic-release/git": "^10.0.1",
|
|
44
48
|
"@vitejs/plugin-vue": "^5.0.0",
|
|
49
|
+
"semantic-release": "^25.0.2",
|
|
45
50
|
"vite": "^5.0.0",
|
|
46
51
|
"vue": "^3.4.0"
|
|
47
52
|
},
|
package/src/index.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* VueSeq - Public API
|
|
3
|
-
*
|
|
3
|
+
*
|
|
4
4
|
* A minimal, deterministic video renderer for Vue 3 + GSAP.
|
|
5
|
+
* Uses WebCodecs API for hardware-accelerated video encoding.
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
8
|
export { renderFrames } from './renderer/render.js'
|
|
8
|
-
export {
|
|
9
|
+
export { renderToMp4, isWebCodecsSupported } from './renderer/encode.js'
|
|
9
10
|
export { createVideoServer } from './bundler/vite.js'
|