fix screen recording: resolve relative paths, buffer chunks, wait for encoder

This commit is contained in:
Tommy D. Rossi
2026-01-29 23:30:01 +01:00
parent db54c35b83
commit a9a40e1183
3 changed files with 69 additions and 3 deletions
+47
View File
@@ -27,6 +27,44 @@ let tabGroupQueue: Promise<void> = 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<void> | 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
+16 -2
View File
@@ -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<void>((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) {
+6 -1
View File
@@ -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<Re
relayPort = 19988,
} = options
// Resolve relative paths to absolute using the caller's cwd.
// The relay server may have a different cwd, so we must resolve here.
const absoluteOutputPath = path.resolve(outputPath)
const response = await fetch(`http://127.0.0.1:${relayPort}/recording/start`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ sessionId, frameRate, videoBitsPerSecond, audioBitsPerSecond, audio, outputPath }),
body: JSON.stringify({ sessionId, frameRate, videoBitsPerSecond, audioBitsPerSecond, audio, outputPath: absoluteOutputPath }),
})
const result = await response.json() as StartRecordingResult