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,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
|
+
}
|