From a9a40e11834eb74a24e883f844b60ecfda020418 Mon Sep 17 00:00:00 2001 From: "Tommy D. Rossi" Date: Thu, 29 Jan 2026 23:30:01 +0100 Subject: [PATCH] fix screen recording: resolve relative paths, buffer chunks, wait for encoder --- extension/src/background.ts | 47 ++++++++++++++++++++++++++++++ extension/src/offscreen.ts | 18 ++++++++++-- playwriter/src/screen-recording.ts | 7 ++++- 3 files changed, 69 insertions(+), 3 deletions(-) diff --git a/extension/src/background.ts b/extension/src/background.ts index bd96057..0f632e8 100644 --- a/extension/src/background.ts +++ b/extension/src/background.ts @@ -27,6 +27,44 @@ let tabGroupQueue: Promise = Promise.resolve() // This ensures Playwright can build the iframe frame tree when connecting over CDP. let autoAttachParams: Protocol.Target.SetAutoAttachRequest | null = null +// Buffer for recording chunks when WebSocket isn't ready. +// Chunks are keyed by tabId and flushed when WebSocket opens. +interface BufferedChunk { + tabId: number + data?: number[] + final?: boolean +} +const recordingChunkBuffer: BufferedChunk[] = [] + +/** + * Flush buffered recording chunks to the WebSocket. + * Called when WebSocket becomes ready. + */ +function flushRecordingChunkBuffer(ws: WebSocket): void { + if (recordingChunkBuffer.length === 0) { + return + } + + logger.debug(`Flushing ${recordingChunkBuffer.length} buffered recording chunks`) + + while (recordingChunkBuffer.length > 0) { + const chunk = recordingChunkBuffer.shift()! + const { tabId, data, final } = chunk + + // Send metadata message first + ws.send(JSON.stringify({ + method: 'recordingData', + params: { tabId, final }, + })) + + // Then send binary data if not final + if (data && !final) { + const buffer = new Uint8Array(data) + ws.send(buffer) + } + } +} + class ConnectionManager { ws: WebSocket | null = null private connectionPromise: Promise | null = null @@ -106,6 +144,10 @@ class ConnectionManager { settled = true logger.debug('WebSocket connected') clearTimeout(timeout) + + // Flush any buffered recording chunks now that WebSocket is ready + flushRecordingChunkBuffer(socket) + resolve() } @@ -1486,6 +1528,11 @@ chrome.runtime.onMessage.addListener((message, _sender, _sendResponse) => { const buffer = new Uint8Array(data) connectionManager.ws.send(buffer) } + } else { + // Buffer chunks when WebSocket isn't ready - they'll be flushed when it opens. + // This prevents data loss during brief disconnections or slow WebSocket startup. + logger.debug(`Buffering recording chunk for tab ${tabId} (WebSocket not ready)`) + recordingChunkBuffer.push({ tabId, data, final }) } return false // Sync response, no need to keep channel open diff --git a/extension/src/offscreen.ts b/extension/src/offscreen.ts index 59a5226..243e195 100644 --- a/extension/src/offscreen.ts +++ b/extension/src/offscreen.ts @@ -154,8 +154,22 @@ async function handleStartRecording(params: OffscreenStartRecordingMessage): Pro console.log(`MediaRecorder stopped for tab ${tabId}`) } - // Start with 1 second chunks - recorder.start(1000) + // Wait for MediaRecorder to actually start before returning. + // This ensures the encoder is initialized and ready to capture frames. + await new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + reject(new Error('MediaRecorder failed to start within 5 seconds')) + }, 5000) + + recorder.onstart = () => { + clearTimeout(timeout) + console.log(`MediaRecorder started for tab ${tabId}`) + resolve() + } + + // Start with 1 second chunks + recorder.start(1000) + }) return { success: true, tabId, startedAt, mimeType: 'video/mp4' } } catch (error: any) { diff --git a/playwriter/src/screen-recording.ts b/playwriter/src/screen-recording.ts index 3d172f2..8ebe313 100644 --- a/playwriter/src/screen-recording.ts +++ b/playwriter/src/screen-recording.ts @@ -7,6 +7,7 @@ */ import os from 'node:os' +import path from 'node:path' import type { Page } from 'playwright-core' import type { StartRecordingResult, @@ -96,10 +97,14 @@ export async function startRecording(options: StartRecordingOptions): Promise