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
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FFmpeg Encoder (Legacy)
|
|
3
|
+
*
|
|
4
|
+
* Kept for backward compatibility. WebCodecs is now the default.
|
|
5
|
+
* Requires FFmpeg to be installed on the system.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { spawn } from 'child_process'
|
|
9
|
+
import { join } from 'path'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Encode frames to video using FFmpeg
|
|
13
|
+
* @param {Object} options
|
|
14
|
+
* @param {string} options.framesDir - Directory containing frame-XXXXX.png files
|
|
15
|
+
* @param {string} options.output - Output video file path
|
|
16
|
+
* @param {number} [options.fps=30] - Frames per second
|
|
17
|
+
* @returns {Promise<string>} - Path to the output video
|
|
18
|
+
* @deprecated Use WebCodecs-based encoding instead
|
|
19
|
+
*/
|
|
20
|
+
export function encodeVideo({ framesDir, output, fps = 30 }) {
|
|
21
|
+
console.warn('FFmpeg encoding is deprecated. WebCodecs is now the default.')
|
|
22
|
+
|
|
23
|
+
return new Promise((resolve, reject) => {
|
|
24
|
+
const args = [
|
|
25
|
+
'-y', // Overwrite output file without asking
|
|
26
|
+
'-framerate',
|
|
27
|
+
String(fps),
|
|
28
|
+
'-i',
|
|
29
|
+
join(framesDir, 'frame-%05d.png'),
|
|
30
|
+
'-c:v',
|
|
31
|
+
'libx264',
|
|
32
|
+
'-pix_fmt',
|
|
33
|
+
'yuv420p', // Compatibility with most players
|
|
34
|
+
'-preset',
|
|
35
|
+
'fast',
|
|
36
|
+
'-crf',
|
|
37
|
+
'18', // High quality (lower = better, 18-23 is good range)
|
|
38
|
+
output,
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
const ffmpeg = spawn('ffmpeg', args, {
|
|
42
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
let stderr = ''
|
|
46
|
+
ffmpeg.stderr.on('data', (data) => {
|
|
47
|
+
stderr += data.toString()
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
ffmpeg.on('close', (code) => {
|
|
51
|
+
if (code === 0) {
|
|
52
|
+
resolve(output)
|
|
53
|
+
} else {
|
|
54
|
+
reject(new Error(`FFmpeg exited with code ${code}\n${stderr}`))
|
|
55
|
+
}
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
ffmpeg.on('error', (err) => {
|
|
59
|
+
if (err.code === 'ENOENT') {
|
|
60
|
+
reject(
|
|
61
|
+
new Error(
|
|
62
|
+
'FFmpeg not found. Please install FFmpeg or use WebCodecs (default).',
|
|
63
|
+
),
|
|
64
|
+
)
|
|
65
|
+
} else {
|
|
66
|
+
reject(new Error(`FFmpeg error: ${err.message}`))
|
|
67
|
+
}
|
|
68
|
+
})
|
|
69
|
+
})
|
|
70
|
+
}
|
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GPU Detection and Configuration Module
|
|
3
|
+
*
|
|
4
|
+
* Provides adaptive GPU acceleration by detecting the optimal
|
|
5
|
+
* GPU backend for the current system and providing appropriate
|
|
6
|
+
* Chromium flags.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { chromium } from 'playwright'
|
|
10
|
+
import { platform } from 'os'
|
|
11
|
+
import { writeFile, readFile, mkdir } from 'fs/promises'
|
|
12
|
+
import { join } from 'path'
|
|
13
|
+
import { tmpdir } from 'os'
|
|
14
|
+
|
|
15
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
16
|
+
// GPU Configuration Profiles (ordered by priority)
|
|
17
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
18
|
+
|
|
19
|
+
const GPU_BACKENDS = {
|
|
20
|
+
linux: [
|
|
21
|
+
{
|
|
22
|
+
name: 'vulkan',
|
|
23
|
+
label: 'Vulkan (NVIDIA/AMD)',
|
|
24
|
+
// Use channel: 'chromium' for new headless mode with GPU support
|
|
25
|
+
channel: 'chromium',
|
|
26
|
+
headless: true,
|
|
27
|
+
args: [
|
|
28
|
+
'--no-sandbox',
|
|
29
|
+
'--disable-background-timer-throttling',
|
|
30
|
+
'--disable-backgrounding-occluded-windows',
|
|
31
|
+
'--disable-renderer-backgrounding',
|
|
32
|
+
'--disable-ipc-flooding-protection',
|
|
33
|
+
'--use-angle=vulkan',
|
|
34
|
+
'--enable-features=Vulkan',
|
|
35
|
+
'--disable-vulkan-surface',
|
|
36
|
+
'--enable-unsafe-webgpu',
|
|
37
|
+
'--ignore-gpu-blocklist',
|
|
38
|
+
],
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: 'egl',
|
|
42
|
+
label: 'EGL (Intel/Mesa)',
|
|
43
|
+
channel: 'chromium',
|
|
44
|
+
headless: true,
|
|
45
|
+
args: [
|
|
46
|
+
'--no-sandbox',
|
|
47
|
+
'--disable-background-timer-throttling',
|
|
48
|
+
'--disable-backgrounding-occluded-windows',
|
|
49
|
+
'--disable-renderer-backgrounding',
|
|
50
|
+
'--use-gl=egl',
|
|
51
|
+
'--ignore-gpu-blocklist',
|
|
52
|
+
],
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: 'desktop',
|
|
56
|
+
label: 'Desktop OpenGL',
|
|
57
|
+
channel: 'chromium',
|
|
58
|
+
headless: true,
|
|
59
|
+
args: [
|
|
60
|
+
'--no-sandbox',
|
|
61
|
+
'--disable-background-timer-throttling',
|
|
62
|
+
'--disable-backgrounding-occluded-windows',
|
|
63
|
+
'--disable-renderer-backgrounding',
|
|
64
|
+
'--use-gl=desktop',
|
|
65
|
+
'--ignore-gpu-blocklist',
|
|
66
|
+
],
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
name: 'angle-gl',
|
|
70
|
+
label: 'ANGLE OpenGL ES',
|
|
71
|
+
channel: 'chromium',
|
|
72
|
+
headless: true,
|
|
73
|
+
args: [
|
|
74
|
+
'--no-sandbox',
|
|
75
|
+
'--disable-background-timer-throttling',
|
|
76
|
+
'--disable-backgrounding-occluded-windows',
|
|
77
|
+
'--disable-renderer-backgrounding',
|
|
78
|
+
'--use-angle=gl',
|
|
79
|
+
'--ignore-gpu-blocklist',
|
|
80
|
+
],
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
darwin: [
|
|
84
|
+
{
|
|
85
|
+
name: 'metal',
|
|
86
|
+
label: 'Metal (Recommended)',
|
|
87
|
+
channel: 'chromium',
|
|
88
|
+
headless: true,
|
|
89
|
+
args: ['--use-angle=metal'],
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
name: 'desktop',
|
|
93
|
+
label: 'Desktop OpenGL',
|
|
94
|
+
channel: 'chromium',
|
|
95
|
+
headless: true,
|
|
96
|
+
args: ['--use-gl=desktop'],
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
name: 'auto',
|
|
100
|
+
label: 'Auto',
|
|
101
|
+
channel: 'chromium',
|
|
102
|
+
headless: true,
|
|
103
|
+
args: [],
|
|
104
|
+
},
|
|
105
|
+
],
|
|
106
|
+
win32: [
|
|
107
|
+
{
|
|
108
|
+
name: 'd3d11',
|
|
109
|
+
label: 'Direct3D 11 (Recommended)',
|
|
110
|
+
channel: 'chromium',
|
|
111
|
+
headless: true,
|
|
112
|
+
args: ['--no-sandbox', '--use-angle=d3d11'],
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
name: 'd3d9',
|
|
116
|
+
label: 'Direct3D 9',
|
|
117
|
+
channel: 'chromium',
|
|
118
|
+
headless: true,
|
|
119
|
+
args: ['--no-sandbox', '--use-angle=d3d9'],
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
name: 'desktop',
|
|
123
|
+
label: 'Desktop OpenGL',
|
|
124
|
+
channel: 'chromium',
|
|
125
|
+
headless: true,
|
|
126
|
+
args: ['--no-sandbox', '--use-gl=desktop'],
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
name: 'auto',
|
|
130
|
+
label: 'Auto',
|
|
131
|
+
channel: 'chromium',
|
|
132
|
+
headless: true,
|
|
133
|
+
args: ['--no-sandbox'],
|
|
134
|
+
},
|
|
135
|
+
],
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Software fallback for all platforms (legacy headless, no channel)
|
|
139
|
+
const SOFTWARE_FALLBACK = {
|
|
140
|
+
name: 'software',
|
|
141
|
+
label: 'Software (SwiftShader)',
|
|
142
|
+
channel: undefined,
|
|
143
|
+
headless: true,
|
|
144
|
+
args: ['--no-sandbox'],
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
148
|
+
// GPU Detection Cache
|
|
149
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
150
|
+
|
|
151
|
+
const CACHE_FILE = join(tmpdir(), 'vueseq-gpu-config.json')
|
|
152
|
+
const CACHE_TTL = 24 * 60 * 60 * 1000 // 24 hours
|
|
153
|
+
|
|
154
|
+
let cachedConfig = null
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Load cached GPU configuration
|
|
158
|
+
*/
|
|
159
|
+
async function loadCachedConfig() {
|
|
160
|
+
try {
|
|
161
|
+
const data = await readFile(CACHE_FILE, 'utf-8')
|
|
162
|
+
const cache = JSON.parse(data)
|
|
163
|
+
|
|
164
|
+
// Check if cache is still valid
|
|
165
|
+
if (Date.now() - cache.timestamp < CACHE_TTL) {
|
|
166
|
+
return cache.config
|
|
167
|
+
}
|
|
168
|
+
} catch {
|
|
169
|
+
// Cache doesn't exist or is invalid
|
|
170
|
+
}
|
|
171
|
+
return null
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Save GPU configuration to cache
|
|
176
|
+
*/
|
|
177
|
+
async function saveCachedConfig(config) {
|
|
178
|
+
try {
|
|
179
|
+
const cache = {
|
|
180
|
+
timestamp: Date.now(),
|
|
181
|
+
config,
|
|
182
|
+
}
|
|
183
|
+
await writeFile(CACHE_FILE, JSON.stringify(cache, null, 2))
|
|
184
|
+
} catch {
|
|
185
|
+
// Ignore cache write errors
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
190
|
+
// GPU Detection Functions
|
|
191
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Test if a GPU configuration provides hardware acceleration
|
|
195
|
+
*/
|
|
196
|
+
async function testGPUBackend(backend, timeout = 8000) {
|
|
197
|
+
try {
|
|
198
|
+
const launchOptions = {
|
|
199
|
+
headless: backend.headless,
|
|
200
|
+
args: backend.args,
|
|
201
|
+
timeout,
|
|
202
|
+
}
|
|
203
|
+
// Add channel option for new headless mode (GPU support)
|
|
204
|
+
if (backend.channel) {
|
|
205
|
+
launchOptions.channel = backend.channel
|
|
206
|
+
}
|
|
207
|
+
const browser = await chromium.launch(launchOptions)
|
|
208
|
+
|
|
209
|
+
const context = await browser.newContext()
|
|
210
|
+
const page = await context.newPage()
|
|
211
|
+
|
|
212
|
+
const gpuInfo = await page.evaluate(() => {
|
|
213
|
+
const canvas = document.createElement('canvas')
|
|
214
|
+
const gl =
|
|
215
|
+
canvas.getContext('webgl') || canvas.getContext('experimental-webgl')
|
|
216
|
+
|
|
217
|
+
if (!gl) {
|
|
218
|
+
return { supported: false }
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const debugInfo = gl.getExtension('WEBGL_debug_renderer_info')
|
|
222
|
+
if (debugInfo) {
|
|
223
|
+
const renderer = gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL)
|
|
224
|
+
const vendor = gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL)
|
|
225
|
+
|
|
226
|
+
const rendererLower = (renderer || '').toLowerCase()
|
|
227
|
+
const isSoftware =
|
|
228
|
+
rendererLower.includes('swiftshader') ||
|
|
229
|
+
rendererLower.includes('llvmpipe') ||
|
|
230
|
+
rendererLower.includes('software') ||
|
|
231
|
+
rendererLower.includes('microsoft basic render')
|
|
232
|
+
|
|
233
|
+
return {
|
|
234
|
+
supported: true,
|
|
235
|
+
renderer,
|
|
236
|
+
vendor,
|
|
237
|
+
isHardwareAccelerated: !isSoftware,
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return { supported: true, isHardwareAccelerated: false }
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
await browser.close()
|
|
245
|
+
return gpuInfo
|
|
246
|
+
} catch (error) {
|
|
247
|
+
return { supported: false, error: error.message }
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Detect the best GPU configuration for the current system
|
|
253
|
+
* @param {Object} options
|
|
254
|
+
* @param {boolean} [options.forceDetect=false] - Skip cache and re-detect
|
|
255
|
+
* @param {string} [options.preferBackend] - Prefer a specific backend if available
|
|
256
|
+
* @returns {Promise<GPUConfig>}
|
|
257
|
+
*/
|
|
258
|
+
export async function detectBestGPUConfig(options = {}) {
|
|
259
|
+
const { forceDetect = false, preferBackend } = options
|
|
260
|
+
|
|
261
|
+
// Check cache first (unless forced)
|
|
262
|
+
if (!forceDetect && cachedConfig) {
|
|
263
|
+
return cachedConfig
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (!forceDetect) {
|
|
267
|
+
const cached = await loadCachedConfig()
|
|
268
|
+
if (cached) {
|
|
269
|
+
cachedConfig = cached
|
|
270
|
+
return cached
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const plat = platform()
|
|
275
|
+
const backends = GPU_BACKENDS[plat] || GPU_BACKENDS.linux
|
|
276
|
+
|
|
277
|
+
// If a specific backend is preferred, try it first
|
|
278
|
+
if (preferBackend) {
|
|
279
|
+
const preferred = backends.find((b) => b.name === preferBackend)
|
|
280
|
+
if (preferred) {
|
|
281
|
+
const result = await testGPUBackend(preferred)
|
|
282
|
+
if (result.isHardwareAccelerated) {
|
|
283
|
+
const config = {
|
|
284
|
+
backend: preferred.name,
|
|
285
|
+
label: preferred.label,
|
|
286
|
+
channel: preferred.channel,
|
|
287
|
+
headless: preferred.headless,
|
|
288
|
+
args: preferred.args,
|
|
289
|
+
isHardwareAccelerated: true,
|
|
290
|
+
renderer: result.renderer,
|
|
291
|
+
vendor: result.vendor,
|
|
292
|
+
}
|
|
293
|
+
cachedConfig = config
|
|
294
|
+
await saveCachedConfig(config)
|
|
295
|
+
return config
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Try each backend in priority order
|
|
301
|
+
for (const backend of backends) {
|
|
302
|
+
const result = await testGPUBackend(backend)
|
|
303
|
+
|
|
304
|
+
if (result.isHardwareAccelerated) {
|
|
305
|
+
const config = {
|
|
306
|
+
backend: backend.name,
|
|
307
|
+
label: backend.label,
|
|
308
|
+
channel: backend.channel,
|
|
309
|
+
headless: backend.headless,
|
|
310
|
+
args: backend.args,
|
|
311
|
+
isHardwareAccelerated: true,
|
|
312
|
+
renderer: result.renderer,
|
|
313
|
+
vendor: result.vendor,
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
cachedConfig = config
|
|
317
|
+
await saveCachedConfig(config)
|
|
318
|
+
return config
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Fall back to software rendering
|
|
323
|
+
const config = {
|
|
324
|
+
backend: SOFTWARE_FALLBACK.name,
|
|
325
|
+
label: SOFTWARE_FALLBACK.label,
|
|
326
|
+
channel: SOFTWARE_FALLBACK.channel,
|
|
327
|
+
headless: SOFTWARE_FALLBACK.headless,
|
|
328
|
+
args: SOFTWARE_FALLBACK.args,
|
|
329
|
+
isHardwareAccelerated: false,
|
|
330
|
+
renderer: 'SwiftShader',
|
|
331
|
+
vendor: 'Google',
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
cachedConfig = config
|
|
335
|
+
await saveCachedConfig(config)
|
|
336
|
+
return config
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Get Chromium flags for a specific backend or auto-detect
|
|
341
|
+
* @param {string} [backend='auto'] - Backend name or 'auto' for detection
|
|
342
|
+
* @returns {Promise<{headless: boolean, channel?: string, args: string[]}>}
|
|
343
|
+
*/
|
|
344
|
+
export async function getOptimalChromiumConfig(backend = 'auto') {
|
|
345
|
+
if (backend === 'auto') {
|
|
346
|
+
const config = await detectBestGPUConfig()
|
|
347
|
+
return {
|
|
348
|
+
headless: config.headless,
|
|
349
|
+
channel: config.channel,
|
|
350
|
+
args: config.args,
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Manual backend selection
|
|
355
|
+
const plat = platform()
|
|
356
|
+
const backends = GPU_BACKENDS[plat] || GPU_BACKENDS.linux
|
|
357
|
+
const selected = backends.find((b) => b.name === backend)
|
|
358
|
+
|
|
359
|
+
if (selected) {
|
|
360
|
+
return {
|
|
361
|
+
headless: selected.headless,
|
|
362
|
+
channel: selected.channel,
|
|
363
|
+
args: selected.args,
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Fallback
|
|
368
|
+
return {
|
|
369
|
+
headless: SOFTWARE_FALLBACK.headless,
|
|
370
|
+
channel: SOFTWARE_FALLBACK.channel,
|
|
371
|
+
args: SOFTWARE_FALLBACK.args,
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Get just the Chromium args (for compatibility with existing code)
|
|
377
|
+
* @returns {Promise<string[]>}
|
|
378
|
+
*/
|
|
379
|
+
export async function getOptimalChromiumFlags() {
|
|
380
|
+
const config = await getOptimalChromiumConfig('auto')
|
|
381
|
+
return config.args
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Get available GPU backends for the current platform
|
|
386
|
+
* @returns {Array<{name: string, label: string}>}
|
|
387
|
+
*/
|
|
388
|
+
export function getAvailableBackends() {
|
|
389
|
+
const plat = platform()
|
|
390
|
+
const backends = GPU_BACKENDS[plat] || GPU_BACKENDS.linux
|
|
391
|
+
return [
|
|
392
|
+
{ name: 'auto', label: 'Auto-detect' },
|
|
393
|
+
...backends.map((b) => ({ name: b.name, label: b.label })),
|
|
394
|
+
{ name: 'software', label: 'Software (SwiftShader)' },
|
|
395
|
+
]
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Clear the GPU configuration cache
|
|
400
|
+
*/
|
|
401
|
+
export async function clearGPUCache() {
|
|
402
|
+
cachedConfig = null
|
|
403
|
+
try {
|
|
404
|
+
const { unlink } = await import('fs/promises')
|
|
405
|
+
await unlink(CACHE_FILE)
|
|
406
|
+
} catch {
|
|
407
|
+
// Ignore if file doesn't exist
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Check GPU acceleration status (quick check)
|
|
413
|
+
* @returns {Promise<{accelerated: boolean, status: string, backend: string}>}
|
|
414
|
+
*/
|
|
415
|
+
export async function checkGPUAcceleration() {
|
|
416
|
+
const config = await detectBestGPUConfig()
|
|
417
|
+
return {
|
|
418
|
+
accelerated: config.isHardwareAccelerated,
|
|
419
|
+
status: config.renderer,
|
|
420
|
+
backend: config.backend,
|
|
421
|
+
label: config.label,
|
|
422
|
+
}
|
|
423
|
+
}
|