@t3lnet/sceneforge 1.0.23 → 1.0.24
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,8 +247,21 @@ 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();
|
|
252
|
+
|
|
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();
|
|
264
|
+
}
|
|
243
265
|
const startUrl = resolveStartUrl(startPath, baseUrl);
|
|
244
266
|
|
|
245
267
|
if (startUrl) {
|
|
@@ -260,10 +282,28 @@ export async function runRecordDemoCommand(argv) {
|
|
|
260
282
|
}
|
|
261
283
|
);
|
|
262
284
|
|
|
285
|
+
// Stop CDP recorder before closing context (if using HQ video)
|
|
286
|
+
if (cdpRecorder) {
|
|
287
|
+
await cdpRecorder.stop();
|
|
288
|
+
}
|
|
289
|
+
|
|
263
290
|
await context.close();
|
|
264
291
|
await browser.close();
|
|
265
292
|
|
|
266
|
-
|
|
293
|
+
// Handle video based on recording method
|
|
294
|
+
if (cdpRecorder) {
|
|
295
|
+
// Assemble CDP frames into video
|
|
296
|
+
const finalVideoPath = path.join(outputPaths.videosDir, `${definition.name}.webm`);
|
|
297
|
+
try {
|
|
298
|
+
await cdpRecorder.assembleVideo(finalVideoPath);
|
|
299
|
+
console.log(`[record] High-quality video saved: ${finalVideoPath}`);
|
|
300
|
+
if (result.scriptPath) {
|
|
301
|
+
await alignScriptToVideo(result.scriptPath, finalVideoPath, videoRecordingStartTime);
|
|
302
|
+
}
|
|
303
|
+
} finally {
|
|
304
|
+
await cdpRecorder.cleanup();
|
|
305
|
+
}
|
|
306
|
+
} else if (video) {
|
|
267
307
|
const recordedPath = await video.path();
|
|
268
308
|
const finalVideoPath = path.join(outputPaths.videosDir, `${definition.name}.webm`);
|
|
269
309
|
await moveVideo(recordedPath, finalVideoPath);
|
|
@@ -0,0 +1,236 @@
|
|
|
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
|
+
|
|
59
|
+
const frameHandler = async (params) => {
|
|
60
|
+
if (!isRecording) return;
|
|
61
|
+
|
|
62
|
+
const { data, metadata, sessionId } = params;
|
|
63
|
+
frameCount += 1;
|
|
64
|
+
|
|
65
|
+
// Acknowledge the frame to continue receiving
|
|
66
|
+
try {
|
|
67
|
+
await cdpSession.send("Page.screencastFrameAck", { sessionId });
|
|
68
|
+
} 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 = frameTimestamps.length;
|
|
79
|
+
const paddedNumber = String(frameNumber).padStart(8, "0");
|
|
80
|
+
const extension = format === "png" ? "png" : "jpg";
|
|
81
|
+
const framePath = path.join(framesDir, `frame_${paddedNumber}.${extension}`);
|
|
82
|
+
|
|
83
|
+
// Decode base64 and save frame
|
|
84
|
+
const buffer = Buffer.from(data, "base64");
|
|
85
|
+
await fs.writeFile(framePath, buffer);
|
|
86
|
+
|
|
87
|
+
frameTimestamps.push({
|
|
88
|
+
frameNumber,
|
|
89
|
+
timestamp,
|
|
90
|
+
path: framePath,
|
|
91
|
+
});
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
/**
|
|
96
|
+
* Start recording frames via CDP screencast
|
|
97
|
+
*/
|
|
98
|
+
async start() {
|
|
99
|
+
await fs.mkdir(framesDir, { recursive: true });
|
|
100
|
+
|
|
101
|
+
// Create CDP session for the page
|
|
102
|
+
cdpSession = await page.context().newCDPSession(page);
|
|
103
|
+
|
|
104
|
+
// Set up frame handler
|
|
105
|
+
cdpSession.on("Page.screencastFrame", frameHandler);
|
|
106
|
+
|
|
107
|
+
// Start screencast with high quality settings
|
|
108
|
+
// format: 'jpeg' or 'png'
|
|
109
|
+
// quality: 0-100 for jpeg (100 = best), ignored for png
|
|
110
|
+
// maxWidth/maxHeight: optional frame dimensions
|
|
111
|
+
// everyNthFrame: capture frequency (1 = every frame)
|
|
112
|
+
await cdpSession.send("Page.startScreencast", {
|
|
113
|
+
format,
|
|
114
|
+
quality,
|
|
115
|
+
maxWidth,
|
|
116
|
+
maxHeight,
|
|
117
|
+
everyNthFrame,
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
isRecording = true;
|
|
121
|
+
startTime = Date.now();
|
|
122
|
+
frameCount = 0;
|
|
123
|
+
frameTimestamps = [];
|
|
124
|
+
|
|
125
|
+
console.log("[cdp-recorder] Started high-quality screen capture");
|
|
126
|
+
},
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Stop recording and return frame information
|
|
130
|
+
* @returns {Promise<{frameCount: number, duration: number, framesDir: string}>}
|
|
131
|
+
*/
|
|
132
|
+
async stop() {
|
|
133
|
+
if (!isRecording || !cdpSession) {
|
|
134
|
+
return { frameCount: 0, duration: 0, framesDir };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
isRecording = false;
|
|
138
|
+
const duration = Date.now() - startTime;
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
await cdpSession.send("Page.stopScreencast");
|
|
142
|
+
cdpSession.off("Page.screencastFrame", frameHandler);
|
|
143
|
+
await cdpSession.detach();
|
|
144
|
+
} catch {
|
|
145
|
+
// Session may already be closed
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
console.log(
|
|
149
|
+
`[cdp-recorder] Captured ${frameTimestamps.length} frames in ${(duration / 1000).toFixed(2)}s`
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
frameCount: frameTimestamps.length,
|
|
154
|
+
duration,
|
|
155
|
+
framesDir,
|
|
156
|
+
frameTimestamps,
|
|
157
|
+
};
|
|
158
|
+
},
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Assemble captured frames into a video using FFmpeg
|
|
162
|
+
*
|
|
163
|
+
* @param {string} outputPath - Output video path
|
|
164
|
+
* @param {Object} [assembleOptions] - Assembly options
|
|
165
|
+
* @param {number} [assembleOptions.fps] - Output framerate (auto-detected if not specified)
|
|
166
|
+
* @returns {Promise<string>} - Path to assembled video
|
|
167
|
+
*/
|
|
168
|
+
async assembleVideo(outputPath, assembleOptions = {}) {
|
|
169
|
+
const { fps: fpsOverride } = assembleOptions;
|
|
170
|
+
|
|
171
|
+
if (frameTimestamps.length === 0) {
|
|
172
|
+
throw new Error("No frames captured - cannot assemble video");
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Calculate actual FPS from captured frame timestamps
|
|
176
|
+
let fps = fpsOverride;
|
|
177
|
+
if (!fps && frameTimestamps.length > 1) {
|
|
178
|
+
const firstTimestamp = frameTimestamps[0].timestamp;
|
|
179
|
+
const lastTimestamp = frameTimestamps[frameTimestamps.length - 1].timestamp;
|
|
180
|
+
const durationSec = lastTimestamp - firstTimestamp;
|
|
181
|
+
if (durationSec > 0) {
|
|
182
|
+
fps = Math.round(frameTimestamps.length / durationSec);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
fps = fps || 30; // Default to 30fps
|
|
186
|
+
|
|
187
|
+
console.log(`[cdp-recorder] Assembling video at ${fps} fps`);
|
|
188
|
+
|
|
189
|
+
const extension = format === "png" ? "png" : "jpg";
|
|
190
|
+
const framePattern = path.join(framesDir, `frame_%08d.${extension}`);
|
|
191
|
+
|
|
192
|
+
// Use lossless encoding for the assembled video (will be compressed at concat step)
|
|
193
|
+
const encodingArgs = getIntermediateEncodingArgs({ includeAudio: false });
|
|
194
|
+
|
|
195
|
+
await runFFmpeg([
|
|
196
|
+
"-y",
|
|
197
|
+
"-framerate",
|
|
198
|
+
String(fps),
|
|
199
|
+
"-i",
|
|
200
|
+
framePattern,
|
|
201
|
+
...encodingArgs,
|
|
202
|
+
"-pix_fmt",
|
|
203
|
+
"yuv420p", // Ensure compatibility
|
|
204
|
+
outputPath,
|
|
205
|
+
]);
|
|
206
|
+
|
|
207
|
+
console.log(`[cdp-recorder] Video assembled: ${outputPath}`);
|
|
208
|
+
|
|
209
|
+
return outputPath;
|
|
210
|
+
},
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Clean up frame files
|
|
214
|
+
*/
|
|
215
|
+
async cleanup() {
|
|
216
|
+
try {
|
|
217
|
+
await fs.rm(framesDir, { recursive: true, force: true });
|
|
218
|
+
} catch {
|
|
219
|
+
// Ignore cleanup errors
|
|
220
|
+
}
|
|
221
|
+
},
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Get help text for CDP recording options
|
|
227
|
+
* @returns {string}
|
|
228
|
+
*/
|
|
229
|
+
export function getCDPRecordingHelpText() {
|
|
230
|
+
return `
|
|
231
|
+
High-Quality Recording Options:
|
|
232
|
+
--hq-video Use CDP-based high-quality recording instead of Playwright
|
|
233
|
+
Captures frames at maximum quality and assembles with FFmpeg
|
|
234
|
+
--hq-format <format> Frame format: jpeg (default) or png (lossless, larger files)
|
|
235
|
+
--hq-quality <1-100> JPEG quality (default: 100, ignored for PNG)`;
|
|
236
|
+
}
|
|
@@ -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
|