@t3lnet/sceneforge 1.0.27 → 1.0.28

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.
@@ -182,7 +182,8 @@ export async function runRecordDemoCommand(argv) {
182
182
  const hqVideo = hasFlag(args, "--hq-video");
183
183
  const hqFormat = getFlagValue(args, "--hq-format") ?? "jpeg";
184
184
  const hqQuality = parseInt(getFlagValue(args, "--hq-quality") ?? "100", 10);
185
- const hqFps = parseInt(getFlagValue(args, "--hq-fps") ?? "30", 10);
185
+ const hqFpsArg = getFlagValue(args, "--hq-fps");
186
+ const hqFps = hqFpsArg ? parseInt(hqFpsArg, 10) : undefined; // undefined = use actual capture rate
186
187
 
187
188
  if (!baseUrl) {
188
189
  console.error("[error] --base-url is required");
@@ -262,6 +263,8 @@ export async function runRecordDemoCommand(argv) {
262
263
  maxHeight: viewport.height,
263
264
  });
264
265
  await cdpRecorder.start();
266
+ // Wait for first frame to ensure recording is active before demo starts
267
+ await cdpRecorder.waitForFirstFrame();
265
268
  }
266
269
 
267
270
  const startUrl = resolveStartUrl(startPath, baseUrl);
