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/README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<p align="center">
|
|
2
|
-
<img src="./vueseq.svg" alt="VueSeq Logo"
|
|
2
|
+
<img src="./vueseq.svg" alt="VueSeq Logo" >
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
5
|
# VueSeq
|
|
@@ -15,22 +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
|
|
|
25
|
+
https://github.com/user-attachments/assets/84d01c02-4b4f-4d86-879e-720a7e367967
|
|
21
26
|
|
|
27
|
+
_This video was rendered with VueSeq from [examples/Showcase.vue](./examples/Showcase.vue)_
|
|
22
28
|
|
|
23
|
-
|
|
29
|
+
## What Can You Build?
|
|
30
|
+
|
|
31
|
+
**Your app. As a video. Using your actual components.**
|
|
32
|
+
|
|
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
|
+
|
|
35
|
+
### 🎬 App Showcases & Demos
|
|
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.
|
|
38
|
+
|
|
39
|
+
### 📱 Social Media Content
|
|
40
|
+
|
|
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.
|
|
42
|
+
|
|
43
|
+
### 📚 Documentation & Tutorials
|
|
44
|
+
|
|
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.
|
|
46
|
+
|
|
47
|
+
### 📊 Data Visualizations
|
|
48
|
+
|
|
49
|
+
Animated charts, dashboards, infographics. Watch your bar charts grow, your line graphs draw, your data come alive.
|
|
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
|
+
|
|
55
|
+
### 🎨 Design System Demos
|
|
56
|
+
|
|
57
|
+
Showcase your component library in motion. Let designers and developers _see_ how components animate, transition, and interact.
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
**The idea is simple:** If you can build it in Vue, you can render it to video. One command. Deterministic output. Every time.
|
|
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
|
+
```
|
|
24
91
|
|
|
92
|
+
### 📦 Standalone Projects
|
|
25
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.
|
|
26
95
|
|
|
27
|
-
|
|
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
|
+
```
|
|
28
102
|
|
|
29
103
|
## Philosophy
|
|
30
104
|
|
|
31
|
-
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).
|
|
32
106
|
|
|
33
107
|
We don't include CSS frameworks (Tailwind, UnoCSS, etc.) because:
|
|
108
|
+
|
|
34
109
|
- Every developer has their preferred styling approach
|
|
35
110
|
- Your project likely already has styling configured
|
|
36
111
|
- Video components are self-contained—vanilla CSS works great
|
|
@@ -47,10 +122,7 @@ npm install vueseq
|
|
|
47
122
|
### Requirements
|
|
48
123
|
|
|
49
124
|
- Node.js 18+
|
|
50
|
-
-
|
|
51
|
-
- macOS: `brew install ffmpeg`
|
|
52
|
-
- Ubuntu/Debian: `sudo apt install ffmpeg`
|
|
53
|
-
- Windows: Download from [ffmpeg.org](https://ffmpeg.org/download.html)
|
|
125
|
+
- A modern Chromium browser (Playwright will download this automatically)
|
|
54
126
|
|
|
55
127
|
## Quick Start
|
|
56
128
|
|
|
@@ -69,25 +141,29 @@ const textRef = ref(null)
|
|
|
69
141
|
|
|
70
142
|
onMounted(() => {
|
|
71
143
|
const tl = gsap.timeline()
|
|
72
|
-
|
|
73
|
-
tl.from(boxRef.value, {
|
|
74
|
-
x: -200,
|
|
75
|
-
opacity: 0,
|
|
144
|
+
|
|
145
|
+
tl.from(boxRef.value, {
|
|
146
|
+
x: -200,
|
|
147
|
+
opacity: 0,
|
|
76
148
|
duration: 1,
|
|
77
|
-
ease: 'power2.out'
|
|
149
|
+
ease: 'power2.out',
|
|
78
150
|
})
|
|
79
|
-
|
|
80
|
-
tl.from(
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
+
|
|
87
163
|
tl.to(boxRef.value, {
|
|
88
164
|
rotation: 360,
|
|
89
165
|
duration: 2,
|
|
90
|
-
ease: 'elastic.out(1, 0.3)'
|
|
166
|
+
ease: 'elastic.out(1, 0.3)',
|
|
91
167
|
})
|
|
92
168
|
})
|
|
93
169
|
</script>
|
|
@@ -133,10 +209,19 @@ span {
|
|
|
133
209
|
### 2. Render to Video
|
|
134
210
|
|
|
135
211
|
```bash
|
|
136
|
-
npx vueseq
|
|
212
|
+
npx vueseq examples/HelloWorld.vue -o examples/hello.mp4
|
|
213
|
+
```
|
|
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
|
|
137
221
|
```
|
|
222
|
+
*Automatically detects CPU cores and scales accordingly (e.g., 60s for a 20min render).*
|
|
138
223
|
|
|
139
|
-
That's it! Your video will be rendered at 1920x1080, 30fps.
|
|
224
|
+
That's it! Duration is auto-detected from your timeline. Your video will be rendered at 1920x1080, 30fps.
|
|
140
225
|
|
|
141
226
|
## CLI Options
|
|
142
227
|
|
|
@@ -145,57 +230,67 @@ vueseq <Video.vue> [options]
|
|
|
145
230
|
|
|
146
231
|
Options:
|
|
147
232
|
-o, --output Output file (default: ./output.mp4)
|
|
148
|
-
-d, --duration Duration in seconds (
|
|
233
|
+
-d, --duration Duration in seconds (auto-detected from timeline if not specified)
|
|
149
234
|
-f, --fps Frames per second (default: 30)
|
|
150
235
|
-w, --width Video width in pixels (default: 1920)
|
|
151
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
|
|
152
243
|
--help Show help message
|
|
153
244
|
```
|
|
154
245
|
|
|
155
246
|
### Examples
|
|
156
247
|
|
|
157
248
|
```bash
|
|
158
|
-
#
|
|
159
|
-
vueseq
|
|
249
|
+
# Simple example (auto-detects duration)
|
|
250
|
+
npx vueseq examples/HelloWorld.vue -o examples/hello.mp4
|
|
251
|
+
|
|
252
|
+
# Multi-scene showcase
|
|
253
|
+
npx vueseq examples/Showcase.vue -o examples/showcase.mp4
|
|
254
|
+
|
|
255
|
+
# Override duration (partial render)
|
|
256
|
+
npx vueseq examples/Showcase.vue -d 10 -o examples/partial.mp4
|
|
160
257
|
|
|
161
258
|
# 4K at 60fps
|
|
162
|
-
vueseq
|
|
259
|
+
npx vueseq examples/HelloWorld.vue -f 60 -w 3840 -H 2160 -o examples/hello-4k.mp4
|
|
260
|
+
|
|
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
|
|
163
266
|
|
|
164
|
-
#
|
|
165
|
-
vueseq
|
|
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
|
|
166
272
|
```
|
|
167
273
|
|
|
168
274
|
## Programmatic API
|
|
169
275
|
|
|
170
276
|
```javascript
|
|
171
|
-
import { renderToMp4,
|
|
277
|
+
import { renderToMp4, isWebCodecsSupported } from 'vueseq'
|
|
172
278
|
|
|
173
|
-
//
|
|
279
|
+
// Check if WebCodecs is supported
|
|
280
|
+
const supported = await isWebCodecsSupported()
|
|
281
|
+
|
|
282
|
+
// Render directly to MP4 (WebCodecs encoding)
|
|
174
283
|
await renderToMp4({
|
|
175
284
|
input: '/path/to/MyVideo.vue',
|
|
176
|
-
duration: 5,
|
|
285
|
+
duration: 5, // Optional: auto-detected from timeline if not provided
|
|
177
286
|
fps: 30,
|
|
178
287
|
width: 1920,
|
|
179
288
|
height: 1080,
|
|
180
289
|
output: './output.mp4',
|
|
181
290
|
onProgress: ({ frame, total, percent }) => {
|
|
182
291
|
console.log(`Rendering: ${percent}%`)
|
|
183
|
-
}
|
|
292
|
+
},
|
|
184
293
|
})
|
|
185
|
-
|
|
186
|
-
// Or render frames separately for custom processing
|
|
187
|
-
const { framesDir, totalFrames, cleanup } = await renderFrames({
|
|
188
|
-
input: '/path/to/MyVideo.vue',
|
|
189
|
-
duration: 5,
|
|
190
|
-
fps: 30,
|
|
191
|
-
width: 1920,
|
|
192
|
-
height: 1080
|
|
193
|
-
})
|
|
194
|
-
|
|
195
|
-
// Process frames here...
|
|
196
|
-
|
|
197
|
-
await encodeVideo({ framesDir, output: './output.mp4', fps: 30 })
|
|
198
|
-
await cleanup()
|
|
199
294
|
```
|
|
200
295
|
|
|
201
296
|
## How It Works
|
|
@@ -206,9 +301,9 @@ VueSeq uses GSAP's deterministic timeline control:
|
|
|
206
301
|
2. **Playwright** opens it in headless Chrome
|
|
207
302
|
3. For each frame, GSAP's `globalTimeline.seek(time)` jumps to the exact moment
|
|
208
303
|
4. **Screenshot** captures the frame
|
|
209
|
-
5. **
|
|
304
|
+
5. **WebCodecs API** (via Mediabunny) encodes frames to video with hardware acceleration
|
|
210
305
|
|
|
211
|
-
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.
|
|
212
307
|
|
|
213
308
|
## Multi-Scene Videos
|
|
214
309
|
|
|
@@ -217,16 +312,16 @@ For longer videos with multiple scenes, use nested GSAP timelines:
|
|
|
217
312
|
```javascript
|
|
218
313
|
onMounted(() => {
|
|
219
314
|
const master = gsap.timeline()
|
|
220
|
-
|
|
315
|
+
|
|
221
316
|
master.add(createIntro())
|
|
222
|
-
master.add(createMainContent(), '-=0.5')
|
|
317
|
+
master.add(createMainContent(), '-=0.5') // Overlap for smooth transition
|
|
223
318
|
master.add(createOutro())
|
|
224
319
|
})
|
|
225
320
|
|
|
226
321
|
function createIntro() {
|
|
227
322
|
const tl = gsap.timeline()
|
|
228
323
|
tl.from('.title', { opacity: 0, duration: 1 })
|
|
229
|
-
tl.to({}, { duration: 2 })
|
|
324
|
+
tl.to({}, { duration: 2 }) // Hold
|
|
230
325
|
tl.to('.scene-intro', { opacity: 0, duration: 0.5 })
|
|
231
326
|
return tl
|
|
232
327
|
}
|
package/bin/cli.js
CHANGED
|
@@ -2,90 +2,123 @@
|
|
|
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
|
-
vueseq
|
|
37
|
-
vueseq
|
|
43
|
+
npx vueseq examples/HelloWorld.vue -o examples/hello.mp4
|
|
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
|
-
//
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
process.exit(1)
|
|
83
|
-
}
|
|
110
|
+
// Parse duration if provided
|
|
111
|
+
let duration = null
|
|
112
|
+
let durationAuto = false
|
|
84
113
|
|
|
85
|
-
|
|
86
|
-
|
|
114
|
+
if (values.duration) {
|
|
115
|
+
duration = parseFloat(values.duration)
|
|
116
|
+
if (isNaN(duration) || duration <= 0) {
|
|
87
117
|
console.error('Error: Duration must be a positive number')
|
|
88
118
|
process.exit(1)
|
|
119
|
+
}
|
|
120
|
+
} else {
|
|
121
|
+
durationAuto = true
|
|
89
122
|
}
|
|
90
123
|
|
|
91
124
|
// Parse numeric options
|
|
@@ -94,47 +127,134 @@ const width = parseInt(values.width)
|
|
|
94
127
|
const height = parseInt(values.height)
|
|
95
128
|
|
|
96
129
|
if (isNaN(fps) || fps <= 0) {
|
|
97
|
-
|
|
98
|
-
|
|
130
|
+
console.error('Error: FPS must be a positive number')
|
|
131
|
+
process.exit(1)
|
|
99
132
|
}
|
|
100
133
|
|
|
101
134
|
if (isNaN(width) || width <= 0 || isNaN(height) || height <= 0) {
|
|
102
|
-
|
|
103
|
-
|
|
135
|
+
console.error('Error: Width and height must be positive numbers')
|
|
136
|
+
process.exit(1)
|
|
104
137
|
}
|
|
105
138
|
|
|
106
139
|
// Import renderer and start rendering
|
|
107
|
-
console.log(`\nVueSeq - Rendering ${input}`)
|
|
108
|
-
console.log(` Duration: ${duration}s at ${fps}fps (${Math.ceil(duration * fps)} frames)`)
|
|
109
|
-
console.log(` Resolution: ${width}x${height}`)
|
|
110
|
-
console.log(` Output: ${values.output}\n`)
|
|
111
|
-
|
|
112
140
|
try {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
+
}
|
|
158
|
+
|
|
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,
|
|
132
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
|
+
}
|
|
133
209
|
|
|
134
|
-
|
|
135
|
-
|
|
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`)
|
|
136
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)`)
|
|
137
254
|
} catch (error) {
|
|
138
|
-
|
|
139
|
-
|
|
255
|
+
console.error(`\nError: ${error.message} `)
|
|
256
|
+
process.exit(1)
|
|
140
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
|
},
|
|
@@ -51,4 +56,4 @@
|
|
|
51
56
|
"engines": {
|
|
52
57
|
"node": ">=18.0.0"
|
|
53
58
|
}
|
|
54
|
-
}
|
|
59
|
+
}
|
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'
|