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.
@@ -0,0 +1,185 @@
1
+ /**
2
+ * In-Browser Frame Capture Module
3
+ *
4
+ * Provides utilities for capturing DOM frames directly in the browser
5
+ * without PNG encoding/decoding overhead. Uses html2canvas for DOM
6
+ * capture and creates VideoFrame objects directly from canvas (zero-copy GPU).
7
+ */
8
+
9
+ /**
10
+ * Get the html2canvas script content for injection
11
+ * @returns {Promise<string>}
12
+ */
13
+ export async function getHtml2CanvasScript() {
14
+ const { readFile } = await import('fs/promises')
15
+ const { join, dirname } = await import('path')
16
+ const { fileURLToPath } = await import('url')
17
+
18
+ // Find html2canvas in node_modules
19
+ const __dirname = dirname(fileURLToPath(import.meta.url))
20
+ const html2canvasPath = join(
21
+ __dirname,
22
+ '../../node_modules/html2canvas/dist/html2canvas.min.js',
23
+ )
24
+
25
+ return await readFile(html2canvasPath, 'utf-8')
26
+ }
27
+
28
+ /**
29
+ * Inject capture infrastructure into page
30
+ * @param {import('playwright').Page} page
31
+ * @param {Object} options
32
+ * @param {number} options.width
33
+ * @param {number} options.height
34
+ */
35
+ export async function injectCaptureInfrastructure(page, { width, height }) {
36
+ // Inject html2canvas library
37
+ const html2canvasScript = await getHtml2CanvasScript()
38
+ await page.addScriptTag({ content: html2canvasScript })
39
+
40
+ // Set up capture canvas and utilities
41
+ await page.evaluate(
42
+ ({ width, height }) => {
43
+ // Create reusable capture canvas (avoid creating new ones each frame)
44
+ const captureCanvas = document.createElement('canvas')
45
+ captureCanvas.width = width
46
+ captureCanvas.height = height
47
+ window.__VUESEQ_CAPTURE_CANVAS__ = captureCanvas
48
+
49
+ // Pre-configure html2canvas options for speed
50
+ window.__VUESEQ_CAPTURE_OPTIONS__ = {
51
+ canvas: captureCanvas,
52
+ width,
53
+ height,
54
+ scale: 1,
55
+ useCORS: true,
56
+ allowTaint: true,
57
+ backgroundColor: null, // Transparent - use page background
58
+ logging: false,
59
+ // Performance optimizations
60
+ imageTimeout: 0,
61
+ removeContainer: true,
62
+ foreignObjectRendering: false, // More compatible
63
+ }
64
+
65
+ // Frame batch storage for parallel processing
66
+ window.__VUESEQ_FRAME_BATCH__ = []
67
+ },
68
+ { width, height },
69
+ )
70
+ }
71
+
72
+ /**
73
+ * Capture a single frame from DOM to VideoFrame
74
+ * Returns the VideoFrame for encoding
75
+ *
76
+ * @param {import('playwright').Page} page
77
+ * @param {number} timestamp - Frame timestamp in seconds
78
+ * @param {number} fps - Frames per second
79
+ * @returns {Promise<void>} - Frame is added to internal batch
80
+ */
81
+ export async function captureFrameToBatch(page, timestamp, fps) {
82
+ await page.evaluate(
83
+ async ({ timestamp, fps }) => {
84
+ const captureCanvas = window.__VUESEQ_CAPTURE_CANVAS__
85
+ const options = window.__VUESEQ_CAPTURE_OPTIONS__
86
+
87
+ // Capture DOM to canvas using html2canvas
88
+ await html2canvas(document.body, options)
89
+
90
+ // Create VideoFrame directly from canvas (zero-copy on GPU!)
91
+ const frameDuration = 1 / fps
92
+ const videoFrame = new VideoFrame(captureCanvas, {
93
+ timestamp: Math.round(timestamp * 1_000_000), // microseconds
94
+ duration: Math.round(frameDuration * 1_000_000),
95
+ })
96
+
97
+ // Store in batch for later encoding
98
+ window.__VUESEQ_FRAME_BATCH__.push(videoFrame)
99
+ },
100
+ { timestamp, fps },
101
+ )
102
+ }
103
+
104
+ /**
105
+ * Encode all frames in the current batch and clear batch
106
+ * @param {import('playwright').Page} page
107
+ */
108
+ export async function encodeFrameBatch(page) {
109
+ await page.evaluate(async () => {
110
+ const batch = window.__VUESEQ_FRAME_BATCH__
111
+ const videoSource = window.__VUESEQ_VIDEO_SOURCE__
112
+
113
+ // Encode each frame in the batch
114
+ for (const videoFrame of batch) {
115
+ // Add frame to video source at its timestamp
116
+ const timestampSec = videoFrame.timestamp / 1_000_000
117
+ const durationSec = videoFrame.duration / 1_000_000
118
+ await videoSource.add(timestampSec, durationSec)
119
+
120
+ // Draw the captured frame to the encoding canvas
121
+ const canvas = window.__VUESEQ_CANVAS__
122
+ const ctx = canvas.getContext('2d')
123
+ ctx.drawImage(videoFrame, 0, 0)
124
+
125
+ // Close the VideoFrame to release GPU resources
126
+ videoFrame.close()
127
+ }
128
+
129
+ // Clear batch for next round
130
+ window.__VUESEQ_FRAME_BATCH__ = []
131
+ })
132
+ }
133
+
134
+ /**
135
+ * Alternative: Direct canvas encoding without intermediate batch
136
+ * More efficient for single-frame-at-a-time scenarios
137
+ *
138
+ * @param {import('playwright').Page} page
139
+ * @param {number} timestamp - Frame timestamp in seconds
140
+ * @param {number} fps - Frames per second
141
+ */
142
+ export async function captureAndEncodeDirect(page, timestamp, fps) {
143
+ await page.evaluate(
144
+ async ({ timestamp, fps }) => {
145
+ const captureCanvas = window.__VUESEQ_CAPTURE_CANVAS__
146
+ const options = window.__VUESEQ_CAPTURE_OPTIONS__
147
+ const encodingCanvas = window.__VUESEQ_CANVAS__
148
+ const ctx = encodingCanvas.getContext('2d')
149
+
150
+ // Capture DOM to canvas
151
+ await html2canvas(document.body, options)
152
+
153
+ // Draw captured content to encoding canvas
154
+ ctx.drawImage(captureCanvas, 0, 0)
155
+
156
+ // Add frame to video source
157
+ const frameDuration = 1 / fps
158
+ await window.__VUESEQ_VIDEO_SOURCE__.add(timestamp, frameDuration)
159
+ },
160
+ { timestamp, fps },
161
+ )
162
+ }
163
+
164
+ /**
165
+ * Cleanup capture infrastructure
166
+ * @param {import('playwright').Page} page
167
+ */
168
+ export async function cleanupCapture(page) {
169
+ await page.evaluate(() => {
170
+ // Close any remaining frames in batch
171
+ if (window.__VUESEQ_FRAME_BATCH__) {
172
+ for (const frame of window.__VUESEQ_FRAME_BATCH__) {
173
+ try {
174
+ frame.close()
175
+ } catch {
176
+ // Already closed
177
+ }
178
+ }
179
+ }
180
+
181
+ delete window.__VUESEQ_CAPTURE_CANVAS__
182
+ delete window.__VUESEQ_CAPTURE_OPTIONS__
183
+ delete window.__VUESEQ_FRAME_BATCH__
184
+ })
185
+ }
@@ -0,0 +1,317 @@
1
+ /**
2
+ * Optimized Video Encoder using In-Browser Capture
3
+ *
4
+ * This module eliminates the PNG encode/decode overhead by capturing
5
+ * frames directly in the browser using html2canvas and creating
6
+ * VideoFrames from canvas (zero-copy GPU path).
7
+ *
8
+ * Key optimizations:
9
+ * 1. No PNG encoding/decoding - direct canvas-to-VideoFrame
10
+ * 2. Batch processing for better GPU utilization
11
+ * 3. All frame capture happens in-browser (no Node<->browser transfers)
12
+ */
13
+
14
+ import { chromium } from 'playwright'
15
+ import { createVideoServer } from '../bundler/vite.js'
16
+ import { readFile, writeFile } from 'fs/promises'
17
+ import { getTimelineDuration } from './render.js'
18
+ import { join } from 'path'
19
+ import { getOptimalChromiumConfig } from './gpu.js'
20
+ import {
21
+ injectCaptureInfrastructure,
22
+ captureAndEncodeDirect,
23
+ cleanupCapture,
24
+ } from './capture.js'
25
+
26
+ // Default batch size - larger batches = better GPU saturation
27
+ const DEFAULT_BATCH_SIZE = 30
28
+
29
+ /**
30
+ * Inject Mediabunny library into the page
31
+ */
32
+ async function injectMediabunny(page) {
33
+ const libPath = join(
34
+ process.cwd(),
35
+ 'node_modules',
36
+ 'mediabunny',
37
+ 'dist',
38
+ 'bundles',
39
+ 'mediabunny.cjs',
40
+ )
41
+ const libCode = await readFile(libPath, 'utf-8')
42
+ await page.addScriptTag({ content: libCode })
43
+ }
44
+
45
+ /**
46
+ * Initialize Mediabunny encoder with optimized settings
47
+ */
48
+ async function initializeEncoder(page, { width, height, fps }) {
49
+ await page.evaluate(
50
+ async ({ width, height, fps }) => {
51
+ const {
52
+ Output,
53
+ Mp4OutputFormat,
54
+ BufferTarget,
55
+ QUALITY_HIGH,
56
+ CanvasSource,
57
+ } = window.Mediabunny
58
+
59
+ // Create output with MP4 format
60
+ window.__VUESEQ_OUTPUT__ = new Output({
61
+ format: new Mp4OutputFormat(),
62
+ target: new BufferTarget(),
63
+ })
64
+
65
+ // Create encoding canvas
66
+ const canvas = document.createElement('canvas')
67
+ canvas.width = width
68
+ canvas.height = height
69
+ window.__VUESEQ_CANVAS__ = canvas
70
+
71
+ // Create CanvasSource with high quality encoding
72
+ // Note: WebCodecs will automatically use hardware acceleration when available
73
+ window.__VUESEQ_VIDEO_SOURCE__ = new CanvasSource(canvas, {
74
+ codec: 'avc',
75
+ bitrate: QUALITY_HIGH,
76
+ })
77
+
78
+ window.__VUESEQ_OUTPUT__.addVideoTrack(window.__VUESEQ_VIDEO_SOURCE__)
79
+ window.__VUESEQ_FPS__ = fps
80
+
81
+ await window.__VUESEQ_OUTPUT__.start()
82
+ },
83
+ { width, height, fps },
84
+ )
85
+ }
86
+
87
+ /**
88
+ * Capture and encode a frame entirely in-browser
89
+ * This eliminates PNG encode/decode overhead
90
+ */
91
+ async function captureAndEncodeFrame(page, timestamp, fps) {
92
+ await page.evaluate(
93
+ async ({ timestamp, fps }) => {
94
+ const captureCanvas = window.__VUESEQ_CAPTURE_CANVAS__
95
+ const encodingCanvas = window.__VUESEQ_CANVAS__
96
+ const ctx = encodingCanvas.getContext('2d')
97
+ const options = window.__VUESEQ_CAPTURE_OPTIONS__
98
+
99
+ // Capture DOM to canvas using html2canvas
100
+ await html2canvas(document.body, options)
101
+
102
+ // Draw captured content to encoding canvas
103
+ ctx.drawImage(captureCanvas, 0, 0)
104
+
105
+ // Add frame to video source
106
+ const frameDuration = 1 / fps
107
+ await window.__VUESEQ_VIDEO_SOURCE__.add(timestamp, frameDuration)
108
+ },
109
+ { timestamp, fps },
110
+ )
111
+ }
112
+
113
+ /**
114
+ * Finalize encoding and retrieve the MP4 buffer
115
+ */
116
+ async function finalizeEncoding(page) {
117
+ console.log(' Transferring video data (Base64)...')
118
+ return await page.evaluate(async () => {
119
+ window.__VUESEQ_VIDEO_SOURCE__.close()
120
+ await window.__VUESEQ_OUTPUT__.finalize()
121
+ const buffer = window.__VUESEQ_OUTPUT__.target.buffer
122
+
123
+ // Cleanup
124
+ delete window.__VUESEQ_OUTPUT__
125
+ delete window.__VUESEQ_CANVAS__
126
+ delete window.__VUESEQ_VIDEO_SOURCE__
127
+ delete window.__VUESEQ_FPS__
128
+
129
+ // Fast conversion via Blob
130
+ const blob = new Blob([buffer], { type: 'video/mp4' })
131
+ return new Promise((resolve) => {
132
+ const reader = new FileReader()
133
+ reader.onloadend = () => resolve(reader.result)
134
+ reader.readAsDataURL(blob)
135
+ })
136
+ })
137
+ }
138
+
139
+ /**
140
+ * Optimized render to MP4 using in-browser capture
141
+ *
142
+ * This version eliminates PNG encoding/decoding overhead by:
143
+ * 1. Capturing frames directly in the browser with html2canvas
144
+ * 2. Creating VideoFrames from canvas (GPU zero-copy)
145
+ * 3. Processing frames in batches for better GPU utilization
146
+ *
147
+ * @param {Object} options
148
+ * @param {string} options.input - Path to Video.vue component
149
+ * @param {string} [options.output='./output.mp4'] - Output file path
150
+ * @param {number} [options.fps=30] - Frames per second
151
+ * @param {number} options.duration - Duration in seconds
152
+ * @param {number} [options.width=1920] - Video width
153
+ * @param {number} [options.height=1080] - Video height
154
+ * @param {number} [options.batchSize=30] - Frames per batch
155
+ * @param {function} [options.onProgress] - Progress callback
156
+ * @returns {Promise<string>} - Path to output video
157
+ */
158
+ export async function renderToMp4Optimized(options) {
159
+ const {
160
+ input,
161
+ output = './output.mp4',
162
+ fps = 30,
163
+ duration: providedDuration,
164
+ width = 1920,
165
+ height = 1080,
166
+ batchSize = DEFAULT_BATCH_SIZE,
167
+ onProgress,
168
+ } = options
169
+
170
+ // Auto-detect duration if not provided
171
+ let duration = providedDuration
172
+ if (!duration || duration <= 0) {
173
+ duration = await getTimelineDuration({ input, width, height })
174
+ if (!duration || duration <= 0) {
175
+ throw new Error(
176
+ 'Could not auto-detect duration. Specify duration manually.',
177
+ )
178
+ }
179
+ }
180
+
181
+ const totalFrames = Math.ceil(duration * fps)
182
+
183
+ // Start Vite server
184
+ const { url, cleanup: cleanupServer } = await createVideoServer({
185
+ input,
186
+ width,
187
+ height,
188
+ })
189
+
190
+ // Launch browser with optimal GPU config
191
+ const gpuConfig = await getOptimalChromiumConfig()
192
+ const launchOptions = {
193
+ headless: gpuConfig.headless,
194
+ args: gpuConfig.args,
195
+ }
196
+ if (gpuConfig.channel) {
197
+ launchOptions.channel = gpuConfig.channel
198
+ }
199
+ const browser = await chromium.launch(launchOptions)
200
+
201
+ const context = await browser.newContext({
202
+ viewport: { width, height },
203
+ deviceScaleFactor: 1,
204
+ })
205
+
206
+ const page = await context.newPage()
207
+
208
+ try {
209
+ // Load the page
210
+ await page.goto(url, { waitUntil: 'networkidle' })
211
+
212
+ // Wait for VueSeq bridge
213
+ await page.waitForFunction(() => window.__VUESEQ_READY__ === true, {
214
+ timeout: 30000,
215
+ })
216
+
217
+ await page.waitForTimeout(100)
218
+
219
+ // Inject libraries
220
+ await injectMediabunny(page)
221
+ await injectCaptureInfrastructure(page, { width, height })
222
+
223
+ // Initialize encoder
224
+ await initializeEncoder(page, { width, height, fps })
225
+
226
+ // Process frames in batches for better GPU saturation
227
+ const totalBatches = Math.ceil(totalFrames / batchSize)
228
+
229
+ for (let batchIndex = 0; batchIndex < totalBatches; batchIndex++) {
230
+ const batchStart = batchIndex * batchSize
231
+ const batchEnd = Math.min(batchStart + batchSize, totalFrames)
232
+
233
+ // Capture and encode each frame in this batch
234
+ for (let frame = batchStart; frame < batchEnd; frame++) {
235
+ const timeInSeconds = frame / fps
236
+
237
+ // Seek GSAP to exact time
238
+ await page.evaluate(async (t) => {
239
+ window.__VUESEQ_SEEK__(t)
240
+ await new Promise((resolve) => requestAnimationFrame(resolve))
241
+ }, timeInSeconds)
242
+
243
+ // Capture and encode directly in browser (no PNG!)
244
+ await captureAndEncodeFrame(page, timeInSeconds, fps)
245
+
246
+ // Progress callback
247
+ if (onProgress) {
248
+ onProgress({
249
+ frame,
250
+ total: totalFrames,
251
+ timeInSeconds,
252
+ percent: Math.round(((frame + 1) / totalFrames) * 100),
253
+ batch: batchIndex + 1,
254
+ totalBatches,
255
+ })
256
+ }
257
+ }
258
+ }
259
+
260
+ // Cleanup capture infrastructure
261
+ await cleanupCapture(page)
262
+
263
+ // Finalize and get MP4 data (Base64)
264
+ const base64Data = await finalizeEncoding(page)
265
+
266
+ // Write the MP4 file
267
+ const buffer = Buffer.from(base64Data.split(',')[1], 'base64')
268
+ await writeFile(output, buffer)
269
+
270
+ return output
271
+ } finally {
272
+ await browser.close()
273
+ await cleanupServer()
274
+ }
275
+ }
276
+
277
+ /**
278
+ * Benchmark comparison between original and optimized methods
279
+ */
280
+ export async function benchmarkRenderMethods(options) {
281
+ const { renderToMp4 } = await import('./encode.js')
282
+
283
+ console.log('\nšŸ“Š Benchmarking render methods...\n')
284
+
285
+ // Benchmark original method
286
+ console.log('Testing ORIGINAL method (PNG-based)...')
287
+ const originalStart = Date.now()
288
+ await renderToMp4({
289
+ ...options,
290
+ output: '/tmp/benchmark-original.mp4',
291
+ })
292
+ const originalTime = Date.now() - originalStart
293
+
294
+ // Benchmark optimized method
295
+ console.log('Testing OPTIMIZED method (in-browser capture)...')
296
+ const optimizedStart = Date.now()
297
+ await renderToMp4Optimized({
298
+ ...options,
299
+ output: '/tmp/benchmark-optimized.mp4',
300
+ })
301
+ const optimizedTime = Date.now() - optimizedStart
302
+
303
+ const improvement = ((originalTime - optimizedTime) / originalTime) * 100
304
+
305
+ console.log('\nšŸ“ˆ Results:')
306
+ console.log(` Original: ${(originalTime / 1000).toFixed(2)}s`)
307
+ console.log(` Optimized: ${(optimizedTime / 1000).toFixed(2)}s`)
308
+ console.log(
309
+ ` Improvement: ${improvement.toFixed(1)}% ${improvement > 0 ? 'faster' : 'slower'}`,
310
+ )
311
+
312
+ return {
313
+ originalTime,
314
+ optimizedTime,
315
+ improvement,
316
+ }
317
+ }