@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.
@@ -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
- if (!noVideo) {
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: noVideo ? undefined : { dir: recordDir, size: viewport },
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
- if (startUrl) {
246
- await page.goto(startUrl, { waitUntil: "networkidle" });
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
- const result = await runDemo(
249
- definition,
250
- {
251
- page,
252
- baseURL: baseUrl,
253
- outputDir: outputPaths.outputDir,
254
- assetBaseDir: assetRoot ? toAbsolute(rootDir, assetRoot) : undefined,
255
- videoRecordingStartTime,
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
- if (video) {
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.scriptPath) {
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@t3lnet/sceneforge",
3
- "version": "1.0.23",
3
+ "version": "1.0.25",
4
4
  "description": "SceneForge runner and generation utilities for YAML-driven demos",
5
5
  "license": "MIT",
6
6
  "author": "T3LNET",