@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.
@@ -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,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
- if (video) {
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@t3lnet/sceneforge",
3
- "version": "1.0.23",
3
+ "version": "1.0.24",
4
4
  "description": "SceneForge runner and generation utilities for YAML-driven demos",
5
5
  "license": "MIT",
6
6
  "author": "T3LNET",