fix screen recording: resolve relative paths, buffer chunks, wait for encoder
This commit is contained in:
@@ -27,6 +27,44 @@ let tabGroupQueue: Promise<void> = Promise.resolve()
|
|||||||
// This ensures Playwright can build the iframe frame tree when connecting over CDP.
|
// This ensures Playwright can build the iframe frame tree when connecting over CDP.
|
||||||
let autoAttachParams: Protocol.Target.SetAutoAttachRequest | null = null
|
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 {
|
class ConnectionManager {
|
||||||
ws: WebSocket | null = null
|
ws: WebSocket | null = null
|
||||||
private connectionPromise: Promise<void> | null = null
|
private connectionPromise: Promise<void> | null = null
|
||||||
@@ -106,6 +144,10 @@ class ConnectionManager {
|
|||||||
settled = true
|
settled = true
|
||||||
logger.debug('WebSocket connected')
|
logger.debug('WebSocket connected')
|
||||||
clearTimeout(timeout)
|
clearTimeout(timeout)
|
||||||
|
|
||||||
|
// Flush any buffered recording chunks now that WebSocket is ready
|
||||||
|
flushRecordingChunkBuffer(socket)
|
||||||
|
|
||||||
resolve()
|
resolve()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1486,6 +1528,11 @@ chrome.runtime.onMessage.addListener((message, _sender, _sendResponse) => {
|
|||||||
const buffer = new Uint8Array(data)
|
const buffer = new Uint8Array(data)
|
||||||
connectionManager.ws.send(buffer)
|
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
|
return false // Sync response, no need to keep channel open
|
||||||
|
|||||||
@@ -154,8 +154,22 @@ async function handleStartRecording(params: OffscreenStartRecordingMessage): Pro
|
|||||||
console.log(`MediaRecorder stopped for tab ${tabId}`)
|
console.log(`MediaRecorder stopped for tab ${tabId}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start with 1 second chunks
|
// Wait for MediaRecorder to actually start before returning.
|
||||||
recorder.start(1000)
|
// 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' }
|
return { success: true, tabId, startedAt, mimeType: 'video/mp4' }
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import os from 'node:os'
|
import os from 'node:os'
|
||||||
|
import path from 'node:path'
|
||||||
import type { Page } from 'playwright-core'
|
import type { Page } from 'playwright-core'
|
||||||
import type {
|
import type {
|
||||||
StartRecordingResult,
|
StartRecordingResult,
|
||||||
@@ -96,10 +97,14 @@ export async function startRecording(options: StartRecordingOptions): Promise<Re
|
|||||||
relayPort = 19988,
|
relayPort = 19988,
|
||||||
} = options
|
} = 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`, {
|
const response = await fetch(`http://127.0.0.1:${relayPort}/recording/start`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
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
|
const result = await response.json() as StartRecordingResult
|
||||||
|
|||||||
Reference in New Issue
Block a user