@t3lnet/sceneforge 1.0.23 → 1.0.25
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/cli/commands/pipeline.js
CHANGED
|
@@ -5,6 +5,7 @@ import { getFlagValue, hasFlag } from "../utils/args.js";
|
|
|
5
5
|
import { ensureDir, getOutputPaths, resolveRoot, toAbsolute } from "../utils/paths.js";
|
|
6
6
|
import { getQualityHelpText } from "../utils/quality.js";
|
|
7
7
|
import { getViewportHelpText, getOutputDimensionsHelpText } from "../utils/dimensions.js";
|
|
8
|
+
import { getCDPRecordingHelpText } from "../utils/cdp-recorder.js";
|
|
8
9
|
import { runRecordDemoCommand } from "./record-demo.js";
|
|
9
10
|
import { runSplitVideoCommand } from "./split-video.js";
|
|
10
11
|
import { runGenerateVoiceoverCommand } from "./generate-voiceover.js";
|
|
@@ -45,6 +46,7 @@ Media options (for final video):
|
|
|
45
46
|
${getQualityHelpText()}
|
|
46
47
|
${getViewportHelpText()}
|
|
47
48
|
${getOutputDimensionsHelpText()}
|
|
49
|
+
${getCDPRecordingHelpText()}
|
|
48
50
|
|
|
49
51
|
--help, -h Show this help message
|
|
50
52
|
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
parseViewportArgs,
|
|
20
20
|
getViewportHelpText,
|
|
21
21
|
} from "../utils/dimensions.js";
|
|
22
|
+
import { createCDPRecorder, getCDPRecordingHelpText } from "../utils/cdp-recorder.js";
|
|
22
23
|
|
|
23
24
|
function printHelp() {
|
|
24
25
|
console.log(`
|
|
@@ -44,6 +45,7 @@ Options:
|
|
|
44
45
|
--no-video Skip video recording
|
|
45
46
|
--help, -h Show this help message
|
|
46
47
|
${getViewportHelpText()}
|
|
48
|
+
${getCDPRecordingHelpText()}
|
|
47
49
|
|
|
48
50
|
Examples:
|
|
49
51
|
sceneforge record --definition demo-definitions/create-quote.yaml --base-url http://localhost:5173
|
|
@@ -177,6 +179,9 @@ export async function runRecordDemoCommand(argv) {
|
|
|
177
179
|
const headed = hasFlag(args, "--headed");
|
|
178
180
|
const slowMo = getFlagValue(args, "--slowmo");
|
|
179
181
|
const noVideo = hasFlag(args, "--no-video");
|
|
182
|
+
const hqVideo = hasFlag(args, "--hq-video");
|
|
183
|
+
const hqFormat = getFlagValue(args, "--hq-format") ?? "jpeg";
|
|
184
|
+
const hqQuality = parseInt(getFlagValue(args, "--hq-quality") ?? "100", 10);
|
|
180
185
|
|
|
181
186
|
if (!baseUrl) {
|
|
182
187
|
console.error("[error] --base-url is required");
|
|
@@ -216,7 +221,8 @@ export async function runRecordDemoCommand(argv) {
|
|
|
216
221
|
console.log(`[record] Viewport: ${viewport.width}x${viewport.height}`);
|
|
217
222
|
|
|
218
223
|
const recordDir = path.join(outputPaths.videosDir, ".recordings", definition.name);
|
|
219
|
-
|
|
224
|
+
const framesDir = path.join(outputPaths.videosDir, ".frames", definition.name);
|
|
225
|
+
if (!noVideo && !hqVideo) {
|
|
220
226
|
await ensureDir(recordDir);
|
|
221
227
|
}
|
|
222
228
|
|
|
@@ -225,9 +231,12 @@ export async function runRecordDemoCommand(argv) {
|
|
|
225
231
|
slowMo: slowMo ? Number(slowMo) : undefined,
|
|
226
232
|
});
|
|
227
233
|
|
|
234
|
+
// When using HQ video, don't use Playwright's built-in recording
|
|
235
|
+
const usePlaywrightRecording = !noVideo && !hqVideo;
|
|
236
|
+
|
|
228
237
|
const context = await browser.newContext({
|
|
229
238
|
viewport,
|
|
230
|
-
recordVideo:
|
|
239
|
+
recordVideo: usePlaywrightRecording ? { dir: recordDir, size: viewport } : undefined,
|
|
231
240
|
storageState: storageState ? toAbsolute(rootDir, storageState) : undefined,
|
|
232
241
|
locale: locale || undefined,
|
|
233
242
|
extraHTTPHeaders: locale
|
|
@@ -238,41 +247,88 @@ export async function runRecordDemoCommand(argv) {
|
|
|
238
247
|
});
|
|
239
248
|
|
|
240
249
|
const page = await context.newPage();
|
|
241
|
-
const video = page.video();
|
|
250
|
+
const video = usePlaywrightRecording ? page.video() : null;
|
|
242
251
|
const videoRecordingStartTime = Date.now();
|
|
243
|
-
const startUrl = resolveStartUrl(startPath, baseUrl);
|
|
244
252
|
|
|
245
|
-
|
|
246
|
-
|
|
253
|
+
// Set up CDP recorder for high-quality video
|
|
254
|
+
let cdpRecorder = null;
|
|
255
|
+
if (hqVideo && !noVideo) {
|
|
256
|
+
cdpRecorder = createCDPRecorder(page, {
|
|
257
|
+
framesDir,
|
|
258
|
+
format: hqFormat,
|
|
259
|
+
quality: hqQuality,
|
|
260
|
+
maxWidth: viewport.width,
|
|
261
|
+
maxHeight: viewport.height,
|
|
262
|
+
});
|
|
263
|
+
await cdpRecorder.start();
|
|
247
264
|
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
{
|
|
258
|
-
generateScripts: true,
|
|
259
|
-
scriptOutputDir: path.join(outputPaths.outputDir, "scripts"),
|
|
265
|
+
|
|
266
|
+
const startUrl = resolveStartUrl(startPath, baseUrl);
|
|
267
|
+
|
|
268
|
+
let result;
|
|
269
|
+
let demoError = null;
|
|
270
|
+
|
|
271
|
+
try {
|
|
272
|
+
if (startUrl) {
|
|
273
|
+
await page.goto(startUrl, { waitUntil: "networkidle" });
|
|
260
274
|
}
|
|
261
|
-
|
|
275
|
+
result = await runDemo(
|
|
276
|
+
definition,
|
|
277
|
+
{
|
|
278
|
+
page,
|
|
279
|
+
baseURL: baseUrl,
|
|
280
|
+
outputDir: outputPaths.outputDir,
|
|
281
|
+
assetBaseDir: assetRoot ? toAbsolute(rootDir, assetRoot) : undefined,
|
|
282
|
+
videoRecordingStartTime,
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
generateScripts: true,
|
|
286
|
+
scriptOutputDir: path.join(outputPaths.outputDir, "scripts"),
|
|
287
|
+
}
|
|
288
|
+
);
|
|
289
|
+
} catch (err) {
|
|
290
|
+
demoError = err;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Stop CDP recorder before closing context
|
|
294
|
+
if (cdpRecorder) {
|
|
295
|
+
await cdpRecorder.stop();
|
|
296
|
+
}
|
|
262
297
|
|
|
263
298
|
await context.close();
|
|
264
299
|
await browser.close();
|
|
265
300
|
|
|
266
|
-
|
|
301
|
+
// Handle video based on recording method
|
|
302
|
+
if (cdpRecorder) {
|
|
303
|
+
const finalVideoPath = path.join(outputPaths.videosDir, `${definition.name}.webm`);
|
|
304
|
+
try {
|
|
305
|
+
if (!demoError && result?.success) {
|
|
306
|
+
await cdpRecorder.assembleVideo(finalVideoPath);
|
|
307
|
+
console.log(`[record] High-quality video saved: ${finalVideoPath}`);
|
|
308
|
+
if (result.scriptPath) {
|
|
309
|
+
await alignScriptToVideo(result.scriptPath, finalVideoPath, videoRecordingStartTime);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
} finally {
|
|
313
|
+
// Always cleanup frames
|
|
314
|
+
await cdpRecorder.cleanup();
|
|
315
|
+
console.log(`[record] Cleaned up temporary frames from: ${framesDir}`);
|
|
316
|
+
}
|
|
317
|
+
} else if (video) {
|
|
267
318
|
const recordedPath = await video.path();
|
|
268
319
|
const finalVideoPath = path.join(outputPaths.videosDir, `${definition.name}.webm`);
|
|
269
320
|
await moveVideo(recordedPath, finalVideoPath);
|
|
270
321
|
console.log(`[record] Video saved: ${finalVideoPath}`);
|
|
271
|
-
if (result
|
|
322
|
+
if (result?.scriptPath) {
|
|
272
323
|
await alignScriptToVideo(result.scriptPath, finalVideoPath, videoRecordingStartTime);
|
|
273
324
|
}
|
|
274
325
|
}
|
|
275
326
|
|
|
327
|
+
// Re-throw demo error after cleanup
|
|
328
|
+
if (demoError) {
|
|
329
|
+
throw demoError;
|
|
330
|
+
}
|
|
331
|
+
|
|
276
332
|
if (result.success) {
|
|
277
333
|
console.log(`[record] ✓ Completed ${definition.name}`);
|
|
278
334
|
if (result.scriptPath) {
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CDP-based high-quality screen recorder
|
|
3
|
+
*
|
|
4
|
+
* Uses Chrome DevTools Protocol screencast API to capture high-quality frames,
|
|
5
|
+
* then assembles them into a video using FFmpeg. This bypasses Playwright's
|
|
6
|
+
* hardcoded low-quality video settings.
|
|
7
|
+
*
|
|
8
|
+
* Benefits over Playwright's built-in recording:
|
|
9
|
+
* - Configurable quality (PNG lossless or high-quality JPEG)
|
|
10
|
+
* - No 1Mbps bitrate cap
|
|
11
|
+
* - Better compression settings via FFmpeg
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import * as fs from "fs/promises";
|
|
15
|
+
import * as path from "path";
|
|
16
|
+
import { runFFmpeg } from "./media.js";
|
|
17
|
+
import { getIntermediateEncodingArgs } from "./quality.js";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @typedef {Object} CDPRecorderOptions
|
|
21
|
+
* @property {string} framesDir - Directory to store captured frames
|
|
22
|
+
* @property {number} [quality=100] - JPEG quality (1-100), 100 = best quality
|
|
23
|
+
* @property {string} [format='jpeg'] - Frame format: 'jpeg' or 'png'
|
|
24
|
+
* @property {number} [maxWidth] - Max frame width (undefined = viewport width)
|
|
25
|
+
* @property {number} [maxHeight] - Max frame height (undefined = viewport height)
|
|
26
|
+
* @property {number} [everyNthFrame=1] - Capture every Nth frame (1 = all frames)
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @typedef {Object} CDPRecorder
|
|
31
|
+
* @property {Function} start - Start recording
|
|
32
|
+
* @property {Function} stop - Stop recording and return frame info
|
|
33
|
+
* @property {Function} assembleVideo - Assemble frames into video
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Create a CDP-based screen recorder for a Playwright page
|
|
38
|
+
*
|
|
39
|
+
* @param {import('@playwright/test').Page} page - Playwright page
|
|
40
|
+
* @param {CDPRecorderOptions} options - Recorder options
|
|
41
|
+
* @returns {CDPRecorder}
|
|
42
|
+
*/
|
|
43
|
+
export function createCDPRecorder(page, options) {
|
|
44
|
+
const {
|
|
45
|
+
framesDir,
|
|
46
|
+
quality = 100,
|
|
47
|
+
format = "jpeg",
|
|
48
|
+
maxWidth,
|
|
49
|
+
maxHeight,
|
|
50
|
+
everyNthFrame = 1,
|
|
51
|
+
} = options;
|
|
52
|
+
|
|
53
|
+
let cdpSession = null;
|
|
54
|
+
let frameCount = 0;
|
|
55
|
+
let frameTimestamps = [];
|
|
56
|
+
let isRecording = false;
|
|
57
|
+
let startTime = null;
|
|
58
|
+
let writeQueue = Promise.resolve();
|
|
59
|
+
let nextFrameNumber = 0;
|
|
60
|
+
|
|
61
|
+
const frameHandler = (params) => {
|
|
62
|
+
if (!isRecording) return;
|
|
63
|
+
|
|
64
|
+
const { data, metadata, sessionId } = params;
|
|
65
|
+
frameCount += 1;
|
|
66
|
+
|
|
67
|
+
// Acknowledge the frame immediately to continue receiving
|
|
68
|
+
cdpSession.send("Page.screencastFrameAck", { sessionId }).catch(() => {
|
|
69
|
+
// Session may be closed
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// Skip frames based on everyNthFrame setting
|
|
73
|
+
if (everyNthFrame > 1 && frameCount % everyNthFrame !== 0) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const timestamp = metadata.timestamp;
|
|
78
|
+
const frameNumber = nextFrameNumber;
|
|
79
|
+
nextFrameNumber += 1;
|
|
80
|
+
const paddedNumber = String(frameNumber).padStart(8, "0");
|
|
81
|
+
const extension = format === "png" ? "png" : "jpg";
|
|
82
|
+
const framePath = path.join(framesDir, `frame_${paddedNumber}.${extension}`);
|
|
83
|
+
|
|
84
|
+
// Decode base64 frame data
|
|
85
|
+
const buffer = Buffer.from(data, "base64");
|
|
86
|
+
|
|
87
|
+
// Queue frame writes to ensure sequential completion
|
|
88
|
+
writeQueue = writeQueue.then(async () => {
|
|
89
|
+
try {
|
|
90
|
+
await fs.writeFile(framePath, buffer);
|
|
91
|
+
frameTimestamps.push({
|
|
92
|
+
frameNumber,
|
|
93
|
+
timestamp,
|
|
94
|
+
path: framePath,
|
|
95
|
+
});
|
|
96
|
+
} catch (err) {
|
|
97
|
+
console.warn(`[cdp-recorder] Failed to write frame ${frameNumber}:`, err.message);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
/**
|
|
104
|
+
* Start recording frames via CDP screencast
|
|
105
|
+
*/
|
|
106
|
+
async start() {
|
|
107
|
+
await fs.mkdir(framesDir, { recursive: true });
|
|
108
|
+
|
|
109
|
+
// Create CDP session for the page
|
|
110
|
+
cdpSession = await page.context().newCDPSession(page);
|
|
111
|
+
|
|
112
|
+
// Set up frame handler
|
|
113
|
+
cdpSession.on("Page.screencastFrame", frameHandler);
|
|
114
|
+
|
|
115
|
+
// Start screencast with high quality settings
|
|
116
|
+
// format: 'jpeg' or 'png'
|
|
117
|
+
// quality: 0-100 for jpeg (100 = best), ignored for png
|
|
118
|
+
// maxWidth/maxHeight: optional frame dimensions
|
|
119
|
+
// everyNthFrame: capture frequency (1 = every frame)
|
|
120
|
+
await cdpSession.send("Page.startScreencast", {
|
|
121
|
+
format,
|
|
122
|
+
quality,
|
|
123
|
+
maxWidth,
|
|
124
|
+
maxHeight,
|
|
125
|
+
everyNthFrame,
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
isRecording = true;
|
|
129
|
+
startTime = Date.now();
|
|
130
|
+
frameCount = 0;
|
|
131
|
+
frameTimestamps = [];
|
|
132
|
+
|
|
133
|
+
console.log("[cdp-recorder] Started high-quality screen capture");
|
|
134
|
+
},
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Stop recording and return frame information
|
|
138
|
+
* @returns {Promise<{frameCount: number, duration: number, framesDir: string}>}
|
|
139
|
+
*/
|
|
140
|
+
async stop() {
|
|
141
|
+
if (!isRecording || !cdpSession) {
|
|
142
|
+
return { frameCount: 0, duration: 0, framesDir };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
isRecording = false;
|
|
146
|
+
const duration = Date.now() - startTime;
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
await cdpSession.send("Page.stopScreencast");
|
|
150
|
+
cdpSession.off("Page.screencastFrame", frameHandler);
|
|
151
|
+
await cdpSession.detach();
|
|
152
|
+
} catch {
|
|
153
|
+
// Session may already be closed
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Wait for all pending frame writes to complete
|
|
157
|
+
await writeQueue;
|
|
158
|
+
|
|
159
|
+
console.log(
|
|
160
|
+
`[cdp-recorder] Captured ${frameTimestamps.length} frames in ${(duration / 1000).toFixed(2)}s`
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
frameCount: frameTimestamps.length,
|
|
165
|
+
duration,
|
|
166
|
+
framesDir,
|
|
167
|
+
frameTimestamps,
|
|
168
|
+
};
|
|
169
|
+
},
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Assemble captured frames into a video using FFmpeg
|
|
173
|
+
*
|
|
174
|
+
* @param {string} outputPath - Output video path
|
|
175
|
+
* @param {Object} [assembleOptions] - Assembly options
|
|
176
|
+
* @param {number} [assembleOptions.fps] - Output framerate (auto-detected if not specified)
|
|
177
|
+
* @returns {Promise<string>} - Path to assembled video
|
|
178
|
+
*/
|
|
179
|
+
async assembleVideo(outputPath, assembleOptions = {}) {
|
|
180
|
+
const { fps: fpsOverride } = assembleOptions;
|
|
181
|
+
|
|
182
|
+
if (frameTimestamps.length === 0) {
|
|
183
|
+
throw new Error("No frames captured - cannot assemble video");
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Calculate actual FPS from captured frame timestamps
|
|
187
|
+
let fps = fpsOverride;
|
|
188
|
+
if (!fps && frameTimestamps.length > 1) {
|
|
189
|
+
const firstTimestamp = frameTimestamps[0].timestamp;
|
|
190
|
+
const lastTimestamp = frameTimestamps[frameTimestamps.length - 1].timestamp;
|
|
191
|
+
const durationSec = lastTimestamp - firstTimestamp;
|
|
192
|
+
if (durationSec > 0) {
|
|
193
|
+
fps = Math.round(frameTimestamps.length / durationSec);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
fps = fps || 30; // Default to 30fps
|
|
197
|
+
|
|
198
|
+
console.log(`[cdp-recorder] Assembling video at ${fps} fps`);
|
|
199
|
+
|
|
200
|
+
const frameExtension = format === "png" ? "png" : "jpg";
|
|
201
|
+
const framePattern = path.join(framesDir, `frame_%08d.${frameExtension}`);
|
|
202
|
+
|
|
203
|
+
// Determine encoding based on output format
|
|
204
|
+
// WebM requires VP8/VP9/AV1, MP4 can use H.264
|
|
205
|
+
const outputExtension = path.extname(outputPath).toLowerCase();
|
|
206
|
+
const isWebM = outputExtension === ".webm";
|
|
207
|
+
|
|
208
|
+
let encodingArgs;
|
|
209
|
+
if (isWebM) {
|
|
210
|
+
// Use VP9 with lossless mode for WebM
|
|
211
|
+
encodingArgs = [
|
|
212
|
+
"-c:v", "libvpx-vp9",
|
|
213
|
+
"-lossless", "1", // Lossless mode
|
|
214
|
+
"-row-mt", "1", // Enable row-based multithreading
|
|
215
|
+
"-an", // No audio for now
|
|
216
|
+
];
|
|
217
|
+
} else {
|
|
218
|
+
// Use H.264 lossless for MP4
|
|
219
|
+
encodingArgs = getIntermediateEncodingArgs({ includeAudio: false });
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
await runFFmpeg([
|
|
223
|
+
"-y",
|
|
224
|
+
"-framerate",
|
|
225
|
+
String(fps),
|
|
226
|
+
"-i",
|
|
227
|
+
framePattern,
|
|
228
|
+
...encodingArgs,
|
|
229
|
+
"-pix_fmt",
|
|
230
|
+
isWebM ? "yuv420p" : "yuv420p",
|
|
231
|
+
outputPath,
|
|
232
|
+
]);
|
|
233
|
+
|
|
234
|
+
console.log(`[cdp-recorder] Video assembled: ${outputPath}`);
|
|
235
|
+
|
|
236
|
+
return outputPath;
|
|
237
|
+
},
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Clean up frame files
|
|
241
|
+
*/
|
|
242
|
+
async cleanup() {
|
|
243
|
+
try {
|
|
244
|
+
await fs.rm(framesDir, { recursive: true, force: true });
|
|
245
|
+
} catch {
|
|
246
|
+
// Ignore cleanup errors
|
|
247
|
+
}
|
|
248
|
+
},
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Get help text for CDP recording options
|
|
254
|
+
* @returns {string}
|
|
255
|
+
*/
|
|
256
|
+
export function getCDPRecordingHelpText() {
|
|
257
|
+
return `
|
|
258
|
+
High-Quality Recording Options:
|
|
259
|
+
--hq-video Use CDP-based high-quality recording instead of Playwright
|
|
260
|
+
Captures frames at maximum quality and assembles with FFmpeg
|
|
261
|
+
--hq-format <format> Frame format: jpeg (default) or png (lossless, larger files)
|
|
262
|
+
--hq-quality <1-100> JPEG quality (default: 100, ignored for PNG)`;
|
|
263
|
+
}
|
|
@@ -28,6 +28,9 @@ sceneforge record --definition <path> [options]
|
|
|
28
28
|
| `--viewport` | Target video resolution (preset or WxH, default: 1440x900) |
|
|
29
29
|
| `--width` | Video width (overrides --viewport) |
|
|
30
30
|
| `--height` | Video height (overrides --viewport) |
|
|
31
|
+
| `--hq-video` | Use CDP-based high-quality recording |
|
|
32
|
+
| `--hq-format` | Frame format: jpeg (default) or png (lossless) |
|
|
33
|
+
| `--hq-quality` | JPEG quality 1-100 (default: 100) |
|
|
31
34
|
|
|
32
35
|
**Viewport Presets:** 720p (1280x720), 1080p (1920x1080), 1440p (2560x1440), 4k (3840x2160)
|
|
33
36
|
|
|
@@ -47,6 +50,12 @@ sceneforge record -d demo.yaml -b http://localhost:3000 --viewport 1080p
|
|
|
47
50
|
|
|
48
51
|
# Record at 4K resolution
|
|
49
52
|
sceneforge record -d demo.yaml -b http://localhost:3000 --viewport 4k
|
|
53
|
+
|
|
54
|
+
# High-quality CDP recording (bypasses Playwright's bitrate limits)
|
|
55
|
+
sceneforge record -d demo.yaml -b http://localhost:3000 --hq-video
|
|
56
|
+
|
|
57
|
+
# HQ recording with lossless PNG frames
|
|
58
|
+
sceneforge record -d demo.yaml -b http://localhost:3000 --hq-video --hq-format png
|
|
50
59
|
```
|
|
51
60
|
|
|
52
61
|
### setup
|
|
@@ -96,6 +105,9 @@ sceneforge pipeline --definition <path> [options]
|
|
|
96
105
|
| `--output-size` | Final output dimensions (preset or WxH) |
|
|
97
106
|
| `--output-width` | Output width (-1 for auto) |
|
|
98
107
|
| `--output-height` | Output height (-1 for auto) |
|
|
108
|
+
| `--hq-video` | Use CDP-based high-quality recording |
|
|
109
|
+
| `--hq-format` | Frame format: jpeg (default) or png |
|
|
110
|
+
| `--hq-quality` | JPEG quality 1-100 (default: 100) |
|
|
99
111
|
|
|
100
112
|
**Examples:**
|
|
101
113
|
```bash
|
|
@@ -434,3 +446,72 @@ sceneforge pipeline -d demo.yaml -b http://localhost:3000 \
|
|
|
434
446
|
--output-size 1080p \
|
|
435
447
|
--quality high
|
|
436
448
|
```
|
|
449
|
+
|
|
450
|
+
## High-Quality Recording (CDP)
|
|
451
|
+
|
|
452
|
+
SceneForge offers an alternative high-quality recording mode that bypasses Playwright's built-in video recording limitations.
|
|
453
|
+
|
|
454
|
+
### Why Use HQ Recording?
|
|
455
|
+
|
|
456
|
+
Playwright's built-in video recording uses hardcoded FFmpeg settings optimized for debugging rather than production quality:
|
|
457
|
+
- **1Mbps bitrate cap** - Severely limits quality at higher resolutions
|
|
458
|
+
- **Speed 8 encoding** - Prioritizes speed over quality
|
|
459
|
+
- **Realtime deadline** - No time for quality optimization
|
|
460
|
+
|
|
461
|
+
The CDP-based recorder captures frames directly from Chrome DevTools Protocol and assembles them with FFmpeg using our quality settings.
|
|
462
|
+
|
|
463
|
+
### CLI Flags (record, pipeline)
|
|
464
|
+
|
|
465
|
+
```bash
|
|
466
|
+
--hq-video # Enable CDP-based high-quality recording
|
|
467
|
+
--hq-format <format> # Frame format: jpeg (default) or png
|
|
468
|
+
--hq-quality <1-100> # JPEG quality (default: 100, ignored for PNG)
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
### Frame Formats
|
|
472
|
+
|
|
473
|
+
| Format | Quality | File Size | Use Case |
|
|
474
|
+
|--------|---------|-----------|----------|
|
|
475
|
+
| `jpeg` | Very high (at quality 100) | Smaller | Default, good for most cases |
|
|
476
|
+
| `png` | Lossless | Larger | Maximum quality, longer recordings |
|
|
477
|
+
|
|
478
|
+
### How It Works
|
|
479
|
+
|
|
480
|
+
1. **Frame Capture**: Uses Chrome's `Page.startScreencast` CDP method
|
|
481
|
+
2. **Frame Storage**: Saves frames as JPEG (quality 100) or lossless PNG
|
|
482
|
+
3. **Video Assembly**: FFmpeg assembles frames with lossless intermediate encoding
|
|
483
|
+
4. **Final Output**: Quality settings applied at concat step
|
|
484
|
+
|
|
485
|
+
### Examples
|
|
486
|
+
|
|
487
|
+
```bash
|
|
488
|
+
# High-quality recording with default settings
|
|
489
|
+
sceneforge record -d demo.yaml -b http://localhost:3000 --hq-video
|
|
490
|
+
|
|
491
|
+
# Lossless PNG frames for maximum quality
|
|
492
|
+
sceneforge record -d demo.yaml -b http://localhost:3000 --hq-video --hq-format png
|
|
493
|
+
|
|
494
|
+
# Full pipeline with HQ recording
|
|
495
|
+
sceneforge pipeline -d demo.yaml -b http://localhost:3000 --hq-video --quality high
|
|
496
|
+
|
|
497
|
+
# HQ recording at 4K
|
|
498
|
+
sceneforge pipeline -d demo.yaml -b http://localhost:3000 \
|
|
499
|
+
--hq-video \
|
|
500
|
+
--viewport 4k \
|
|
501
|
+
--output-size 1080p \
|
|
502
|
+
--quality high
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
### Trade-offs
|
|
506
|
+
|
|
507
|
+
| Aspect | Standard Recording | HQ Recording |
|
|
508
|
+
|--------|-------------------|--------------|
|
|
509
|
+
| Quality | Limited by 1Mbps cap | Full quality frames |
|
|
510
|
+
| Speed | Faster | Slower (frame-by-frame) |
|
|
511
|
+
| Disk Usage | Moderate | Higher (temporary frames) |
|
|
512
|
+
| Compatibility | Always works | Requires FFmpeg |
|
|
513
|
+
|
|
514
|
+
### When to Use
|
|
515
|
+
|
|
516
|
+
- **Use HQ Recording** when final video quality is critical
|
|
517
|
+
- **Use Standard Recording** for quick drafts, debugging, or when disk space is limited
|
|
@@ -28,6 +28,9 @@ sceneforge record --definition <path> [options]
|
|
|
28
28
|
| `--viewport` | Target video resolution (preset or WxH, default: 1440x900) |
|
|
29
29
|
| `--width` | Video width (overrides --viewport) |
|
|
30
30
|
| `--height` | Video height (overrides --viewport) |
|
|
31
|
+
| `--hq-video` | Use CDP-based high-quality recording |
|
|
32
|
+
| `--hq-format` | Frame format: jpeg (default) or png (lossless) |
|
|
33
|
+
| `--hq-quality` | JPEG quality 1-100 (default: 100) |
|
|
31
34
|
|
|
32
35
|
**Viewport Presets:** 720p (1280x720), 1080p (1920x1080), 1440p (2560x1440), 4k (3840x2160)
|
|
33
36
|
|
|
@@ -47,6 +50,12 @@ sceneforge record -d demo.yaml -b http://localhost:3000 --viewport 1080p
|
|
|
47
50
|
|
|
48
51
|
# Record at 4K resolution
|
|
49
52
|
sceneforge record -d demo.yaml -b http://localhost:3000 --viewport 4k
|
|
53
|
+
|
|
54
|
+
# High-quality CDP recording (bypasses Playwright's bitrate limits)
|
|
55
|
+
sceneforge record -d demo.yaml -b http://localhost:3000 --hq-video
|
|
56
|
+
|
|
57
|
+
# HQ recording with lossless PNG frames
|
|
58
|
+
sceneforge record -d demo.yaml -b http://localhost:3000 --hq-video --hq-format png
|
|
50
59
|
```
|
|
51
60
|
|
|
52
61
|
### setup
|
|
@@ -96,6 +105,9 @@ sceneforge pipeline --definition <path> [options]
|
|
|
96
105
|
| `--output-size` | Final output dimensions (preset or WxH) |
|
|
97
106
|
| `--output-width` | Output width (-1 for auto) |
|
|
98
107
|
| `--output-height` | Output height (-1 for auto) |
|
|
108
|
+
| `--hq-video` | Use CDP-based high-quality recording |
|
|
109
|
+
| `--hq-format` | Frame format: jpeg (default) or png |
|
|
110
|
+
| `--hq-quality` | JPEG quality 1-100 (default: 100) |
|
|
99
111
|
|
|
100
112
|
**Examples:**
|
|
101
113
|
```bash
|
|
@@ -434,3 +446,72 @@ sceneforge pipeline -d demo.yaml -b http://localhost:3000 \
|
|
|
434
446
|
--output-size 1080p \
|
|
435
447
|
--quality high
|
|
436
448
|
```
|
|
449
|
+
|
|
450
|
+
## High-Quality Recording (CDP)
|
|
451
|
+
|
|
452
|
+
SceneForge offers an alternative high-quality recording mode that bypasses Playwright's built-in video recording limitations.
|
|
453
|
+
|
|
454
|
+
### Why Use HQ Recording?
|
|
455
|
+
|
|
456
|
+
Playwright's built-in video recording uses hardcoded FFmpeg settings optimized for debugging rather than production quality:
|
|
457
|
+
- **1Mbps bitrate cap** - Severely limits quality at higher resolutions
|
|
458
|
+
- **Speed 8 encoding** - Prioritizes speed over quality
|
|
459
|
+
- **Realtime deadline** - No time for quality optimization
|
|
460
|
+
|
|
461
|
+
The CDP-based recorder captures frames directly from Chrome DevTools Protocol and assembles them with FFmpeg using our quality settings.
|
|
462
|
+
|
|
463
|
+
### CLI Flags (record, pipeline)
|
|
464
|
+
|
|
465
|
+
```bash
|
|
466
|
+
--hq-video # Enable CDP-based high-quality recording
|
|
467
|
+
--hq-format <format> # Frame format: jpeg (default) or png
|
|
468
|
+
--hq-quality <1-100> # JPEG quality (default: 100, ignored for PNG)
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
### Frame Formats
|
|
472
|
+
|
|
473
|
+
| Format | Quality | File Size | Use Case |
|
|
474
|
+
|--------|---------|-----------|----------|
|
|
475
|
+
| `jpeg` | Very high (at quality 100) | Smaller | Default, good for most cases |
|
|
476
|
+
| `png` | Lossless | Larger | Maximum quality, longer recordings |
|
|
477
|
+
|
|
478
|
+
### How It Works
|
|
479
|
+
|
|
480
|
+
1. **Frame Capture**: Uses Chrome's `Page.startScreencast` CDP method
|
|
481
|
+
2. **Frame Storage**: Saves frames as JPEG (quality 100) or lossless PNG
|
|
482
|
+
3. **Video Assembly**: FFmpeg assembles frames with lossless intermediate encoding
|
|
483
|
+
4. **Final Output**: Quality settings applied at concat step
|
|
484
|
+
|
|
485
|
+
### Examples
|
|
486
|
+
|
|
487
|
+
```bash
|
|
488
|
+
# High-quality recording with default settings
|
|
489
|
+
sceneforge record -d demo.yaml -b http://localhost:3000 --hq-video
|
|
490
|
+
|
|
491
|
+
# Lossless PNG frames for maximum quality
|
|
492
|
+
sceneforge record -d demo.yaml -b http://localhost:3000 --hq-video --hq-format png
|
|
493
|
+
|
|
494
|
+
# Full pipeline with HQ recording
|
|
495
|
+
sceneforge pipeline -d demo.yaml -b http://localhost:3000 --hq-video --quality high
|
|
496
|
+
|
|
497
|
+
# HQ recording at 4K
|
|
498
|
+
sceneforge pipeline -d demo.yaml -b http://localhost:3000 \
|
|
499
|
+
--hq-video \
|
|
500
|
+
--viewport 4k \
|
|
501
|
+
--output-size 1080p \
|
|
502
|
+
--quality high
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
### Trade-offs
|
|
506
|
+
|
|
507
|
+
| Aspect | Standard Recording | HQ Recording |
|
|
508
|
+
|--------|-------------------|--------------|
|
|
509
|
+
| Quality | Limited by 1Mbps cap | Full quality frames |
|
|
510
|
+
| Speed | Faster | Slower (frame-by-frame) |
|
|
511
|
+
| Disk Usage | Moderate | Higher (temporary frames) |
|
|
512
|
+
| Compatibility | Always works | Requires FFmpeg |
|
|
513
|
+
|
|
514
|
+
### When to Use
|
|
515
|
+
|
|
516
|
+
- **Use HQ Recording** when final video quality is critical
|
|
517
|
+
- **Use Standard Recording** for quick drafts, debugging, or when disk space is limited
|