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.
@@ -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
+ }