@@ -305,7 +308,7 @@ export async function runRecordDemoCommand(argv) {
305
308
  const finalVideoPath = path.join(outputPaths.videosDir, `${definition.name}.webm`);
306
309
  try {
307
310
  if (!demoError && result?.success) {
308
- await cdpRecorder.assembleVideo(finalVideoPath, { fps: hqFps });
311
+ await cdpRecorder.assembleVideo(finalVideoPath, hqFps ? { fps: hqFps } : {});
309
312
  console.log(`[record] High-quality video saved: ${finalVideoPath}`);
310
313
  if (result.scriptPath) {
311
314
  // Use the actual first frame time for alignment, not when we started the recorder
@@ -58,6 +58,7 @@ export function createCDPRecorder(page, options) {
58
58
  let firstFrameTime = null; // Wall clock time when first frame was received
59
59
  let writeQueue = Promise.resolve();
60
60
  let nextFrameNumber = 0;
61
+ let recordingDuration = 0; // Duration in ms from start to stop
61
62
 
62
63
  const frameHandler = (params) => {
63
64
  if (!isRecording) return;
@@ -139,6 +140,24 @@ export function createCDPRecorder(page, options) {
139
140
  console.log("[cdp-recorder] Started high-quality screen capture");
140
141
  },
141
142
 
143
+ /**
144
+ * Wait for the first frame to be captured.
145
+ * Call this after start() and before beginning the demo to ensure recording is active.
146
+ * @param {number} [timeoutMs=5000] - Maximum time to wait for first frame
147
+ * @returns {Promise<void>}
148
+ */
149
+ async waitForFirstFrame(timeoutMs = 5000) {
150
+ const startWait = Date.now();
151
+ while (firstFrameTime === null) {
152
+ if (Date.now() - startWait > timeoutMs) {
153
+ console.warn("[cdp-recorder] Timeout waiting for first frame - proceeding anyway");
154
+ return;
155
+ }
156
+ await new Promise(resolve => setTimeout(resolve, 50));
157
+ }
158
+ console.log(`[cdp-recorder] First frame received after ${Date.now() - startWait}ms`);
159
+ },
160
+
142
161
  /**
143
162
  * Stop recording and return frame information
144
163
  * @returns {Promise<{frameCount: number, duration: number, framesDir: string}>}
@@ -149,7 +168,7 @@ export function createCDPRecorder(page, options) {
149
168
  }
150
169
 
151
170
  isRecording = false;
152
- const duration = Date.now() - startTime;
171
+ recordingDuration = Date.now() - startTime;
153
172
 
154
173
  try {
155
174
  await cdpSession.send("Page.stopScreencast");
@@ -163,8 +182,12 @@ export function createCDPRecorder(page, options) {
163
182
  await writeQueue;
164
183
 
165
184
  const delayToFirstFrame = firstFrameTime ? firstFrameTime - startTime : 0;
185
+ const actualFps = frameTimestamps.length > 0 && recordingDuration > 0
186
+ ? frameTimestamps.length / (recordingDuration / 1000)
187
+ : 30;
188
+
166
189
  console.log(
167
- `[cdp-recorder] Captured ${frameTimestamps.length} frames in ${(duration / 1000).toFixed(2)}s`
190
+ `[cdp-recorder] Captured ${frameTimestamps.length} frames in ${(recordingDuration / 1000).toFixed(2)}s (${actualFps.toFixed(1)} fps)`
168
191
  );
169
192
  if (delayToFirstFrame > 100) {
170
193
  console.log(`[cdp-recorder] First frame delay: ${delayToFirstFrame}ms`);
@@ -172,11 +195,12 @@ export function createCDPRecorder(page, options) {
172
195
 
173
196
  return {
174
197
  frameCount: frameTimestamps.length,
175
- duration,
198
+ duration: recordingDuration,
176
199
  framesDir,
177
200
  frameTimestamps,
178
201
  firstFrameTime, // Wall clock time when first frame was captured
179
202
  startTime, // Wall clock time when recording was started
203
+ actualFps, // Actual capture framerate for real-time playback
180
204
  };
181
205
  },
182
206
 
@@ -195,10 +219,21 @@ export function createCDPRecorder(page, options) {
195
219
  throw new Error("No frames captured - cannot assemble video");
196
220
  }
197
221
 
198
- // Use provided fps or default to 30
199
- const fps = fpsOverride || 30;
222
+ // Calculate actual capture FPS for real-time playback
223
+ // This ensures video duration matches the actual recording duration
224
+ const calculatedFps = frameTimestamps.length > 0 && recordingDuration > 0
225
+ ? frameTimestamps.length / (recordingDuration / 1000)
226
+ : 30;
227
+
228
+ // Use provided fps override or actual capture fps for real-time playback
229
+ const fps = fpsOverride || calculatedFps;
230
+
231
+ if (fpsOverride && Math.abs(fpsOverride - calculatedFps) > 1) {
232
+ console.log(`[cdp-recorder] Note: Using ${fps} fps (actual capture rate was ${calculatedFps.toFixed(1)} fps)`);
233
+ console.log(`[cdp-recorder] This will ${fpsOverride > calculatedFps ? 'speed up' : 'slow down'} playback`);
234
+ }
200
235
 
201
- console.log(`[cdp-recorder] Assembling video at ${fps} fps`);
236
+ console.log(`[cdp-recorder] Assembling video at ${fps.toFixed(1)} fps`);
202
237
 
203
238
  const frameExtension = format === "png" ? "png" : "jpg";
204
239
  const framePattern = path.join(framesDir, `frame_%08d.${frameExtension}`);
@@ -263,5 +298,6 @@ High-Quality Recording Options:
263
298
  Captures frames at maximum quality and assembles with FFmpeg
264
299
  --hq-format <format> Frame format: jpeg (default) or png (lossless, larger files)
265
300
  --hq-quality <1-100> JPEG quality (default: 100, ignored for PNG)
266
- --hq-fps <fps> Output framerate (default: 30)`;
301
+ --hq-fps <fps> Output framerate (default: actual capture rate for real-time)
302
+ Override only if you want to change playback speed`;
267
303
  }
@@ -466,6 +466,7 @@ The CDP-based recorder captures frames directly from Chrome DevTools Protocol an
466
466
  --hq-video # Enable CDP-based high-quality recording
467
467
  --hq-format <format> # Frame format: jpeg (default) or png
468
468
  --hq-quality <1-100> # JPEG quality (default: 100, ignored for PNG)
469
+ --hq-fps <fps> # Override output framerate (default: actual capture rate for real-time)
469
470
  ```
470
471
 
471
472
  ### Frame Formats
@@ -466,6 +466,7 @@ The CDP-based recorder captures frames directly from Chrome DevTools Protocol an
466
466
  --hq-video # Enable CDP-based high-quality recording
467
467
  --hq-format <format> # Frame format: jpeg (default) or png
468
468
  --hq-quality <1-100> # JPEG quality (default: 100, ignored for PNG)
469
+ --hq-fps <fps> # Override output framerate (default: actual capture rate for real-time)
469
470
  ```
470
471
 
471
472
  ### Frame Formats
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@t3lnet/sceneforge",
3
- "version": "1.0.27",
3
+ "version": "1.0.28",
4
4
  "description": "SceneForge runner and generation utilities for YAML-driven demos",
5
5
  "license": "MIT",
6
6
  "author": "T3LNET",