@t3lnet/sceneforge 1.0.24 → 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.
@@ -262,27 +262,35 @@ export async function runRecordDemoCommand(argv) {
262
262
  });
263
263
  await cdpRecorder.start();
264
264
  }
265
+
265
266
  const startUrl = resolveStartUrl(startPath, baseUrl);
266
267
 
267
- if (startUrl) {
268
- await page.goto(startUrl, { waitUntil: "networkidle" });
269
- }
270
- const result = await runDemo(
271
- definition,
272
- {
273
- page,
274
- baseURL: baseUrl,
275
- outputDir: outputPaths.outputDir,
276
- assetBaseDir: assetRoot ? toAbsolute(rootDir, assetRoot) : undefined,
277
- videoRecordingStartTime,
278
- },
279
- {
280
- generateScripts: true,
281
- scriptOutputDir: path.join(outputPaths.outputDir, "scripts"),
268
+ let result;
269
+ let demoError = null;
270
+
271
+ try {
272
+ if (startUrl) {
273
+ await page.goto(startUrl, { waitUntil: "networkidle" });
282
274
  }
283
- );
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
+ }
284
292
 
285
- // Stop CDP recorder before closing context (if using HQ video)
293
+ // Stop CDP recorder before closing context
286
294
  if (cdpRecorder) {
287
295
  await cdpRecorder.stop();
288
296
  }
@@ -292,27 +300,35 @@ export async function runRecordDemoCommand(argv) {
292
300
 
293
301
  // Handle video based on recording method
294
302
  if (cdpRecorder) {
295
- // Assemble CDP frames into video
296
303
  const finalVideoPath = path.join(outputPaths.videosDir, `${definition.name}.webm`);
297
304
  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);
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
+ }
302
311
  }
303
312
  } finally {
313
+ // Always cleanup frames
304
314
  await cdpRecorder.cleanup();
315
+ console.log(`[record] Cleaned up temporary frames from: ${framesDir}`);
305
316
  }
306
317
  } else if (video) {
307
318
  const recordedPath = await video.path();
308
319
  const finalVideoPath = path.join(outputPaths.videosDir, `${definition.name}.webm`);
309
320
  await moveVideo(recordedPath, finalVideoPath);
310
321
  console.log(`[record] Video saved: ${finalVideoPath}`);
311
- if (result.scriptPath) {
322
+ if (result?.scriptPath) {
312
323
  await alignScriptToVideo(result.scriptPath, finalVideoPath, videoRecordingStartTime);
313
324
  }
314
325
  }
315
326
 
327
+ // Re-throw demo error after cleanup
328
+ if (demoError) {
329
+ throw demoError;
330
+ }
331
+
316
332
  if (result.success) {
317
333
  console.log(`[record] ✓ Completed ${definition.name}`);
318
334
  if (result.scriptPath) {
@@ -55,19 +55,19 @@ export function createCDPRecorder(page, options) {
55
55
  let frameTimestamps = [];
56
56
  let isRecording = false;
57
57
  let startTime = null;
58
+ let writeQueue = Promise.resolve();
59
+ let nextFrameNumber = 0;
58
60
 
59
- const frameHandler = async (params) => {
61
+ const frameHandler = (params) => {
60
62
  if (!isRecording) return;
61
63
 
62
64
  const { data, metadata, sessionId } = params;
63
65
  frameCount += 1;
64
66
 
65
- // Acknowledge the frame to continue receiving
66
- try {
67
- await cdpSession.send("Page.screencastFrameAck", { sessionId });
68
- } catch {
67
+ // Acknowledge the frame immediately to continue receiving
68
+ cdpSession.send("Page.screencastFrameAck", { sessionId }).catch(() => {
69
69
  // Session may be closed
70
- }
70
+ });
71
71
 
72
72
  // Skip frames based on everyNthFrame setting
73
73
  if (everyNthFrame > 1 && frameCount % everyNthFrame !== 0) {
@@ -75,19 +75,27 @@ export function createCDPRecorder(page, options) {
75
75
  }
76
76
 
77
77
  const timestamp = metadata.timestamp;
78
- const frameNumber = frameTimestamps.length;
78
+ const frameNumber = nextFrameNumber;
79
+ nextFrameNumber += 1;
79
80
  const paddedNumber = String(frameNumber).padStart(8, "0");
80
81
  const extension = format === "png" ? "png" : "jpg";
81
82
  const framePath = path.join(framesDir, `frame_${paddedNumber}.${extension}`);
82
83
 
83
- // Decode base64 and save frame
84
+ // Decode base64 frame data
84
85
  const buffer = Buffer.from(data, "base64");
85
- await fs.writeFile(framePath, buffer);
86
86
 
87
- frameTimestamps.push({
88
- frameNumber,
89
- timestamp,
90
- path: framePath,
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
+ }
91
99
  });
92
100
  };
93
101
 
@@ -145,6 +153,9 @@ export function createCDPRecorder(page, options) {
145
153
  // Session may already be closed
146
154
  }
147
155
 
156
+ // Wait for all pending frame writes to complete
157
+ await writeQueue;
158
+
148
159
  console.log(
149
160
  `[cdp-recorder] Captured ${frameTimestamps.length} frames in ${(duration / 1000).toFixed(2)}s`
150
161
  );
@@ -186,11 +197,27 @@ export function createCDPRecorder(page, options) {
186
197
 
187
198
  console.log(`[cdp-recorder] Assembling video at ${fps} fps`);
188
199
 
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 });
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
+ }
194
221
 
195
222
  await runFFmpeg([
196
223
  "-y",
@@ -200,7 +227,7 @@ export function createCDPRecorder(page, options) {
200
227
  framePattern,
201
228
  ...encodingArgs,
202
229
  "-pix_fmt",
203
- "yuv420p", // Ensure compatibility
230
+ isWebM ? "yuv420p" : "yuv420p",
204
231
  outputPath,
205
232
  ]);
206
233
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@t3lnet/sceneforge",
3
- "version": "1.0.24",
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",