@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.
- package/cli/commands/record-demo.js +39 -23
- package/cli/utils/cdp-recorder.js +46 -19
- package/package.json +1 -1
|
@@ -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
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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
|
|
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
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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
|
|
84
|
+
// Decode base64 frame data
|
|
84
85
|
const buffer = Buffer.from(data, "base64");
|
|
85
|
-
await fs.writeFile(framePath, buffer);
|
|
86
86
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
|
190
|
-
const framePattern = path.join(framesDir, `frame_%08d.${
|
|
191
|
-
|
|
192
|
-
//
|
|
193
|
-
|
|
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"
|
|
230
|
+
isWebM ? "yuv420p" : "yuv420p",
|
|
204
231
|
outputPath,
|
|
205
232
|
]);
|
|
206
233
|
|