@t3lnet/sceneforge 1.0.26 → 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);
@@ -292,8 +295,9 @@ export async function runRecordDemoCommand(argv) {
292
295
  }
293
296
 
294
297
  // Stop CDP recorder before closing context
298
+ let cdpStopInfo = null;
295
299
  if (cdpRecorder) {
296
- await cdpRecorder.stop();
300
+ cdpStopInfo = await cdpRecorder.stop();
297
301
  }
298
302
 
299
303
  await context.close();
@@ -304,10 +308,12 @@ export async function runRecordDemoCommand(argv) {
304
308
  const finalVideoPath = path.join(outputPaths.videosDir, `${definition.name}.webm`);
305
309
  try {
306
310
  if (!demoError && result?.success) {
307
- await cdpRecorder.assembleVideo(finalVideoPath, { fps: hqFps });
311
+ await cdpRecorder.assembleVideo(finalVideoPath, hqFps ? { fps: hqFps } : {});
308
312
  console.log(`[record] High-quality video saved: ${finalVideoPath}`);
309
313
  if (result.scriptPath) {
310
- await alignScriptToVideo(result.scriptPath, finalVideoPath, videoRecordingStartTime);
314
+ // Use the actual first frame time for alignment, not when we started the recorder
315
+ const actualVideoStartTime = cdpStopInfo?.firstFrameTime || videoRecordingStartTime;
316
+ await alignScriptToVideo(result.scriptPath, finalVideoPath, actualVideoStartTime);
311
317
  }
312
318
  }
313
319
  } finally {
@@ -55,8 +55,10 @@ export function createCDPRecorder(page, options) {
55
55
  let frameTimestamps = [];
56
56
  let isRecording = false;
57
57
  let startTime = null;
58
+ let firstFrameTime = null; // Wall clock time when first frame was received
58
59
  let writeQueue = Promise.resolve();
59
60
  let nextFrameNumber = 0;
61
+ let recordingDuration = 0; // Duration in ms from start to stop
60
62
 
61
63
  const frameHandler = (params) => {
62
64
  if (!isRecording) return;
@@ -64,6 +66,11 @@ export function createCDPRecorder(page, options) {
64
66
  const { data, metadata, sessionId } = params;
65
67
  frameCount += 1;
66
68
 
69
+ // Track when we receive the first frame (wall clock time)
70
+ if (firstFrameTime === null) {
71
+ firstFrameTime = Date.now();
72
+ }
73
+
67
74
  // Acknowledge the frame immediately to continue receiving
68
75
  cdpSession.send("Page.screencastFrameAck", { sessionId }).catch(() => {
69
76
  // Session may be closed
@@ -133,6 +140,24 @@ export function createCDPRecorder(page, options) {
133
140
  console.log("[cdp-recorder] Started high-quality screen capture");
134
141
  },
135
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
+
136
161
  /**
137
162
  * Stop recording and return frame information
138
163
  * @returns {Promise<{frameCount: number, duration: number, framesDir: string}>}
@@ -143,7 +168,7 @@ export function createCDPRecorder(page, options) {
143
168
  }
144
169
 
145
170
  isRecording = false;
146
- const duration = Date.now() - startTime;
171
+ recordingDuration = Date.now() - startTime;
147
172
 
148
173
  try {
149
174
  await cdpSession.send("Page.stopScreencast");
@@ -156,15 +181,26 @@ export function createCDPRecorder(page, options) {
156
181
  // Wait for all pending frame writes to complete
157
182
  await writeQueue;
158
183
 
184
+ const delayToFirstFrame = firstFrameTime ? firstFrameTime - startTime : 0;
185
+ const actualFps = frameTimestamps.length > 0 && recordingDuration > 0
186
+ ? frameTimestamps.length / (recordingDuration / 1000)
187
+ : 30;
188
+
159
189
  console.log(
160
- `[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)`
161
191
  );
192
+ if (delayToFirstFrame > 100) {
193
+ console.log(`[cdp-recorder] First frame delay: ${delayToFirstFrame}ms`);
194
+ }
162
195
 
163
196
  return {
164
197
  frameCount: frameTimestamps.length,
165
- duration,
198
+ duration: recordingDuration,
166
199
  framesDir,
167
200
  frameTimestamps,
201
+ firstFrameTime, // Wall clock time when first frame was captured
202
+ startTime, // Wall clock time when recording was started
203
+ actualFps, // Actual capture framerate for real-time playback
168
204
  };
169
205
  },
170
206
 
@@ -183,10 +219,21 @@ export function createCDPRecorder(page, options) {
183
219
  throw new Error("No frames captured - cannot assemble video");
184
220
  }
185
221
 
186
- // Use provided fps or default to 30
187
- 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
+ }
188
235
 
189
- console.log(`[cdp-recorder] Assembling video at ${fps} fps`);
236
+ console.log(`[cdp-recorder] Assembling video at ${fps.toFixed(1)} fps`);
190
237
 
191
238
  const frameExtension = format === "png" ? "png" : "jpg";
192
239
  const framePattern = path.join(framesDir, `frame_%08d.${frameExtension}`);
@@ -251,5 +298,6 @@ High-Quality Recording Options:
251
298
  Captures frames at maximum quality and assembles with FFmpeg
252
299
  --hq-format <format> Frame format: jpeg (default) or png (lossless, larger files)
253
300
  --hq-quality <1-100> JPEG quality (default: 100, ignored for PNG)
254
- --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`;
255
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.26",
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",