refactor: remove dependency injection, use direct imports
- Export store, connectionManager, logger, sendMessage, getTabBySessionId from background.ts - recording.ts now imports directly instead of using initRecording() - Simplify RecordingRelay constructor to take params directly instead of interface
This commit is contained in:
@@ -5,7 +5,6 @@ import type { ExtensionState, ConnectionState, TabState, TabInfo } from './types
|
|||||||
import type { CDPEvent, Protocol } from 'playwriter/src/cdp-types'
|
import type { CDPEvent, Protocol } from 'playwriter/src/cdp-types'
|
||||||
import type { ExtensionCommandMessage, ExtensionResponseMessage } from 'playwriter/src/protocol'
|
import type { ExtensionCommandMessage, ExtensionResponseMessage } from 'playwriter/src/protocol'
|
||||||
import {
|
import {
|
||||||
initRecording,
|
|
||||||
getActiveRecordings,
|
getActiveRecordings,
|
||||||
handleStartRecording,
|
handleStartRecording,
|
||||||
handleStopRecording,
|
handleStopRecording,
|
||||||
@@ -412,9 +411,9 @@ class ConnectionManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const connectionManager = new ConnectionManager()
|
export const connectionManager = new ConnectionManager()
|
||||||
|
|
||||||
const store = createStore<ExtensionState>(() => ({
|
export const store = createStore<ExtensionState>(() => ({
|
||||||
tabs: new Map(),
|
tabs: new Map(),
|
||||||
connectionState: 'idle',
|
connectionState: 'idle',
|
||||||
currentTabId: undefined,
|
currentTabId: undefined,
|
||||||
@@ -477,7 +476,7 @@ function sendLog(level: string, args: any[]) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const logger = {
|
export const logger = {
|
||||||
log: (...args: any[]) => {
|
log: (...args: any[]) => {
|
||||||
console.log(...args)
|
console.log(...args)
|
||||||
sendLog('log', args)
|
sendLog('log', args)
|
||||||
@@ -518,7 +517,7 @@ self.addEventListener('unhandledrejection', (event) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
let messageCount = 0
|
let messageCount = 0
|
||||||
function sendMessage(message: any): void {
|
export function sendMessage(message: any): void {
|
||||||
if (connectionManager.ws?.readyState === WebSocket.OPEN) {
|
if (connectionManager.ws?.readyState === WebSocket.OPEN) {
|
||||||
try {
|
try {
|
||||||
connectionManager.ws.send(JSON.stringify(message))
|
connectionManager.ws.send(JSON.stringify(message))
|
||||||
@@ -602,7 +601,7 @@ async function syncTabGroup(): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTabBySessionId(sessionId: string): { tabId: number; tab: TabInfo } | undefined {
|
export function getTabBySessionId(sessionId: string): { tabId: number; tab: TabInfo } | undefined {
|
||||||
for (const [tabId, tab] of store.getState().tabs) {
|
for (const [tabId, tab] of store.getState().tabs) {
|
||||||
if (tab.sessionId === sessionId) {
|
if (tab.sessionId === sessionId) {
|
||||||
return { tabId, tab }
|
return { tabId, tab }
|
||||||
@@ -620,6 +619,7 @@ function getTabByTargetId(targetId: string): { tabId: number; tab: TabInfo } | u
|
|||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
<<<<<<< HEAD
|
||||||
function emitChildDetachesForTab(tabId: number): void {
|
function emitChildDetachesForTab(tabId: number): void {
|
||||||
const childEntries = Array.from(childSessions.entries())
|
const childEntries = Array.from(childSessions.entries())
|
||||||
.filter(([_, parentTab]) => parentTab.tabId === tabId)
|
.filter(([_, parentTab]) => parentTab.tabId === tabId)
|
||||||
@@ -640,25 +640,6 @@ function emitChildDetachesForTab(tabId: number): void {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize recording module with required dependencies
|
|
||||||
initRecording({
|
|
||||||
getTabBySessionId,
|
|
||||||
getTabs: () => store.getState().tabs,
|
|
||||||
updateTabRecordingState: (tabId: number, isRecording: boolean) => {
|
|
||||||
store.setState((state) => {
|
|
||||||
const newTabs = new Map(state.tabs)
|
|
||||||
const existing = newTabs.get(tabId)
|
|
||||||
if (existing) {
|
|
||||||
newTabs.set(tabId, { ...existing, isRecording })
|
|
||||||
}
|
|
||||||
return { tabs: newTabs }
|
|
||||||
})
|
|
||||||
},
|
|
||||||
sendMessage,
|
|
||||||
isWebSocketOpen: () => connectionManager.ws?.readyState === WebSocket.OPEN,
|
|
||||||
logger,
|
|
||||||
})
|
|
||||||
|
|
||||||
async function handleCommand(msg: ExtensionCommandMessage): Promise<any> {
|
async function handleCommand(msg: ExtensionCommandMessage): Promise<any> {
|
||||||
if (msg.method !== 'forwardCDPCommand') return
|
if (msg.method !== 'forwardCDPCommand') return
|
||||||
|
|
||||||
|
|||||||
+32
-68
@@ -3,7 +3,7 @@
|
|||||||
* Uses chrome.tabCapture to record tabs via an offscreen document.
|
* Uses chrome.tabCapture to record tabs via an offscreen document.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { RecordingInfo, TabInfo } from './types'
|
import type { RecordingInfo } from './types'
|
||||||
import type {
|
import type {
|
||||||
StartRecordingParams,
|
StartRecordingParams,
|
||||||
StopRecordingParams,
|
StopRecordingParams,
|
||||||
@@ -19,37 +19,14 @@ import type {
|
|||||||
OffscreenStopRecordingResult,
|
OffscreenStopRecordingResult,
|
||||||
OffscreenIsRecordingResult,
|
OffscreenIsRecordingResult,
|
||||||
} from './offscreen-types'
|
} from './offscreen-types'
|
||||||
|
import { store, connectionManager, logger, sendMessage, getTabBySessionId } from './background'
|
||||||
// Dependencies injected from background.ts
|
|
||||||
interface RecordingDeps {
|
|
||||||
getTabBySessionId: (sessionId: string) => { tabId: number; tab: TabInfo } | undefined
|
|
||||||
getTabs: () => Map<number, TabInfo>
|
|
||||||
updateTabRecordingState: (tabId: number, isRecording: boolean) => void
|
|
||||||
sendMessage: (message: unknown) => void
|
|
||||||
isWebSocketOpen: () => boolean
|
|
||||||
logger: {
|
|
||||||
debug: (...args: unknown[]) => void
|
|
||||||
error: (...args: unknown[]) => void
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Active recordings - kept outside store since MediaRecorder/MediaStream can't be serialized
|
// Active recordings - kept outside store since MediaRecorder/MediaStream can't be serialized
|
||||||
const activeRecordings: Map<number, RecordingInfo> = new Map()
|
const activeRecordings: Map<number, RecordingInfo> = new Map()
|
||||||
|
|
||||||
// Module-level dependencies (set via initRecording)
|
|
||||||
let deps: RecordingDeps | null = null
|
|
||||||
|
|
||||||
// Offscreen document management
|
// Offscreen document management
|
||||||
let offscreenDocumentCreating: Promise<void> | null = null
|
let offscreenDocumentCreating: Promise<void> | null = null
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize the recording module with dependencies from background.ts.
|
|
||||||
* Must be called before using any recording functions.
|
|
||||||
*/
|
|
||||||
export function initRecording(dependencies: RecordingDeps): void {
|
|
||||||
deps = dependencies
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the active recordings map (for cleanup on tab disconnect).
|
* Get the active recordings map (for cleanup on tab disconnect).
|
||||||
*/
|
*/
|
||||||
@@ -87,13 +64,9 @@ async function ensureOffscreenDocument(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function resolveTabIdFromSessionId(sessionId?: string): number | undefined {
|
function resolveTabIdFromSessionId(sessionId?: string): number | undefined {
|
||||||
if (!deps) {
|
|
||||||
throw new Error('Recording module not initialized')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!sessionId) {
|
if (!sessionId) {
|
||||||
// Return the first connected tab
|
// Return the first connected tab
|
||||||
for (const [tabId, tab] of deps.getTabs()) {
|
for (const [tabId, tab] of store.getState().tabs) {
|
||||||
if (tab.state === 'connected') {
|
if (tab.state === 'connected') {
|
||||||
return tabId
|
return tabId
|
||||||
}
|
}
|
||||||
@@ -101,15 +74,22 @@ function resolveTabIdFromSessionId(sessionId?: string): number | undefined {
|
|||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
const found = deps.getTabBySessionId(sessionId)
|
const found = getTabBySessionId(sessionId)
|
||||||
return found?.tabId
|
return found?.tabId
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function handleStartRecording(params: StartRecordingParams): Promise<StartRecordingResult> {
|
function updateTabRecordingState(tabId: number, isRecording: boolean): void {
|
||||||
if (!deps) {
|
store.setState((state) => {
|
||||||
throw new Error('Recording module not initialized')
|
const newTabs = new Map(state.tabs)
|
||||||
}
|
const existing = newTabs.get(tabId)
|
||||||
|
if (existing) {
|
||||||
|
newTabs.set(tabId, { ...existing, isRecording })
|
||||||
|
}
|
||||||
|
return { tabs: newTabs }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function handleStartRecording(params: StartRecordingParams): Promise<StartRecordingResult> {
|
||||||
const tabId = resolveTabIdFromSessionId(params.sessionId)
|
const tabId = resolveTabIdFromSessionId(params.sessionId)
|
||||||
if (!tabId) {
|
if (!tabId) {
|
||||||
return { success: false, error: 'No connected tab found for recording. Click the Playwriter extension icon on the tab you want to record.' }
|
return { success: false, error: 'No connected tab found for recording. Click the Playwriter extension icon on the tab you want to record.' }
|
||||||
@@ -119,12 +99,12 @@ export async function handleStartRecording(params: StartRecordingParams): Promis
|
|||||||
return { success: false, error: 'Recording already in progress for this tab' }
|
return { success: false, error: 'Recording already in progress for this tab' }
|
||||||
}
|
}
|
||||||
|
|
||||||
const tabInfo = deps.getTabs().get(tabId)
|
const tabInfo = store.getState().tabs.get(tabId)
|
||||||
if (!tabInfo || tabInfo.state !== 'connected') {
|
if (!tabInfo || tabInfo.state !== 'connected') {
|
||||||
return { success: false, error: 'Tab is not connected' }
|
return { success: false, error: 'Tab is not connected' }
|
||||||
}
|
}
|
||||||
|
|
||||||
deps.logger.debug('Starting recording for tab:', tabId, 'params:', params)
|
logger.debug('Starting recording for tab:', tabId, 'params:', params)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Ensure offscreen document exists
|
// Ensure offscreen document exists
|
||||||
@@ -150,7 +130,7 @@ export async function handleStartRecording(params: StartRecordingParams): Promis
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
deps.logger.debug('Got stream ID for tab:', tabId, 'streamId:', streamId.substring(0, 20) + '...')
|
logger.debug('Got stream ID for tab:', tabId, 'streamId:', streamId.substring(0, 20) + '...')
|
||||||
|
|
||||||
// Send message to offscreen document to start recording
|
// Send message to offscreen document to start recording
|
||||||
const result = await chrome.runtime.sendMessage({
|
const result = await chrome.runtime.sendMessage({
|
||||||
@@ -173,22 +153,18 @@ export async function handleStartRecording(params: StartRecordingParams): Promis
|
|||||||
activeRecordings.set(tabId, { tabId, startedAt })
|
activeRecordings.set(tabId, { tabId, startedAt })
|
||||||
|
|
||||||
// Update tab state
|
// Update tab state
|
||||||
deps.updateTabRecordingState(tabId, true)
|
updateTabRecordingState(tabId, true)
|
||||||
|
|
||||||
deps.logger.debug('Recording started for tab:', tabId, 'mimeType:', result.mimeType)
|
logger.debug('Recording started for tab:', tabId, 'mimeType:', result.mimeType)
|
||||||
return { success: true, tabId, startedAt }
|
return { success: true, tabId, startedAt }
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||||
deps.logger.error('Failed to start recording:', error)
|
logger.error('Failed to start recording:', error)
|
||||||
return { success: false, error: errorMessage }
|
return { success: false, error: errorMessage }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function handleStopRecording(params: StopRecordingParams): Promise<ExtensionStopRecordingResult> {
|
export async function handleStopRecording(params: StopRecordingParams): Promise<ExtensionStopRecordingResult> {
|
||||||
if (!deps) {
|
|
||||||
throw new Error('Recording module not initialized')
|
|
||||||
}
|
|
||||||
|
|
||||||
const tabId = resolveTabIdFromSessionId(params.sessionId)
|
const tabId = resolveTabIdFromSessionId(params.sessionId)
|
||||||
if (!tabId) {
|
if (!tabId) {
|
||||||
return { success: false, error: 'No connected tab found' }
|
return { success: false, error: 'No connected tab found' }
|
||||||
@@ -199,7 +175,7 @@ export async function handleStopRecording(params: StopRecordingParams): Promise<
|
|||||||
return { success: false, error: 'No active recording for this tab' }
|
return { success: false, error: 'No active recording for this tab' }
|
||||||
}
|
}
|
||||||
|
|
||||||
deps.logger.debug('Stopping recording for tab:', tabId)
|
logger.debug('Stopping recording for tab:', tabId)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Send message to offscreen document to stop recording - include tabId for concurrent support
|
// Send message to offscreen document to stop recording - include tabId for concurrent support
|
||||||
@@ -216,22 +192,18 @@ export async function handleStopRecording(params: StopRecordingParams): Promise<
|
|||||||
|
|
||||||
// Clean up
|
// Clean up
|
||||||
activeRecordings.delete(tabId)
|
activeRecordings.delete(tabId)
|
||||||
deps.updateTabRecordingState(tabId, false)
|
updateTabRecordingState(tabId, false)
|
||||||
|
|
||||||
deps.logger.debug('Recording stopped for tab:', tabId, 'duration:', duration)
|
logger.debug('Recording stopped for tab:', tabId, 'duration:', duration)
|
||||||
return { success: true, tabId, duration }
|
return { success: true, tabId, duration }
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||||
deps.logger.error('Failed to stop recording:', error)
|
logger.error('Failed to stop recording:', error)
|
||||||
return { success: false, error: errorMessage }
|
return { success: false, error: errorMessage }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function handleIsRecording(params: IsRecordingParams): Promise<IsRecordingResult> {
|
export async function handleIsRecording(params: IsRecordingParams): Promise<IsRecordingResult> {
|
||||||
if (!deps) {
|
|
||||||
throw new Error('Recording module not initialized')
|
|
||||||
}
|
|
||||||
|
|
||||||
const tabId = resolveTabIdFromSessionId(params.sessionId)
|
const tabId = resolveTabIdFromSessionId(params.sessionId)
|
||||||
if (!tabId) {
|
if (!tabId) {
|
||||||
return { isRecording: false }
|
return { isRecording: false }
|
||||||
@@ -261,10 +233,6 @@ export async function handleIsRecording(params: IsRecordingParams): Promise<IsRe
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function handleCancelRecording(params: CancelRecordingParams): Promise<CancelRecordingResult> {
|
export async function handleCancelRecording(params: CancelRecordingParams): Promise<CancelRecordingResult> {
|
||||||
if (!deps) {
|
|
||||||
throw new Error('Recording module not initialized')
|
|
||||||
}
|
|
||||||
|
|
||||||
const tabId = resolveTabIdFromSessionId(params.sessionId)
|
const tabId = resolveTabIdFromSessionId(params.sessionId)
|
||||||
if (!tabId) {
|
if (!tabId) {
|
||||||
return { success: false, error: 'No connected tab found' }
|
return { success: false, error: 'No connected tab found' }
|
||||||
@@ -275,7 +243,7 @@ export async function handleCancelRecording(params: CancelRecordingParams): Prom
|
|||||||
return { success: true } // Already not recording
|
return { success: true } // Already not recording
|
||||||
}
|
}
|
||||||
|
|
||||||
deps.logger.debug('Cancelling recording for tab:', tabId)
|
logger.debug('Cancelling recording for tab:', tabId)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Send message to offscreen document to cancel recording - include tabId for concurrent support
|
// Send message to offscreen document to cancel recording - include tabId for concurrent support
|
||||||
@@ -285,11 +253,11 @@ export async function handleCancelRecording(params: CancelRecordingParams): Prom
|
|||||||
})
|
})
|
||||||
|
|
||||||
activeRecordings.delete(tabId)
|
activeRecordings.delete(tabId)
|
||||||
deps.updateTabRecordingState(tabId, false)
|
updateTabRecordingState(tabId, false)
|
||||||
|
|
||||||
// Send cancel marker
|
// Send cancel marker
|
||||||
if (deps.isWebSocketOpen()) {
|
if (connectionManager.ws?.readyState === WebSocket.OPEN) {
|
||||||
deps.sendMessage({
|
sendMessage({
|
||||||
method: 'recordingCancelled',
|
method: 'recordingCancelled',
|
||||||
params: { tabId },
|
params: { tabId },
|
||||||
})
|
})
|
||||||
@@ -298,7 +266,7 @@ export async function handleCancelRecording(params: CancelRecordingParams): Prom
|
|||||||
return { success: true }
|
return { success: true }
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||||
deps.logger.error('Failed to cancel recording:', error)
|
logger.error('Failed to cancel recording:', error)
|
||||||
return { success: false, error: errorMessage }
|
return { success: false, error: errorMessage }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -307,18 +275,14 @@ export async function handleCancelRecording(params: CancelRecordingParams): Prom
|
|||||||
* Clean up recordings when tab is disconnected.
|
* Clean up recordings when tab is disconnected.
|
||||||
*/
|
*/
|
||||||
export async function cleanupRecordingForTab(tabId: number): Promise<void> {
|
export async function cleanupRecordingForTab(tabId: number): Promise<void> {
|
||||||
if (!deps) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const recording = activeRecordings.get(tabId)
|
const recording = activeRecordings.get(tabId)
|
||||||
if (recording) {
|
if (recording) {
|
||||||
deps.logger.debug('Cleaning up recording for disconnected tab:', tabId)
|
logger.debug('Cleaning up recording for disconnected tab:', tabId)
|
||||||
try {
|
try {
|
||||||
// Tell offscreen document to cancel recording - include tabId for concurrent support
|
// Tell offscreen document to cancel recording - include tabId for concurrent support
|
||||||
await chrome.runtime.sendMessage({ action: 'cancelRecording', tabId })
|
await chrome.runtime.sendMessage({ action: 'cancelRecording', tabId })
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
deps.logger.debug('Error cleaning up recording:', e)
|
logger.debug('Error cleaning up recording:', e)
|
||||||
}
|
}
|
||||||
activeRecordings.delete(tabId)
|
activeRecordings.delete(tabId)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -264,11 +264,11 @@ export async function startPlayWriterCDPRelayServer({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Recording relay for screen recording functionality
|
// Recording relay for screen recording functionality
|
||||||
const recordingRelay = new RecordingRelay({
|
const recordingRelay = new RecordingRelay(
|
||||||
sendToExtension,
|
sendToExtension,
|
||||||
isExtensionConnected: () => extensionWs !== null,
|
() => extensionWs !== null,
|
||||||
logger,
|
logger,
|
||||||
})
|
)
|
||||||
|
|
||||||
// Auto-create initial tab when PLAYWRITER_AUTO_ENABLE is set and no targets exist.
|
// Auto-create initial tab when PLAYWRITER_AUTO_ENABLE is set and no targets exist.
|
||||||
// This allows Playwright to connect and immediately have a page to work with.
|
// This allows Playwright to connect and immediately have a page to work with.
|
||||||
|
|||||||
@@ -29,30 +29,28 @@ export interface ActiveRecording {
|
|||||||
resolveStop?: (result: StopRecordingResult) => void
|
resolveStop?: (result: StopRecordingResult) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RecordingRelayDeps {
|
|
||||||
sendToExtension: (params: { method: string; params?: unknown; timeout?: number }) => Promise<unknown>
|
|
||||||
isExtensionConnected: () => boolean
|
|
||||||
logger?: {
|
|
||||||
log(...args: unknown[]): void
|
|
||||||
error(...args: unknown[]): void
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class RecordingRelay {
|
export class RecordingRelay {
|
||||||
private activeRecordings = new Map<number, ActiveRecording>()
|
private activeRecordings = new Map<number, ActiveRecording>()
|
||||||
// Track which tabId just sent recordingData metadata - used to route the next binary chunk
|
// Track which tabId just sent recordingData metadata - used to route the next binary chunk
|
||||||
private lastRecordingMetadataTabId: number | null = null
|
private lastRecordingMetadataTabId: number | null = null
|
||||||
private deps: RecordingRelayDeps
|
private sendToExtension: (params: { method: string; params?: unknown; timeout?: number }) => Promise<unknown>
|
||||||
|
private isExtensionConnected: () => boolean
|
||||||
|
private logger?: { log(...args: unknown[]): void; error(...args: unknown[]): void }
|
||||||
|
|
||||||
constructor(deps: RecordingRelayDeps) {
|
constructor(
|
||||||
this.deps = deps
|
sendToExtension: (params: { method: string; params?: unknown; timeout?: number }) => Promise<unknown>,
|
||||||
|
isExtensionConnected: () => boolean,
|
||||||
|
logger?: { log(...args: unknown[]): void; error(...args: unknown[]): void }
|
||||||
|
) {
|
||||||
|
this.sendToExtension = sendToExtension
|
||||||
|
this.isExtensionConnected = isExtensionConnected
|
||||||
|
this.logger = logger
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle incoming binary data (recording chunks) from the extension.
|
* Handle incoming binary data (recording chunks) from the extension.
|
||||||
* Returns true if the data was handled as a recording chunk.
|
|
||||||
*/
|
*/
|
||||||
handleBinaryData(buffer: Buffer): boolean {
|
handleBinaryData(buffer: Buffer): void {
|
||||||
const tabId = this.lastRecordingMetadataTabId
|
const tabId = this.lastRecordingMetadataTabId
|
||||||
this.lastRecordingMetadataTabId = null
|
this.lastRecordingMetadataTabId = null
|
||||||
|
|
||||||
@@ -60,41 +58,35 @@ export class RecordingRelay {
|
|||||||
const recording = this.activeRecordings.get(tabId)
|
const recording = this.activeRecordings.get(tabId)
|
||||||
if (recording) {
|
if (recording) {
|
||||||
recording.chunks.push(buffer)
|
recording.chunks.push(buffer)
|
||||||
this.deps.logger?.log(pc.blue(`Received recording chunk for tab ${tabId}: ${buffer.length} bytes (total chunks: ${recording.chunks.length})`))
|
this.logger?.log(pc.blue(`Received recording chunk for tab ${tabId}: ${buffer.length} bytes (total chunks: ${recording.chunks.length})`))
|
||||||
return true
|
|
||||||
} else {
|
} else {
|
||||||
this.deps.logger?.log(pc.yellow(`Received recording chunk for unknown tab ${tabId}, ignoring`))
|
this.logger?.log(pc.yellow(`Received recording chunk for unknown tab ${tabId}, ignoring`))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.deps.logger?.log(pc.yellow('Received recording chunk without preceding metadata, ignoring'))
|
this.logger?.log(pc.yellow('Received recording chunk without preceding metadata, ignoring'))
|
||||||
}
|
}
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle recordingData message from extension.
|
* Handle recordingData message from extension.
|
||||||
* This is sent before binary chunks to identify which recording they belong to.
|
|
||||||
*/
|
*/
|
||||||
handleRecordingData(message: RecordingDataMessage): void {
|
handleRecordingData(message: RecordingDataMessage): void {
|
||||||
const { tabId, final } = message.params
|
const { tabId, final } = message.params
|
||||||
const recording = this.activeRecordings.get(tabId)
|
const recording = this.activeRecordings.get(tabId)
|
||||||
|
|
||||||
// Track which tab sent this metadata for routing the next binary chunk
|
|
||||||
if (!final) {
|
if (!final) {
|
||||||
this.lastRecordingMetadataTabId = tabId
|
this.lastRecordingMetadataTabId = tabId
|
||||||
}
|
}
|
||||||
|
|
||||||
if (recording && final) {
|
if (recording && final) {
|
||||||
// This is the final marker - write all chunks to file
|
|
||||||
try {
|
try {
|
||||||
const totalSize = recording.chunks.reduce((sum, chunk) => sum + chunk.length, 0)
|
const totalSize = recording.chunks.reduce((sum, chunk) => sum + chunk.length, 0)
|
||||||
const combined = Buffer.concat(recording.chunks)
|
const combined = Buffer.concat(recording.chunks)
|
||||||
fs.writeFileSync(recording.outputPath, combined)
|
fs.writeFileSync(recording.outputPath, combined)
|
||||||
|
|
||||||
const duration = Date.now() - recording.startedAt
|
const duration = Date.now() - recording.startedAt
|
||||||
this.deps.logger?.log(pc.green(`Recording saved: ${recording.outputPath} (${totalSize} bytes, ${duration}ms)`))
|
this.logger?.log(pc.green(`Recording saved: ${recording.outputPath} (${totalSize} bytes, ${duration}ms)`))
|
||||||
|
|
||||||
// Resolve the stop promise
|
|
||||||
if (recording.resolveStop) {
|
if (recording.resolveStop) {
|
||||||
recording.resolveStop({
|
recording.resolveStop({
|
||||||
success: true,
|
success: true,
|
||||||
@@ -106,7 +98,7 @@ export class RecordingRelay {
|
|||||||
}
|
}
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||||
this.deps.logger?.error('Failed to write recording:', error)
|
this.logger?.error('Failed to write recording:', error)
|
||||||
if (recording.resolveStop) {
|
if (recording.resolveStop) {
|
||||||
recording.resolveStop({ success: false, error: errorMessage })
|
recording.resolveStop({ success: false, error: errorMessage })
|
||||||
}
|
}
|
||||||
@@ -114,7 +106,6 @@ export class RecordingRelay {
|
|||||||
|
|
||||||
this.activeRecordings.delete(tabId)
|
this.activeRecordings.delete(tabId)
|
||||||
}
|
}
|
||||||
// Non-final recordingData is just a marker that binary follows - handled in handleBinaryData
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -124,7 +115,7 @@ export class RecordingRelay {
|
|||||||
const { tabId } = message.params
|
const { tabId } = message.params
|
||||||
const recording = this.activeRecordings.get(tabId)
|
const recording = this.activeRecordings.get(tabId)
|
||||||
if (recording) {
|
if (recording) {
|
||||||
this.deps.logger?.log(pc.yellow(`Recording cancelled for tab ${tabId}`))
|
this.logger?.log(pc.yellow(`Recording cancelled for tab ${tabId}`))
|
||||||
if (recording.resolveStop) {
|
if (recording.resolveStop) {
|
||||||
recording.resolveStop({ success: false, error: 'Recording was cancelled' })
|
recording.resolveStop({ success: false, error: 'Recording was cancelled' })
|
||||||
}
|
}
|
||||||
@@ -132,9 +123,6 @@ export class RecordingRelay {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Start recording a tab.
|
|
||||||
*/
|
|
||||||
async startRecording(params: StartRecordingParams & { outputPath: string }): Promise<StartRecordingResult> {
|
async startRecording(params: StartRecordingParams & { outputPath: string }): Promise<StartRecordingResult> {
|
||||||
const { outputPath, ...recordingParams } = params
|
const { outputPath, ...recordingParams } = params
|
||||||
|
|
||||||
@@ -142,18 +130,17 @@ export class RecordingRelay {
|
|||||||
return { success: false, error: 'outputPath is required' }
|
return { success: false, error: 'outputPath is required' }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.deps.isExtensionConnected()) {
|
if (!this.isExtensionConnected()) {
|
||||||
return { success: false, error: 'Extension not connected' }
|
return { success: false, error: 'Extension not connected' }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure output directory exists
|
|
||||||
const dir = path.dirname(outputPath)
|
const dir = path.dirname(outputPath)
|
||||||
if (!fs.existsSync(dir)) {
|
if (!fs.existsSync(dir)) {
|
||||||
fs.mkdirSync(dir, { recursive: true })
|
fs.mkdirSync(dir, { recursive: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await this.deps.sendToExtension({
|
const result = await this.sendToExtension({
|
||||||
method: 'startRecording',
|
method: 'startRecording',
|
||||||
params: recordingParams,
|
params: recordingParams,
|
||||||
timeout: 10000,
|
timeout: 10000,
|
||||||
@@ -164,7 +151,6 @@ export class RecordingRelay {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
// Track this recording - store sessionId for lookup when stopping
|
|
||||||
this.activeRecordings.set(result.tabId, {
|
this.activeRecordings.set(result.tabId, {
|
||||||
tabId: result.tabId,
|
tabId: result.tabId,
|
||||||
sessionId: recordingParams.sessionId,
|
sessionId: recordingParams.sessionId,
|
||||||
@@ -172,27 +158,22 @@ export class RecordingRelay {
|
|||||||
chunks: [],
|
chunks: [],
|
||||||
startedAt: result.startedAt,
|
startedAt: result.startedAt,
|
||||||
})
|
})
|
||||||
this.deps.logger?.log(pc.green(`Recording started for tab ${result.tabId} (sessionId: ${recordingParams.sessionId || 'none'}), output: ${outputPath}`))
|
this.logger?.log(pc.green(`Recording started for tab ${result.tabId} (sessionId: ${recordingParams.sessionId || 'none'}), output: ${outputPath}`))
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||||
this.deps.logger?.error('Start recording error:', error)
|
this.logger?.error('Start recording error:', error)
|
||||||
return { success: false, error: errorMessage }
|
return { success: false, error: errorMessage }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Stop recording and save to file.
|
|
||||||
*/
|
|
||||||
async stopRecording(params: StopRecordingParams): Promise<StopRecordingResult> {
|
async stopRecording(params: StopRecordingParams): Promise<StopRecordingResult> {
|
||||||
if (!this.deps.isExtensionConnected()) {
|
if (!this.isExtensionConnected()) {
|
||||||
return { success: false, error: 'Extension not connected' }
|
return { success: false, error: 'Extension not connected' }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find the active recording by sessionId for concurrent recording support
|
|
||||||
// If no sessionId provided, fall back to the first recording (backward compatibility)
|
|
||||||
const findRecording = (): ActiveRecording | undefined => {
|
const findRecording = (): ActiveRecording | undefined => {
|
||||||
if (params.sessionId) {
|
if (params.sessionId) {
|
||||||
for (const recording of this.activeRecordings.values()) {
|
for (const recording of this.activeRecordings.values()) {
|
||||||
@@ -200,10 +181,8 @@ export class RecordingRelay {
|
|||||||
return recording
|
return recording
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// SessionId provided but no matching recording found
|
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
// No sessionId - return first recording (backward compat for single-recording case)
|
|
||||||
return this.activeRecordings.values().next().value
|
return this.activeRecordings.values().next().value
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -216,7 +195,6 @@ export class RecordingRelay {
|
|||||||
return { success: false, error: errorMsg }
|
return { success: false, error: errorMsg }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set up promise to wait for final chunk
|
|
||||||
let timeoutId: ReturnType<typeof setTimeout>
|
let timeoutId: ReturnType<typeof setTimeout>
|
||||||
const finalPromise = new Promise<StopRecordingResult>((resolve) => {
|
const finalPromise = new Promise<StopRecordingResult>((resolve) => {
|
||||||
const wrappedResolve = (result: StopRecordingResult) => {
|
const wrappedResolve = (result: StopRecordingResult) => {
|
||||||
@@ -224,7 +202,6 @@ export class RecordingRelay {
|
|||||||
resolve(result)
|
resolve(result)
|
||||||
}
|
}
|
||||||
recording.resolveStop = wrappedResolve
|
recording.resolveStop = wrappedResolve
|
||||||
// Timeout after 30 seconds
|
|
||||||
timeoutId = setTimeout(() => {
|
timeoutId = setTimeout(() => {
|
||||||
if (recording.resolveStop) {
|
if (recording.resolveStop) {
|
||||||
recording.resolveStop = undefined
|
recording.resolveStop = undefined
|
||||||
@@ -234,8 +211,7 @@ export class RecordingRelay {
|
|||||||
})
|
})
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Tell extension to stop recording
|
const result = await this.sendToExtension({
|
||||||
const result = await this.deps.sendToExtension({
|
|
||||||
method: 'stopRecording',
|
method: 'stopRecording',
|
||||||
params,
|
params,
|
||||||
timeout: 10000,
|
timeout: 10000,
|
||||||
@@ -247,55 +223,44 @@ export class RecordingRelay {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait for final chunk to arrive
|
|
||||||
return await finalPromise
|
return await finalPromise
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||||
this.deps.logger?.error('Stop recording error:', error)
|
this.logger?.error('Stop recording error:', error)
|
||||||
return { success: false, error: errorMessage }
|
return { success: false, error: errorMessage }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if recording is active.
|
|
||||||
*/
|
|
||||||
async isRecording(params: IsRecordingParams): Promise<IsRecordingResult> {
|
async isRecording(params: IsRecordingParams): Promise<IsRecordingResult> {
|
||||||
if (!this.deps.isExtensionConnected()) {
|
if (!this.isExtensionConnected()) {
|
||||||
return { isRecording: false }
|
return { isRecording: false }
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return await this.deps.sendToExtension({
|
return await this.sendToExtension({
|
||||||
method: 'isRecording',
|
method: 'isRecording',
|
||||||
params,
|
params,
|
||||||
timeout: 5000,
|
timeout: 5000,
|
||||||
}) as IsRecordingResult
|
}) as IsRecordingResult
|
||||||
} catch (error: unknown) {
|
} catch {
|
||||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
|
||||||
this.deps.logger?.error('Recording status error:', error)
|
|
||||||
return { isRecording: false }
|
return { isRecording: false }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Cancel recording without saving.
|
|
||||||
*/
|
|
||||||
async cancelRecording(params: CancelRecordingParams): Promise<CancelRecordingResult> {
|
async cancelRecording(params: CancelRecordingParams): Promise<CancelRecordingResult> {
|
||||||
if (!this.deps.isExtensionConnected()) {
|
if (!this.isExtensionConnected()) {
|
||||||
return { success: false, error: 'Extension not connected' }
|
return { success: false, error: 'Extension not connected' }
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Note: Recording cleanup is handled by the 'recordingCancelled' event handler
|
return await this.sendToExtension({
|
||||||
// which is triggered by the extension after cancellation.
|
|
||||||
return await this.deps.sendToExtension({
|
|
||||||
method: 'cancelRecording',
|
method: 'cancelRecording',
|
||||||
params,
|
params,
|
||||||
timeout: 5000,
|
timeout: 5000,
|
||||||
}) as CancelRecordingResult
|
}) as CancelRecordingResult
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||||
this.deps.logger?.error('Cancel recording error:', error)
|
this.logger?.error('Cancel recording error:', error)
|
||||||
return { success: false, error: errorMessage }
|
return { success: false, error: errorMessage }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user