fix(web-test): sync video timeline with wall-clock during static pauses

CDP screencast doesn't send frames for static pages, causing video to be
shorter than real time (gap-fill capped at 2s, smart pauses are 4-6s).

- Add _flushFrames() helper on recorder to write duplicate frames on demand
- Call _flushFrames() every 1s during smart TTS pauses in showCaption
- Call _flushFrames() in wait() for long explicit pauses during recording
- Call _flushFrames() in stopRecording for final gap before closing ffmpeg
- Increase gap-fill cap from fps*2 to fps*30 as safety net

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Nick Shirokov
2026-03-12 13:22:16 +03:00
parent f93a1560a5
commit 8f59d3bc66
+46 -5
View File
@@ -2963,7 +2963,21 @@ export async function wait(seconds) {
ms = Math.max(0, ms - credit);
recorder.captionCredit = null;
}
if (ms > 0) await page.waitForTimeout(ms);
if (ms > 0) {
// During recording, split long waits into chunks and flush frames
// to keep video timeline in sync (CDP may not send frames for static pages)
if (recorder?._flushFrames && ms > 1000) {
let remaining = ms;
while (remaining > 0) {
const chunk = Math.min(remaining, 1000);
await page.waitForTimeout(chunk);
remaining -= chunk;
recorder._flushFrames();
}
} else {
await page.waitForTimeout(ms);
}
}
return await getFormState();
}
@@ -3039,7 +3053,7 @@ export async function startRecording(outputPath, opts = {}) {
// Fill the gap with duplicates of the previous frame
const gap = now - lastFrameTime;
const dupes = Math.round(gap / frameDuration) - 1;
for (let i = 0; i < dupes && i < fps * 2; i++) {
for (let i = 0; i < dupes && i < fps * 30; i++) {
ffmpeg.stdin.write(lastFrameBuf);
framesWritten++;
}
@@ -3062,7 +3076,23 @@ export async function startRecording(outputPath, opts = {}) {
everyNthFrame: 1
});
recorder = { cdp, ffmpeg, startTime: Date.now(), outputPath: resolvedPath, ffmpegError: '', captions: [], videoTimeMs: 0 };
// Expose a frame-writing helper on the recorder object.
// During static periods (e.g. smart TTS pauses), CDP may not send screencast
// frames. Call _flushFrames() to fill the gap with duplicates of the last frame,
// keeping video timeline in sync with wall-clock time.
const _flushFrames = () => {
if (!lastFrameBuf || !lastFrameTime || ffmpeg.stdin.destroyed) return;
const now = Date.now();
const gap = now - lastFrameTime;
const dupes = Math.round(gap / frameDuration);
for (let i = 0; i < dupes; i++) {
ffmpeg.stdin.write(lastFrameBuf);
if (recorder) recorder.videoTimeMs += frameDuration;
}
if (dupes > 0) lastFrameTime = now;
};
recorder = { cdp, ffmpeg, startTime: Date.now(), outputPath: resolvedPath, ffmpegError: '', captions: [], videoTimeMs: 0, _flushFrames };
// Redirect stderr accumulation to the recorder object
ffmpeg.stderr.removeAllListeners('data');
ffmpeg.stderr.on('data', d => { recorder.ffmpegError += d.toString(); });
@@ -3077,6 +3107,9 @@ export async function stopRecording() {
const { cdp, ffmpeg, startTime, outputPath } = recorder;
// Final frame flush: write remaining frames to cover the gap since the last screencast frame
if (recorder._flushFrames) recorder._flushFrames();
// Stop CDP screencast
try { await cdp.send('Page.stopScreencast'); } catch {}
try { await cdp.detach(); } catch {}
@@ -3172,9 +3205,17 @@ export async function showCaption(text, opts = {}) {
el.textContent = text;
}, { text, position, fontSize, bg, color });
// Smart TTS wait: pause for estimated speech duration so video has enough screen time
// Smart TTS wait: pause for estimated speech duration so video has enough screen time.
// Split into chunks and flush frames periodically — CDP doesn't send screencast frames
// for static pages, so we must write duplicate frames to keep video timeline in sync.
if (smartWaitMs > 0) {
await page.waitForTimeout(smartWaitMs);
let remaining = smartWaitMs;
while (remaining > 0) {
const chunk = Math.min(remaining, 1000);
await page.waitForTimeout(chunk);
remaining -= chunk;
if (recorder?._flushFrames) recorder._flushFrames();
}
recorder.captionCredit = { waitedMs: smartWaitMs, at: Date.now() };
}
}