@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
|
|
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
|
-
|
|
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
|
-
|
|
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 ${(
|
|
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
|
-
//
|
|
187
|
-
|
|
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:
|
|
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
|