@midscene/playground 1.10.4 → 1.10.5-beta-20260714101501.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/dist/es/adapters/remote-execution.mjs +38 -1
  2. package/dist/es/adapters/remote-execution.mjs.map +1 -1
  3. package/dist/es/mjpeg-hub.mjs +7 -6
  4. package/dist/es/mjpeg-hub.mjs.map +1 -1
  5. package/dist/es/mjpeg-stream-handler.mjs +7 -1
  6. package/dist/es/mjpeg-stream-handler.mjs.map +1 -1
  7. package/dist/es/platform.mjs.map +1 -1
  8. package/dist/es/sdk/index.mjs +10 -0
  9. package/dist/es/sdk/index.mjs.map +1 -1
  10. package/dist/es/server.mjs +369 -35
  11. package/dist/es/server.mjs.map +1 -1
  12. package/dist/lib/adapters/remote-execution.js +38 -1
  13. package/dist/lib/adapters/remote-execution.js.map +1 -1
  14. package/dist/lib/mjpeg-hub.js +7 -6
  15. package/dist/lib/mjpeg-hub.js.map +1 -1
  16. package/dist/lib/mjpeg-stream-handler.js +7 -1
  17. package/dist/lib/mjpeg-stream-handler.js.map +1 -1
  18. package/dist/lib/platform.js.map +1 -1
  19. package/dist/lib/sdk/index.js +10 -0
  20. package/dist/lib/sdk/index.js.map +1 -1
  21. package/dist/lib/server.js +367 -33
  22. package/dist/lib/server.js.map +1 -1
  23. package/dist/types/adapters/remote-execution.d.ts +3 -0
  24. package/dist/types/platform.d.ts +1 -0
  25. package/dist/types/sdk/index.d.ts +3 -0
  26. package/dist/types/server.d.ts +18 -1
  27. package/dist/types/types.d.ts +1 -0
  28. package/package.json +3 -3
  29. package/static/index.html +1 -1
  30. package/static/static/css/index.2b6b43ad.css +2 -0
  31. package/static/static/css/index.2b6b43ad.css.map +1 -0
  32. package/static/static/js/{445.884e7c8e.js → 890.03bb43a9.js} +3 -3
  33. package/static/static/js/{445.884e7c8e.js.LICENSE.txt → 890.03bb43a9.js.LICENSE.txt} +0 -2
  34. package/static/static/js/890.03bb43a9.js.map +1 -0
  35. package/static/static/js/index.2a85361c.js +960 -0
  36. package/static/static/js/index.2a85361c.js.map +1 -0
  37. package/static/static/css/index.1293237f.css +0 -2
  38. package/static/static/css/index.1293237f.css.map +0 -1
  39. package/static/static/js/445.884e7c8e.js.map +0 -1
  40. package/static/static/js/index.babddf12.js +0 -960
  41. package/static/static/js/index.babddf12.js.map +0 -1
  42. /package/static/static/js/{index.babddf12.js.LICENSE.txt → index.2a85361c.js.LICENSE.txt} +0 -0
@@ -428,7 +428,7 @@ class RemoteExecutionAdapter extends BasePlaygroundAdapter {
428
428
  nextIndex: since
429
429
  };
430
430
  try {
431
- const response = await fetch(`${this.serverUrl}/recorder/events?since=${encodeURIComponent(String(since))}`);
431
+ const response = await fetch(`${this.serverUrl}/recorder/events?since=${encodeURIComponent(String(since))}&flushPending=false`);
432
432
  if (!response.ok) return {
433
433
  events: [],
434
434
  nextIndex: since
@@ -477,6 +477,43 @@ class RemoteExecutionAdapter extends BasePlaygroundAdapter {
477
477
  };
478
478
  }
479
479
  }
480
+ async getRecorderScreenshotAsset(assetId) {
481
+ if (!this.serverUrl || !assetId) return null;
482
+ try {
483
+ const response = await fetch(`${this.serverUrl}/recorder/assets/${encodeURIComponent(assetId)}`);
484
+ if (!response.ok) return null;
485
+ const blob = await response.blob();
486
+ return await new Promise((resolve, reject)=>{
487
+ const reader = new FileReader();
488
+ reader.onload = ()=>resolve('string' == typeof reader.result ? reader.result : null);
489
+ reader.onerror = ()=>reject(reader.error);
490
+ reader.readAsDataURL(blob);
491
+ });
492
+ } catch (error) {
493
+ console.error('Failed to fetch recorder screenshot asset:', error);
494
+ return null;
495
+ }
496
+ }
497
+ async clearRecorderScreenshotAssets(sessionId) {
498
+ if (!this.serverUrl || !sessionId) return;
499
+ const response = await fetch(`${this.serverUrl}/recorder/assets/session/${encodeURIComponent(sessionId)}`, {
500
+ method: 'DELETE'
501
+ });
502
+ if (!response.ok) throw new Error(`Recorder screenshot cleanup request failed (${response.status})`);
503
+ }
504
+ async pruneRecorderScreenshotAssets(sessionId, assetIds) {
505
+ if (!this.serverUrl || !sessionId) return;
506
+ const response = await fetch(`${this.serverUrl}/recorder/assets/session/${encodeURIComponent(sessionId)}/prune`, {
507
+ method: 'POST',
508
+ headers: {
509
+ 'Content-Type': 'application/json'
510
+ },
511
+ body: JSON.stringify({
512
+ assetIds
513
+ })
514
+ });
515
+ if (!response.ok) throw new Error(`Recorder screenshot prune request failed (${response.status})`);
516
+ }
480
517
  async getInterfaceInfo() {
481
518
  if (!this.serverUrl) return null;
482
519
  try {
@@ -1 +1 @@
1
- {"version":3,"file":"adapters/remote-execution.mjs","sources":["../../../src/adapters/remote-execution.ts"],"sourcesContent":["import type {\n ConnectivityTestResult,\n DeviceAction,\n ExecutionDump,\n} from '@midscene/core';\nimport type { TModelConfig } from '@midscene/shared/env';\nimport { parseStructuredParams } from '../common';\nimport type {\n PlaygroundRecorderCapabilitiesResult,\n PlaygroundRecorderDescribeResult,\n PlaygroundRecorderEvent,\n PlaygroundRecorderEventsResult,\n PlaygroundRecorderSourceKind,\n PlaygroundRecorderStartResult,\n PlaygroundSessionSetup,\n PlaygroundSessionState,\n PlaygroundSessionTarget,\n} from '../platform';\nimport type { PlaygroundRuntimeInfo } from '../runtime-metadata';\nimport type { ExecutionOptions, FormValue, ValidationResult } from '../types';\nimport { BasePlaygroundAdapter } from './base';\n\nexport class RemoteExecutionAdapter extends BasePlaygroundAdapter {\n private serverUrl?: string;\n private _id?: string;\n private dumpUpdateCallback?: (\n dump: string,\n executionDump?: ExecutionDump,\n ) => void;\n private pollingIntervalId?: ReturnType<typeof setInterval>;\n\n constructor(serverUrl: string) {\n super();\n this.serverUrl = serverUrl;\n }\n\n // Set dump update callback\n onDumpUpdate(\n callback: (dump: string, executionDump?: ExecutionDump) => void,\n ): void {\n this.dumpUpdateCallback = undefined;\n this.dumpUpdateCallback = callback;\n }\n\n // Get adapter ID (cached after first status check for remote)\n get id(): string | undefined {\n return this._id;\n }\n\n // Override validateParams for remote execution\n // Since schemas from server are JSON-serialized and lack .parse() method\n validateParams(\n value: FormValue,\n action: DeviceAction<unknown> | undefined,\n ): ValidationResult {\n if (!action?.paramSchema) {\n return { valid: true };\n }\n\n const needsStructuredParams = this.actionNeedsStructuredParams(action);\n\n if (!needsStructuredParams) {\n return { valid: true };\n }\n\n if (!value.params) {\n return { valid: false, errorMessage: 'Parameters are required' };\n }\n\n // For remote execution, perform basic validation without .parse()\n // Check if required fields are present\n if (action.paramSchema && typeof action.paramSchema === 'object') {\n const schema = action.paramSchema as any;\n if (schema.shape || schema.type === 'ZodObject') {\n const shape = schema.shape || {};\n const missingFields = Object.keys(shape).filter((key) => {\n const fieldDef = shape[key];\n // Check if field is required (not optional)\n const isOptional =\n fieldDef?.isOptional ||\n fieldDef?._def?.innerType || // ZodOptional\n fieldDef?._def?.typeName === 'ZodOptional';\n return (\n !isOptional &&\n (value.params![key] === undefined || value.params![key] === '')\n );\n });\n\n if (missingFields.length > 0) {\n return {\n valid: false,\n errorMessage: `Missing required parameters: ${missingFields.join(', ')}`,\n };\n }\n }\n }\n\n return { valid: true };\n }\n\n async parseStructuredParams(\n action: DeviceAction<unknown>,\n params: Record<string, unknown>,\n options: ExecutionOptions,\n ): Promise<unknown[]> {\n // Use shared implementation from common.ts\n return await parseStructuredParams(action, params, options);\n }\n\n formatErrorMessage(error: any): string {\n const message = error?.message || '';\n\n // Handle Android-specific errors\n const androidErrors = [\n {\n keyword: 'adb',\n message:\n 'ADB connection error. Please ensure device is connected and USB debugging is enabled.',\n },\n {\n keyword: 'UIAutomator',\n message:\n 'UIAutomator error. Please ensure the UIAutomator server is running on the device.',\n },\n ];\n\n const androidError = androidErrors.find(({ keyword }) =>\n message.includes(keyword),\n );\n if (androidError) {\n return androidError.message;\n }\n\n return this.formatBasicErrorMessage(error);\n }\n\n // Remote execution adapter - simplified interface\n async executeAction(\n actionType: string,\n value: FormValue,\n options: ExecutionOptions,\n ): Promise<unknown> {\n // If serverUrl is provided, use server-side execution\n if (this.serverUrl && typeof window !== 'undefined') {\n return this.executeViaServer(actionType, value, options);\n }\n\n throw new Error(\n 'Remote execution adapter requires server URL for execution',\n );\n }\n\n // Remote execution via server - uses same endpoint as requestPlaygroundServer\n private async executeViaServer(\n actionType: string,\n value: FormValue,\n options: ExecutionOptions,\n ): Promise<unknown> {\n const payload: Record<string, unknown> = {\n type: actionType,\n prompt: value.prompt,\n ...this.buildOptionalPayloadParams(options, value),\n };\n\n // Add context only if it exists (server can handle single agent case without context)\n if (options.context) {\n payload.context = options.context;\n }\n\n // Start polling if requestId is provided and dumpUpdateCallback is set\n if (options.requestId && this.dumpUpdateCallback) {\n this.startProgressPolling(options.requestId);\n }\n\n try {\n const response = await fetch(`${this.serverUrl}/execute`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(payload),\n });\n\n if (!response.ok) {\n const errorText = await response.text().catch(() => 'Unknown error');\n throw new Error(\n `Server request failed (${response.status}): ${errorText}`,\n );\n }\n\n const result = await response.json();\n\n return result;\n } catch (error) {\n console.error('Execute via server failed:', error);\n throw error;\n } finally {\n // Stop polling when execution completes (success or error)\n this.stopProgressPolling();\n }\n }\n\n // Helper method to build optional payload parameters\n private buildOptionalPayloadParams(\n options: ExecutionOptions,\n value: FormValue,\n ): Record<string, unknown> {\n const optionalParams: Record<string, unknown> = {};\n\n // Add optional parameters only if they have meaningful values\n const optionalFields = [\n { key: 'requestId', value: options.requestId },\n { key: 'deepLocate', value: options.deepLocate },\n { key: 'deepThink', value: options.deepThink },\n { key: 'screenshotIncluded', value: options.screenshotIncluded },\n { key: 'domIncluded', value: options.domIncluded },\n { key: 'deviceOptions', value: options.deviceOptions },\n { key: 'reportDisplay', value: options.reportDisplay },\n { key: 'params', value: value.params },\n ] as const;\n\n optionalFields.forEach(({ key, value }) => {\n if (value !== undefined && value !== null && value !== '') {\n optionalParams[key] = value;\n }\n });\n\n return optionalParams;\n }\n\n // Get action space from server with fallback\n async getActionSpace(context?: unknown): Promise<DeviceAction<unknown>[]> {\n // Try server first if available\n if (this.serverUrl && typeof window !== 'undefined') {\n try {\n const response = await fetch(`${this.serverUrl}/action-space`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ context }),\n });\n\n if (!response.ok) {\n throw new Error(`Failed to get action space: ${response.statusText}`);\n }\n\n const result = await response.json();\n return Array.isArray(result) ? result : [];\n } catch (error) {\n console.error('Failed to get action space from server:', error);\n // Fall through to context fallback\n }\n }\n\n // Fallback: try context.actionSpace if available\n if (context && typeof context === 'object' && 'actionSpace' in context) {\n try {\n const actionSpaceMethod = (\n context as {\n actionSpace: () =>\n | DeviceAction<unknown>[]\n | Promise<DeviceAction<unknown>[]>;\n }\n ).actionSpace;\n const result = await actionSpaceMethod();\n return Array.isArray(result) ? result : [];\n } catch (error) {\n console.error('Failed to get action space from context:', error);\n }\n }\n\n return [];\n }\n\n // Uses base implementation for validateParams and createDisplayContent\n\n // Server communication methods\n async checkStatus(): Promise<boolean> {\n if (!this.serverUrl) {\n return false;\n }\n\n try {\n const res = await fetch(`${this.serverUrl}/status`);\n if (res.status === 200) {\n // Try to extract id from response\n try {\n const data = await res.json();\n if (data.id && typeof data.id === 'string') {\n this._id = data.id;\n }\n } catch (jsonError) {\n // If JSON parsing fails, id remains undefined but status is still OK\n console.debug('Failed to parse status response:', jsonError);\n }\n return true;\n }\n return false;\n } catch (error) {\n console.warn('Server status check failed:', error);\n return false;\n }\n }\n\n async overrideConfig(aiConfig: Record<string, unknown>): Promise<void> {\n if (!this.serverUrl) {\n throw new Error('Server URL not configured');\n }\n\n try {\n const response = await fetch(`${this.serverUrl}/config`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ aiConfig }),\n });\n\n if (!response.ok) {\n const body = await response.json().catch(() => null);\n const detail = body?.error || response.statusText;\n throw new Error(detail);\n }\n } catch (error) {\n console.error('Failed to override server config:', error);\n throw error;\n }\n }\n\n async runConnectivityTest(\n aiConfig: TModelConfig,\n ): Promise<ConnectivityTestResult> {\n if (!this.serverUrl) {\n throw new Error('Server URL not configured');\n }\n\n const response = await fetch(`${this.serverUrl}/connectivity-test`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ config: aiConfig }),\n });\n\n if (!response.ok) {\n const body = await response.json().catch(() => null);\n const detail = body?.error || response.statusText;\n throw new Error(detail);\n }\n\n return response.json();\n }\n\n async getTaskProgress(requestId: string): Promise<{\n executionDump?: ExecutionDump;\n }> {\n if (!this.serverUrl) {\n return {};\n }\n\n if (!requestId?.trim()) {\n console.warn('Invalid requestId provided for task progress');\n return {};\n }\n\n try {\n const response = await fetch(\n `${this.serverUrl}/task-progress/${encodeURIComponent(requestId)}`,\n );\n\n if (!response.ok) {\n console.warn(`Task progress request failed: ${response.statusText}`);\n return {};\n }\n\n return await response.json();\n } catch (error) {\n console.error('Failed to poll task progress:', error);\n return {};\n }\n }\n\n /**\n * Start polling for task progress and invoke dump update callback\n */\n private startProgressPolling(requestId: string): void {\n // Clear any existing polling\n this.stopProgressPolling();\n\n // Poll every 500ms for progress updates\n this.pollingIntervalId = setInterval(async () => {\n try {\n const progressData = await this.getTaskProgress(requestId);\n\n if (progressData.executionDump) {\n // Invoke dump update callback if set\n if (this.dumpUpdateCallback) {\n this.dumpUpdateCallback('', progressData.executionDump);\n }\n }\n } catch (error) {\n console.error('Error polling task progress:', error);\n }\n }, 500); // Poll every 500ms\n }\n\n /**\n * Stop polling for task progress\n */\n private stopProgressPolling(): void {\n if (this.pollingIntervalId) {\n clearInterval(this.pollingIntervalId);\n this.pollingIntervalId = undefined;\n }\n }\n\n // Cancel task\n async cancelTask(\n requestId: string,\n ): Promise<{ error?: string; success?: boolean }> {\n if (!this.serverUrl) {\n return { error: 'No server URL configured' };\n }\n\n if (!requestId?.trim()) {\n return { error: 'Invalid request ID' };\n }\n\n try {\n const res = await fetch(\n `${this.serverUrl}/cancel/${encodeURIComponent(requestId)}`,\n {\n method: 'POST',\n },\n );\n\n if (!res.ok) {\n return { error: `Cancel request failed: ${res.statusText}` };\n }\n\n const result = await res.json();\n return { success: true, ...result };\n } catch (error) {\n console.error('Failed to cancel task:', error);\n return { error: 'Failed to cancel task' };\n }\n }\n\n // Get screenshot from server\n async getScreenshot(): Promise<{\n screenshot: string;\n timestamp: number;\n } | null> {\n if (!this.serverUrl) {\n return null;\n }\n\n try {\n const response = await fetch(`${this.serverUrl}/screenshot`);\n\n if (!response.ok) {\n if (response.status !== 409) {\n console.warn(`Screenshot request failed: ${response.statusText}`);\n }\n return null;\n }\n\n return await response.json();\n } catch (error) {\n console.error('Failed to get screenshot:', error);\n return null;\n }\n }\n\n // Direct device manipulation – invokes a named action on the connected\n // device without going through AI planning.\n async interact(\n payload: { actionType: string } & Record<string, unknown>,\n ): Promise<{ ok: boolean; error?: string }> {\n if (!this.serverUrl) {\n return { ok: false, error: 'No server URL configured' };\n }\n\n try {\n const response = await fetch(`${this.serverUrl}/interact`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(payload),\n });\n\n const data = (await response.json().catch(() => null)) as {\n error?: string;\n } | null;\n\n if (!response.ok) {\n return {\n ok: false,\n error: data?.error || `Interact request failed (${response.status})`,\n };\n }\n\n return { ok: true };\n } catch (error) {\n return {\n ok: false,\n error: error instanceof Error ? error.message : 'Unknown error',\n };\n }\n }\n\n async startRecorderSession(\n sessionId: string,\n ): Promise<PlaygroundRecorderStartResult> {\n if (!this.serverUrl) {\n return { ok: false, supported: false, error: 'No server URL configured' };\n }\n\n try {\n const response = await fetch(`${this.serverUrl}/recorder/start`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n sessionId,\n }),\n });\n const data = (await response.json().catch(() => null)) as {\n ok?: boolean;\n supported?: boolean;\n source?: PlaygroundRecorderSourceKind;\n platformId?: string;\n error?: string;\n } | null;\n\n if (!response.ok) {\n return {\n ok: false,\n supported: data?.supported ?? false,\n source: data?.source,\n platformId: data?.platformId,\n error:\n data?.error || `Recorder start request failed (${response.status})`,\n };\n }\n\n return {\n ok: data?.ok ?? true,\n supported: data?.supported ?? true,\n source: data?.source,\n platformId: data?.platformId,\n error: data?.error,\n };\n } catch (error) {\n return {\n ok: false,\n supported: false,\n error: error instanceof Error ? error.message : 'Unknown error',\n };\n }\n }\n\n async getRecorderCapabilities(): Promise<PlaygroundRecorderCapabilitiesResult> {\n if (!this.serverUrl) {\n return {\n supported: false,\n source: 'unsupported',\n error: 'No server URL configured',\n };\n }\n\n try {\n const response = await fetch(`${this.serverUrl}/recorder/capabilities`);\n const data = (await response.json().catch(() => null)) as {\n supported?: boolean;\n source?: PlaygroundRecorderSourceKind;\n platformId?: string;\n error?: string;\n } | null;\n if (!response.ok) {\n return {\n supported: false,\n source: 'unsupported',\n error:\n data?.error ||\n `Recorder capabilities request failed (${response.status})`,\n };\n }\n return {\n supported: data?.supported === true,\n source: data?.source || 'unsupported',\n platformId: data?.platformId,\n error: data?.error,\n };\n } catch (error) {\n return {\n supported: false,\n source: 'unsupported',\n error: error instanceof Error ? error.message : 'Unknown error',\n };\n }\n }\n\n async stopRecorderSession(): Promise<{ ok: boolean; error?: string }> {\n if (!this.serverUrl) {\n return { ok: false, error: 'No server URL configured' };\n }\n\n try {\n const response = await fetch(`${this.serverUrl}/recorder/stop`, {\n method: 'POST',\n });\n if (!response.ok) {\n return {\n ok: false,\n error: `Recorder stop request failed (${response.status})`,\n };\n }\n return { ok: true };\n } catch (error) {\n return {\n ok: false,\n error: error instanceof Error ? error.message : 'Unknown error',\n };\n }\n }\n\n async getRecorderEvents(since = 0): Promise<PlaygroundRecorderEventsResult> {\n if (!this.serverUrl) {\n return { events: [], nextIndex: since };\n }\n\n try {\n const response = await fetch(\n `${this.serverUrl}/recorder/events?since=${encodeURIComponent(String(since))}`,\n );\n if (!response.ok) {\n return { events: [], nextIndex: since };\n }\n const data = (await response.json().catch(() => null)) as {\n events?: unknown;\n nextIndex?: unknown;\n } | null;\n return {\n events: Array.isArray(data?.events) ? data.events : [],\n nextIndex:\n typeof data?.nextIndex === 'number' && Number.isFinite(data.nextIndex)\n ? data.nextIndex\n : since,\n };\n } catch (error) {\n console.error('Failed to poll recorder events:', error);\n return { events: [], nextIndex: since };\n }\n }\n\n async describeRecorderEventAtPoint(\n event: PlaygroundRecorderEvent,\n ): Promise<PlaygroundRecorderDescribeResult> {\n if (!this.serverUrl) {\n return { ok: false, error: 'No server URL configured' };\n }\n\n try {\n const response = await fetch(\n `${this.serverUrl}/recorder/describe-event`,\n {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ event }),\n },\n );\n const data = (await response\n .json()\n .catch(() => null)) as PlaygroundRecorderDescribeResult | null;\n if (!response.ok) {\n return {\n ok: false,\n error:\n data?.error ||\n `Recorder describe request failed (${response.status})`,\n };\n }\n return data || { ok: false, error: 'Empty recorder describe response' };\n } catch (error) {\n return {\n ok: false,\n error:\n error instanceof Error\n ? error.message\n : 'Failed to describe recorder event',\n };\n }\n }\n\n // Get interface information from server\n async getInterfaceInfo(): Promise<{\n type: string;\n description?: string;\n size?: { width: number; height: number };\n navigationState?: { isLoading: boolean };\n actionTypes?: string[];\n } | null> {\n if (!this.serverUrl) {\n return null;\n }\n\n try {\n const response = await fetch(`${this.serverUrl}/interface-info`);\n\n if (!response.ok) {\n console.warn(`Interface info request failed: ${response.statusText}`);\n return null;\n }\n\n return await response.json();\n } catch (error) {\n console.error('Failed to get interface info:', error);\n return null;\n }\n }\n\n async getRuntimeInfo(): Promise<PlaygroundRuntimeInfo | null> {\n if (!this.serverUrl) {\n return null;\n }\n\n try {\n const response = await fetch(`${this.serverUrl}/runtime-info`);\n\n if (!response.ok) {\n console.warn(`Runtime info request failed: ${response.statusText}`);\n return null;\n }\n\n return await response.json();\n } catch (error) {\n console.error('Failed to get runtime info:', error);\n return null;\n }\n }\n\n async getSessionInfo(): Promise<PlaygroundSessionState | null> {\n if (!this.serverUrl) {\n return null;\n }\n\n try {\n const response = await fetch(`${this.serverUrl}/session`);\n if (!response.ok) {\n console.warn(`Session info request failed: ${response.statusText}`);\n return null;\n }\n\n return await response.json();\n } catch (error) {\n console.error('Failed to get session info:', error);\n return null;\n }\n }\n\n async getSessionSetup(\n input?: Record<string, unknown>,\n ): Promise<PlaygroundSessionSetup | null> {\n if (!this.serverUrl) {\n return null;\n }\n\n try {\n const searchParams = new URLSearchParams();\n Object.entries(input || {}).forEach(([key, value]) => {\n if (\n typeof value === 'string' ||\n typeof value === 'number' ||\n typeof value === 'boolean'\n ) {\n searchParams.set(key, String(value));\n }\n });\n const suffix = searchParams.size > 0 ? `?${searchParams.toString()}` : '';\n const response = await fetch(`${this.serverUrl}/session/setup${suffix}`);\n if (!response.ok) {\n const body = await response.json().catch(() => null);\n throw new Error(\n body?.error || response.statusText || 'Failed to load session setup',\n );\n }\n\n return await response.json();\n } catch (error) {\n console.error('Failed to get session setup:', error);\n throw error;\n }\n }\n\n async listSessionTargets(): Promise<PlaygroundSessionTarget[]> {\n if (!this.serverUrl) {\n return [];\n }\n\n try {\n const response = await fetch(`${this.serverUrl}/session/targets`);\n if (!response.ok) {\n const body = await response.json().catch(() => null);\n throw new Error(\n body?.error ||\n response.statusText ||\n 'Failed to load session targets',\n );\n }\n\n const result = await response.json();\n return Array.isArray(result) ? result : [];\n } catch (error) {\n console.error('Failed to get session targets:', error);\n throw error;\n }\n }\n\n async createSession(input?: Record<string, unknown>): Promise<{\n session: PlaygroundSessionState;\n runtimeInfo: PlaygroundRuntimeInfo;\n }> {\n if (!this.serverUrl) {\n throw new Error('Server URL not configured');\n }\n\n const response = await fetch(`${this.serverUrl}/session`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(input || {}),\n });\n\n if (!response.ok) {\n const body = await response.json().catch(() => null);\n throw new Error(body?.error || response.statusText);\n }\n\n return await response.json();\n }\n\n async destroySession(): Promise<{\n session: PlaygroundSessionState;\n runtimeInfo: PlaygroundRuntimeInfo;\n }> {\n if (!this.serverUrl) {\n throw new Error('Server URL not configured');\n }\n\n const response = await fetch(`${this.serverUrl}/session`, {\n method: 'DELETE',\n });\n\n if (!response.ok) {\n const body = await response.json().catch(() => null);\n throw new Error(body?.error || response.statusText);\n }\n\n return await response.json();\n }\n}\n"],"names":["RemoteExecutionAdapter","BasePlaygroundAdapter","callback","undefined","value","action","needsStructuredParams","schema","shape","missingFields","Object","key","fieldDef","isOptional","params","options","parseStructuredParams","error","message","androidErrors","androidError","keyword","actionType","window","Error","payload","response","fetch","JSON","errorText","result","console","optionalParams","optionalFields","context","Array","actionSpaceMethod","res","data","jsonError","aiConfig","body","detail","requestId","encodeURIComponent","setInterval","progressData","clearInterval","sessionId","since","String","Number","event","input","searchParams","URLSearchParams","suffix","serverUrl"],"mappings":"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"}
1
+ {"version":3,"file":"adapters/remote-execution.mjs","sources":["../../../src/adapters/remote-execution.ts"],"sourcesContent":["import type {\n ConnectivityTestResult,\n DeviceAction,\n ExecutionDump,\n} from '@midscene/core';\nimport type { TModelConfig } from '@midscene/shared/env';\nimport { parseStructuredParams } from '../common';\nimport type {\n PlaygroundRecorderCapabilitiesResult,\n PlaygroundRecorderDescribeResult,\n PlaygroundRecorderEvent,\n PlaygroundRecorderEventsResult,\n PlaygroundRecorderSourceKind,\n PlaygroundRecorderStartResult,\n PlaygroundSessionSetup,\n PlaygroundSessionState,\n PlaygroundSessionTarget,\n} from '../platform';\nimport type { PlaygroundRuntimeInfo } from '../runtime-metadata';\nimport type { ExecutionOptions, FormValue, ValidationResult } from '../types';\nimport { BasePlaygroundAdapter } from './base';\n\nexport class RemoteExecutionAdapter extends BasePlaygroundAdapter {\n private serverUrl?: string;\n private _id?: string;\n private dumpUpdateCallback?: (\n dump: string,\n executionDump?: ExecutionDump,\n ) => void;\n private pollingIntervalId?: ReturnType<typeof setInterval>;\n\n constructor(serverUrl: string) {\n super();\n this.serverUrl = serverUrl;\n }\n\n // Set dump update callback\n onDumpUpdate(\n callback: (dump: string, executionDump?: ExecutionDump) => void,\n ): void {\n this.dumpUpdateCallback = undefined;\n this.dumpUpdateCallback = callback;\n }\n\n // Get adapter ID (cached after first status check for remote)\n get id(): string | undefined {\n return this._id;\n }\n\n // Override validateParams for remote execution\n // Since schemas from server are JSON-serialized and lack .parse() method\n validateParams(\n value: FormValue,\n action: DeviceAction<unknown> | undefined,\n ): ValidationResult {\n if (!action?.paramSchema) {\n return { valid: true };\n }\n\n const needsStructuredParams = this.actionNeedsStructuredParams(action);\n\n if (!needsStructuredParams) {\n return { valid: true };\n }\n\n if (!value.params) {\n return { valid: false, errorMessage: 'Parameters are required' };\n }\n\n // For remote execution, perform basic validation without .parse()\n // Check if required fields are present\n if (action.paramSchema && typeof action.paramSchema === 'object') {\n const schema = action.paramSchema as any;\n if (schema.shape || schema.type === 'ZodObject') {\n const shape = schema.shape || {};\n const missingFields = Object.keys(shape).filter((key) => {\n const fieldDef = shape[key];\n // Check if field is required (not optional)\n const isOptional =\n fieldDef?.isOptional ||\n fieldDef?._def?.innerType || // ZodOptional\n fieldDef?._def?.typeName === 'ZodOptional';\n return (\n !isOptional &&\n (value.params![key] === undefined || value.params![key] === '')\n );\n });\n\n if (missingFields.length > 0) {\n return {\n valid: false,\n errorMessage: `Missing required parameters: ${missingFields.join(', ')}`,\n };\n }\n }\n }\n\n return { valid: true };\n }\n\n async parseStructuredParams(\n action: DeviceAction<unknown>,\n params: Record<string, unknown>,\n options: ExecutionOptions,\n ): Promise<unknown[]> {\n // Use shared implementation from common.ts\n return await parseStructuredParams(action, params, options);\n }\n\n formatErrorMessage(error: any): string {\n const message = error?.message || '';\n\n // Handle Android-specific errors\n const androidErrors = [\n {\n keyword: 'adb',\n message:\n 'ADB connection error. Please ensure device is connected and USB debugging is enabled.',\n },\n {\n keyword: 'UIAutomator',\n message:\n 'UIAutomator error. Please ensure the UIAutomator server is running on the device.',\n },\n ];\n\n const androidError = androidErrors.find(({ keyword }) =>\n message.includes(keyword),\n );\n if (androidError) {\n return androidError.message;\n }\n\n return this.formatBasicErrorMessage(error);\n }\n\n // Remote execution adapter - simplified interface\n async executeAction(\n actionType: string,\n value: FormValue,\n options: ExecutionOptions,\n ): Promise<unknown> {\n // If serverUrl is provided, use server-side execution\n if (this.serverUrl && typeof window !== 'undefined') {\n return this.executeViaServer(actionType, value, options);\n }\n\n throw new Error(\n 'Remote execution adapter requires server URL for execution',\n );\n }\n\n // Remote execution via server - uses same endpoint as requestPlaygroundServer\n private async executeViaServer(\n actionType: string,\n value: FormValue,\n options: ExecutionOptions,\n ): Promise<unknown> {\n const payload: Record<string, unknown> = {\n type: actionType,\n prompt: value.prompt,\n ...this.buildOptionalPayloadParams(options, value),\n };\n\n // Add context only if it exists (server can handle single agent case without context)\n if (options.context) {\n payload.context = options.context;\n }\n\n // Start polling if requestId is provided and dumpUpdateCallback is set\n if (options.requestId && this.dumpUpdateCallback) {\n this.startProgressPolling(options.requestId);\n }\n\n try {\n const response = await fetch(`${this.serverUrl}/execute`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(payload),\n });\n\n if (!response.ok) {\n const errorText = await response.text().catch(() => 'Unknown error');\n throw new Error(\n `Server request failed (${response.status}): ${errorText}`,\n );\n }\n\n const result = await response.json();\n\n return result;\n } catch (error) {\n console.error('Execute via server failed:', error);\n throw error;\n } finally {\n // Stop polling when execution completes (success or error)\n this.stopProgressPolling();\n }\n }\n\n // Helper method to build optional payload parameters\n private buildOptionalPayloadParams(\n options: ExecutionOptions,\n value: FormValue,\n ): Record<string, unknown> {\n const optionalParams: Record<string, unknown> = {};\n\n // Add optional parameters only if they have meaningful values\n const optionalFields = [\n { key: 'requestId', value: options.requestId },\n { key: 'deepLocate', value: options.deepLocate },\n { key: 'deepThink', value: options.deepThink },\n { key: 'screenshotIncluded', value: options.screenshotIncluded },\n { key: 'domIncluded', value: options.domIncluded },\n { key: 'deviceOptions', value: options.deviceOptions },\n { key: 'reportDisplay', value: options.reportDisplay },\n { key: 'params', value: value.params },\n ] as const;\n\n optionalFields.forEach(({ key, value }) => {\n if (value !== undefined && value !== null && value !== '') {\n optionalParams[key] = value;\n }\n });\n\n return optionalParams;\n }\n\n // Get action space from server with fallback\n async getActionSpace(context?: unknown): Promise<DeviceAction<unknown>[]> {\n // Try server first if available\n if (this.serverUrl && typeof window !== 'undefined') {\n try {\n const response = await fetch(`${this.serverUrl}/action-space`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ context }),\n });\n\n if (!response.ok) {\n throw new Error(`Failed to get action space: ${response.statusText}`);\n }\n\n const result = await response.json();\n return Array.isArray(result) ? result : [];\n } catch (error) {\n console.error('Failed to get action space from server:', error);\n // Fall through to context fallback\n }\n }\n\n // Fallback: try context.actionSpace if available\n if (context && typeof context === 'object' && 'actionSpace' in context) {\n try {\n const actionSpaceMethod = (\n context as {\n actionSpace: () =>\n | DeviceAction<unknown>[]\n | Promise<DeviceAction<unknown>[]>;\n }\n ).actionSpace;\n const result = await actionSpaceMethod();\n return Array.isArray(result) ? result : [];\n } catch (error) {\n console.error('Failed to get action space from context:', error);\n }\n }\n\n return [];\n }\n\n // Uses base implementation for validateParams and createDisplayContent\n\n // Server communication methods\n async checkStatus(): Promise<boolean> {\n if (!this.serverUrl) {\n return false;\n }\n\n try {\n const res = await fetch(`${this.serverUrl}/status`);\n if (res.status === 200) {\n // Try to extract id from response\n try {\n const data = await res.json();\n if (data.id && typeof data.id === 'string') {\n this._id = data.id;\n }\n } catch (jsonError) {\n // If JSON parsing fails, id remains undefined but status is still OK\n console.debug('Failed to parse status response:', jsonError);\n }\n return true;\n }\n return false;\n } catch (error) {\n console.warn('Server status check failed:', error);\n return false;\n }\n }\n\n async overrideConfig(aiConfig: Record<string, unknown>): Promise<void> {\n if (!this.serverUrl) {\n throw new Error('Server URL not configured');\n }\n\n try {\n const response = await fetch(`${this.serverUrl}/config`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ aiConfig }),\n });\n\n if (!response.ok) {\n const body = await response.json().catch(() => null);\n const detail = body?.error || response.statusText;\n throw new Error(detail);\n }\n } catch (error) {\n console.error('Failed to override server config:', error);\n throw error;\n }\n }\n\n async runConnectivityTest(\n aiConfig: TModelConfig,\n ): Promise<ConnectivityTestResult> {\n if (!this.serverUrl) {\n throw new Error('Server URL not configured');\n }\n\n const response = await fetch(`${this.serverUrl}/connectivity-test`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ config: aiConfig }),\n });\n\n if (!response.ok) {\n const body = await response.json().catch(() => null);\n const detail = body?.error || response.statusText;\n throw new Error(detail);\n }\n\n return response.json();\n }\n\n async getTaskProgress(requestId: string): Promise<{\n executionDump?: ExecutionDump;\n }> {\n if (!this.serverUrl) {\n return {};\n }\n\n if (!requestId?.trim()) {\n console.warn('Invalid requestId provided for task progress');\n return {};\n }\n\n try {\n const response = await fetch(\n `${this.serverUrl}/task-progress/${encodeURIComponent(requestId)}`,\n );\n\n if (!response.ok) {\n console.warn(`Task progress request failed: ${response.statusText}`);\n return {};\n }\n\n return await response.json();\n } catch (error) {\n console.error('Failed to poll task progress:', error);\n return {};\n }\n }\n\n /**\n * Start polling for task progress and invoke dump update callback\n */\n private startProgressPolling(requestId: string): void {\n // Clear any existing polling\n this.stopProgressPolling();\n\n // Poll every 500ms for progress updates\n this.pollingIntervalId = setInterval(async () => {\n try {\n const progressData = await this.getTaskProgress(requestId);\n\n if (progressData.executionDump) {\n // Invoke dump update callback if set\n if (this.dumpUpdateCallback) {\n this.dumpUpdateCallback('', progressData.executionDump);\n }\n }\n } catch (error) {\n console.error('Error polling task progress:', error);\n }\n }, 500); // Poll every 500ms\n }\n\n /**\n * Stop polling for task progress\n */\n private stopProgressPolling(): void {\n if (this.pollingIntervalId) {\n clearInterval(this.pollingIntervalId);\n this.pollingIntervalId = undefined;\n }\n }\n\n // Cancel task\n async cancelTask(\n requestId: string,\n ): Promise<{ error?: string; success?: boolean }> {\n if (!this.serverUrl) {\n return { error: 'No server URL configured' };\n }\n\n if (!requestId?.trim()) {\n return { error: 'Invalid request ID' };\n }\n\n try {\n const res = await fetch(\n `${this.serverUrl}/cancel/${encodeURIComponent(requestId)}`,\n {\n method: 'POST',\n },\n );\n\n if (!res.ok) {\n return { error: `Cancel request failed: ${res.statusText}` };\n }\n\n const result = await res.json();\n return { success: true, ...result };\n } catch (error) {\n console.error('Failed to cancel task:', error);\n return { error: 'Failed to cancel task' };\n }\n }\n\n // Get screenshot from server\n async getScreenshot(): Promise<{\n screenshot: string;\n timestamp: number;\n } | null> {\n if (!this.serverUrl) {\n return null;\n }\n\n try {\n const response = await fetch(`${this.serverUrl}/screenshot`);\n\n if (!response.ok) {\n if (response.status !== 409) {\n console.warn(`Screenshot request failed: ${response.statusText}`);\n }\n return null;\n }\n\n return await response.json();\n } catch (error) {\n console.error('Failed to get screenshot:', error);\n return null;\n }\n }\n\n // Direct device manipulation – invokes a named action on the connected\n // device without going through AI planning.\n async interact(\n payload: { actionType: string } & Record<string, unknown>,\n ): Promise<{ ok: boolean; error?: string }> {\n if (!this.serverUrl) {\n return { ok: false, error: 'No server URL configured' };\n }\n\n try {\n const response = await fetch(`${this.serverUrl}/interact`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(payload),\n });\n\n const data = (await response.json().catch(() => null)) as {\n error?: string;\n } | null;\n\n if (!response.ok) {\n return {\n ok: false,\n error: data?.error || `Interact request failed (${response.status})`,\n };\n }\n\n return { ok: true };\n } catch (error) {\n return {\n ok: false,\n error: error instanceof Error ? error.message : 'Unknown error',\n };\n }\n }\n\n async startRecorderSession(\n sessionId: string,\n ): Promise<PlaygroundRecorderStartResult> {\n if (!this.serverUrl) {\n return { ok: false, supported: false, error: 'No server URL configured' };\n }\n\n try {\n const response = await fetch(`${this.serverUrl}/recorder/start`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n sessionId,\n }),\n });\n const data = (await response.json().catch(() => null)) as {\n ok?: boolean;\n supported?: boolean;\n source?: PlaygroundRecorderSourceKind;\n platformId?: string;\n error?: string;\n } | null;\n\n if (!response.ok) {\n return {\n ok: false,\n supported: data?.supported ?? false,\n source: data?.source,\n platformId: data?.platformId,\n error:\n data?.error || `Recorder start request failed (${response.status})`,\n };\n }\n\n return {\n ok: data?.ok ?? true,\n supported: data?.supported ?? true,\n source: data?.source,\n platformId: data?.platformId,\n error: data?.error,\n };\n } catch (error) {\n return {\n ok: false,\n supported: false,\n error: error instanceof Error ? error.message : 'Unknown error',\n };\n }\n }\n\n async getRecorderCapabilities(): Promise<PlaygroundRecorderCapabilitiesResult> {\n if (!this.serverUrl) {\n return {\n supported: false,\n source: 'unsupported',\n error: 'No server URL configured',\n };\n }\n\n try {\n const response = await fetch(`${this.serverUrl}/recorder/capabilities`);\n const data = (await response.json().catch(() => null)) as {\n supported?: boolean;\n source?: PlaygroundRecorderSourceKind;\n platformId?: string;\n error?: string;\n } | null;\n if (!response.ok) {\n return {\n supported: false,\n source: 'unsupported',\n error:\n data?.error ||\n `Recorder capabilities request failed (${response.status})`,\n };\n }\n return {\n supported: data?.supported === true,\n source: data?.source || 'unsupported',\n platformId: data?.platformId,\n error: data?.error,\n };\n } catch (error) {\n return {\n supported: false,\n source: 'unsupported',\n error: error instanceof Error ? error.message : 'Unknown error',\n };\n }\n }\n\n async stopRecorderSession(): Promise<{ ok: boolean; error?: string }> {\n if (!this.serverUrl) {\n return { ok: false, error: 'No server URL configured' };\n }\n\n try {\n const response = await fetch(`${this.serverUrl}/recorder/stop`, {\n method: 'POST',\n });\n if (!response.ok) {\n return {\n ok: false,\n error: `Recorder stop request failed (${response.status})`,\n };\n }\n return { ok: true };\n } catch (error) {\n return {\n ok: false,\n error: error instanceof Error ? error.message : 'Unknown error',\n };\n }\n }\n\n async getRecorderEvents(since = 0): Promise<PlaygroundRecorderEventsResult> {\n if (!this.serverUrl) {\n return { events: [], nextIndex: since };\n }\n\n try {\n const response = await fetch(\n `${this.serverUrl}/recorder/events?since=${encodeURIComponent(String(since))}&flushPending=false`,\n );\n if (!response.ok) {\n return { events: [], nextIndex: since };\n }\n const data = (await response.json().catch(() => null)) as {\n events?: unknown;\n nextIndex?: unknown;\n } | null;\n return {\n events: Array.isArray(data?.events) ? data.events : [],\n nextIndex:\n typeof data?.nextIndex === 'number' && Number.isFinite(data.nextIndex)\n ? data.nextIndex\n : since,\n };\n } catch (error) {\n console.error('Failed to poll recorder events:', error);\n return { events: [], nextIndex: since };\n }\n }\n\n async describeRecorderEventAtPoint(\n event: PlaygroundRecorderEvent,\n ): Promise<PlaygroundRecorderDescribeResult> {\n if (!this.serverUrl) {\n return { ok: false, error: 'No server URL configured' };\n }\n\n try {\n const response = await fetch(\n `${this.serverUrl}/recorder/describe-event`,\n {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ event }),\n },\n );\n const data = (await response\n .json()\n .catch(() => null)) as PlaygroundRecorderDescribeResult | null;\n if (!response.ok) {\n return {\n ok: false,\n error:\n data?.error ||\n `Recorder describe request failed (${response.status})`,\n };\n }\n return data || { ok: false, error: 'Empty recorder describe response' };\n } catch (error) {\n return {\n ok: false,\n error:\n error instanceof Error\n ? error.message\n : 'Failed to describe recorder event',\n };\n }\n }\n\n async getRecorderScreenshotAsset(assetId: string): Promise<string | null> {\n if (!this.serverUrl || !assetId) {\n return null;\n }\n try {\n const response = await fetch(\n `${this.serverUrl}/recorder/assets/${encodeURIComponent(assetId)}`,\n );\n if (!response.ok) {\n return null;\n }\n const blob = await response.blob();\n return await new Promise<string | null>((resolve, reject) => {\n const reader = new FileReader();\n reader.onload = () =>\n resolve(typeof reader.result === 'string' ? reader.result : null);\n reader.onerror = () => reject(reader.error);\n reader.readAsDataURL(blob);\n });\n } catch (error) {\n console.error('Failed to fetch recorder screenshot asset:', error);\n return null;\n }\n }\n\n async clearRecorderScreenshotAssets(sessionId: string): Promise<void> {\n if (!this.serverUrl || !sessionId) {\n return;\n }\n const response = await fetch(\n `${this.serverUrl}/recorder/assets/session/${encodeURIComponent(sessionId)}`,\n { method: 'DELETE' },\n );\n if (!response.ok) {\n throw new Error(\n `Recorder screenshot cleanup request failed (${response.status})`,\n );\n }\n }\n\n async pruneRecorderScreenshotAssets(\n sessionId: string,\n assetIds: string[],\n ): Promise<void> {\n if (!this.serverUrl || !sessionId) {\n return;\n }\n const response = await fetch(\n `${this.serverUrl}/recorder/assets/session/${encodeURIComponent(sessionId)}/prune`,\n {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ assetIds }),\n },\n );\n if (!response.ok) {\n throw new Error(\n `Recorder screenshot prune request failed (${response.status})`,\n );\n }\n }\n\n // Get interface information from server\n async getInterfaceInfo(): Promise<{\n type: string;\n description?: string;\n size?: { width: number; height: number };\n navigationState?: { isLoading: boolean };\n actionTypes?: string[];\n } | null> {\n if (!this.serverUrl) {\n return null;\n }\n\n try {\n const response = await fetch(`${this.serverUrl}/interface-info`);\n\n if (!response.ok) {\n console.warn(`Interface info request failed: ${response.statusText}`);\n return null;\n }\n\n return await response.json();\n } catch (error) {\n console.error('Failed to get interface info:', error);\n return null;\n }\n }\n\n async getRuntimeInfo(): Promise<PlaygroundRuntimeInfo | null> {\n if (!this.serverUrl) {\n return null;\n }\n\n try {\n const response = await fetch(`${this.serverUrl}/runtime-info`);\n\n if (!response.ok) {\n console.warn(`Runtime info request failed: ${response.statusText}`);\n return null;\n }\n\n return await response.json();\n } catch (error) {\n console.error('Failed to get runtime info:', error);\n return null;\n }\n }\n\n async getSessionInfo(): Promise<PlaygroundSessionState | null> {\n if (!this.serverUrl) {\n return null;\n }\n\n try {\n const response = await fetch(`${this.serverUrl}/session`);\n if (!response.ok) {\n console.warn(`Session info request failed: ${response.statusText}`);\n return null;\n }\n\n return await response.json();\n } catch (error) {\n console.error('Failed to get session info:', error);\n return null;\n }\n }\n\n async getSessionSetup(\n input?: Record<string, unknown>,\n ): Promise<PlaygroundSessionSetup | null> {\n if (!this.serverUrl) {\n return null;\n }\n\n try {\n const searchParams = new URLSearchParams();\n Object.entries(input || {}).forEach(([key, value]) => {\n if (\n typeof value === 'string' ||\n typeof value === 'number' ||\n typeof value === 'boolean'\n ) {\n searchParams.set(key, String(value));\n }\n });\n const suffix = searchParams.size > 0 ? `?${searchParams.toString()}` : '';\n const response = await fetch(`${this.serverUrl}/session/setup${suffix}`);\n if (!response.ok) {\n const body = await response.json().catch(() => null);\n throw new Error(\n body?.error || response.statusText || 'Failed to load session setup',\n );\n }\n\n return await response.json();\n } catch (error) {\n console.error('Failed to get session setup:', error);\n throw error;\n }\n }\n\n async listSessionTargets(): Promise<PlaygroundSessionTarget[]> {\n if (!this.serverUrl) {\n return [];\n }\n\n try {\n const response = await fetch(`${this.serverUrl}/session/targets`);\n if (!response.ok) {\n const body = await response.json().catch(() => null);\n throw new Error(\n body?.error ||\n response.statusText ||\n 'Failed to load session targets',\n );\n }\n\n const result = await response.json();\n return Array.isArray(result) ? result : [];\n } catch (error) {\n console.error('Failed to get session targets:', error);\n throw error;\n }\n }\n\n async createSession(input?: Record<string, unknown>): Promise<{\n session: PlaygroundSessionState;\n runtimeInfo: PlaygroundRuntimeInfo;\n }> {\n if (!this.serverUrl) {\n throw new Error('Server URL not configured');\n }\n\n const response = await fetch(`${this.serverUrl}/session`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(input || {}),\n });\n\n if (!response.ok) {\n const body = await response.json().catch(() => null);\n throw new Error(body?.error || response.statusText);\n }\n\n return await response.json();\n }\n\n async destroySession(): Promise<{\n session: PlaygroundSessionState;\n runtimeInfo: PlaygroundRuntimeInfo;\n }> {\n if (!this.serverUrl) {\n throw new Error('Server URL not configured');\n }\n\n const response = await fetch(`${this.serverUrl}/session`, {\n method: 'DELETE',\n });\n\n if (!response.ok) {\n const body = await response.json().catch(() => null);\n throw new Error(body?.error || response.statusText);\n }\n\n return await response.json();\n }\n}\n"],"names":["RemoteExecutionAdapter","BasePlaygroundAdapter","callback","undefined","value","action","needsStructuredParams","schema","shape","missingFields","Object","key","fieldDef","isOptional","params","options","parseStructuredParams","error","message","androidErrors","androidError","keyword","actionType","window","Error","payload","response","fetch","JSON","errorText","result","console","optionalParams","optionalFields","context","Array","actionSpaceMethod","res","data","jsonError","aiConfig","body","detail","requestId","encodeURIComponent","setInterval","progressData","clearInterval","sessionId","since","String","Number","event","assetId","blob","Promise","resolve","reject","reader","FileReader","assetIds","input","searchParams","URLSearchParams","suffix","serverUrl"],"mappings":"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"}
@@ -21,6 +21,11 @@ function writeMjpegFrame(res, boundary, frame) {
21
21
  writable = res.write('\r\n') && writable;
22
22
  return writable;
23
23
  }
24
+ function endMjpegResponse(res) {
25
+ try {
26
+ res.end();
27
+ } catch {}
28
+ }
24
29
  class InterfaceMjpegHub {
25
30
  async streamRequest(req, res, activeInterface, recoverActiveAgent) {
26
31
  return this.streamRequestInternal(req, res, activeInterface, recoverActiveAgent, true);
@@ -91,9 +96,7 @@ class InterfaceMjpegHub {
91
96
  for (const [oldSubscriber, oldRes] of producer.responses){
92
97
  producer.subscribers.delete(oldSubscriber);
93
98
  producer.responses.delete(oldSubscriber);
94
- try {
95
- oldRes.end();
96
- } catch {}
99
+ endMjpegResponse(oldRes);
97
100
  }
98
101
  producer.subscribers.add(subscriber);
99
102
  producer.responses.set(subscriber, res);
@@ -179,9 +182,7 @@ class InterfaceMjpegHub {
179
182
  clearTimeout(producer.stopTimer);
180
183
  producer.stopTimer = void 0;
181
184
  }
182
- for (const [, res] of producer.responses)try {
183
- res.destroy();
184
- } catch {}
185
+ for (const [, res] of producer.responses)endMjpegResponse(res);
185
186
  producer.responses.clear();
186
187
  producer.subscribers.clear();
187
188
  producer.controller.abort();
@@ -1 +1 @@
1
- {"version":3,"file":"mjpeg-hub.mjs","sources":["../../src/mjpeg-hub.ts"],"sourcesContent":["import type { Agent as PageAgent } from '@midscene/core/agent';\nimport type {\n MjpegStreamFrame,\n MjpegStreamHandle,\n} from '@midscene/core/device';\nimport { type DebugFunction, getDebug } from '@midscene/shared/logger';\nimport type { Request, Response } from 'express';\n\nconst DATA_URL_BASE64_PREFIX = /^data:image\\/\\w+;base64,/;\n\nconst noopDebug: DebugFunction = () => {};\n\ntype ActiveInterface = PageAgent['interface'];\n\ntype Subscriber = (frame: MjpegStreamFrame) => void;\n\ninterface InternalProducer {\n source: ActiveInterface;\n controller: AbortController;\n handle?: MjpegStreamHandle;\n lastFrame?: MjpegStreamFrame;\n startupError?: unknown;\n firstFrameReady: Promise<boolean>;\n subscribers: Set<Subscriber>;\n /** Tracks `res` instances per subscriber so the hub can hard-close them. */\n responses: Map<Subscriber, Response>;\n stopTimer?: ReturnType<typeof setTimeout>;\n}\n\nexport interface InterfaceMjpegHubOptions {\n /** Time the hub waits for the first producer frame before falling back. */\n initialFrameTimeoutMs: number;\n /** Idle window after the last subscriber leaves before tearing the producer down. */\n idleStopMs: number;\n /** Optional debug logger for hub internals. Defaults to a no-op. */\n debug?: DebugFunction;\n}\n\n/**\n * Recovery hook supplied by the server. When the producer fails to start\n * because the underlying page session was closed, the hub asks the server to\n * rebuild the agent and returns the new interface; otherwise the hub gives up\n * and lets `streamRequest` resolve to false.\n */\nexport type RecoverActiveAgent = (\n error: unknown,\n) => Promise<ActiveInterface | null>;\n\n/**\n * Writes one MJPEG part to `res`, preferring backpressure-safe writes.\n *\n * Returns `true` when the chunk has been accepted by the socket buffer and\n * `false` when the kernel buffer is full. Callers SHOULD drop frames or wait\n * for `drain` instead of pushing more data when this returns `false`.\n *\n * `frame.data` may either be raw base64 or a `data:image/...;base64,...` URL;\n * the function strips the prefix defensively. New producers should already\n * normalize to bare base64.\n */\nexport function writeMjpegFrame(\n res: Response,\n boundary: string,\n frame: MjpegStreamFrame,\n): boolean {\n const raw = frame.data.replace(DATA_URL_BASE64_PREFIX, '');\n const buf = Buffer.from(raw, 'base64');\n\n // Each `res.write` returns false when the kernel buffer is full. We\n // surface the worst result so the caller can react to backpressure on the\n // first chunk that exceeds the high water mark.\n let writable = res.write(`--${boundary}\\r\\n`);\n writable =\n res.write(`Content-Type: ${frame.contentType || 'image/jpeg'}\\r\\n`) &&\n writable;\n writable = res.write(`Content-Length: ${buf.length}\\r\\n\\r\\n`) && writable;\n writable = res.write(buf) && writable;\n writable = res.write('\\r\\n') && writable;\n return writable;\n}\n\n/**\n * Owns the lifecycle of an in-process MJPEG frame producer (e.g. Chromium\n * CDP `Page.startScreencast`) and fans frames out to all currently connected\n * HTTP MJPEG clients.\n *\n * Why this is its own class:\n * - CDP screencasts are page-scoped, so multiple concurrent producers would\n * steal frames from each other. Keeping a single producer + N subscribers\n * here prevents the playground server from accidentally racing against\n * itself.\n * - Producer creation, idle teardown, recovery after page-session loss and\n * backpressure handling are all naturally co-located with the producer\n * state. Moving them out of `PlaygroundServer` keeps that class focused on\n * HTTP routing.\n */\nexport class InterfaceMjpegHub {\n private producer?: InternalProducer;\n private readonly debug: DebugFunction;\n\n constructor(private readonly opts: InterfaceMjpegHubOptions) {\n this.debug = opts.debug ?? noopDebug;\n }\n\n /**\n * Streams the active interface's MJPEG frames to `res`. Returns true once\n * the response is committed to streaming, false if the interface has no\n * frame producer or the initial frame never arrived.\n */\n async streamRequest(\n req: Request,\n res: Response,\n activeInterface: ActiveInterface,\n recoverActiveAgent: RecoverActiveAgent,\n ): Promise<boolean> {\n return this.streamRequestInternal(\n req,\n res,\n activeInterface,\n recoverActiveAgent,\n true,\n );\n }\n\n /**\n * Tears down the current producer (used when the server replaces an agent\n * out-of-band, e.g. after a recoverable page-session error during /interact).\n */\n stopProducer(): void {\n this.stopProducerInternal(this.producer);\n }\n\n /**\n * Best-effort shutdown for server.close(). Aborts any active producer and\n * forcibly closes attached subscriber sockets.\n */\n shutdown(): void {\n const producer = this.producer;\n if (!producer) return;\n for (const [subscriber, res] of producer.responses) {\n producer.subscribers.delete(subscriber);\n try {\n res.destroy();\n } catch {\n /* socket already closed */\n }\n }\n producer.responses.clear();\n this.stopProducerInternal(producer);\n }\n\n getLastFrame(): MjpegStreamFrame | undefined {\n return this.producer?.lastFrame;\n }\n\n private async streamRequestInternal(\n req: Request,\n res: Response,\n activeInterface: ActiveInterface,\n recoverActiveAgent: RecoverActiveAgent,\n allowRecovery: boolean,\n ): Promise<boolean> {\n const producer = this.getOrCreateProducer(activeInterface);\n if (!producer) return false;\n\n const hasInitialFrame = await producer.firstFrameReady;\n if (!hasInitialFrame || !producer.lastFrame) {\n this.debug(\n 'interface frame producer did not emit an initial frame, falling back to polling',\n );\n const startupError = producer.startupError;\n this.stopProducerInternal(producer);\n if (allowRecovery && startupError) {\n const recoveredInterface = await recoverActiveAgent(startupError);\n if (recoveredInterface) {\n return this.streamRequestInternal(\n req,\n res,\n recoveredInterface,\n recoverActiveAgent,\n false,\n );\n }\n }\n return false;\n }\n\n this.attachSubscriber(req, res, producer);\n return true;\n }\n\n private attachSubscriber(\n req: Request,\n res: Response,\n producer: InternalProducer,\n ): void {\n const boundary = 'mjpeg-boundary';\n let closed = false;\n let dropping = false;\n\n const closeResponse = () => {\n if (closed) return;\n closed = true;\n this.releaseSubscriber(producer, subscriber);\n };\n\n const subscriber: Subscriber = (frame) => {\n if (closed) return;\n // Drop frames while the socket buffer is full instead of letting the\n // node internal buffer balloon. CDP screencasts can run at 60Hz and a\n // slow client would otherwise OOM the server.\n if (dropping) return;\n try {\n const writable = writeMjpegFrame(res, boundary, frame);\n if (!writable) {\n dropping = true;\n res.once('drain', () => {\n dropping = false;\n });\n }\n } catch (error) {\n this.debug('interface frame write failed: %s', error);\n closeResponse();\n try {\n res.destroy();\n } catch {\n /* socket already closed */\n }\n }\n };\n\n // Chromium's <img> with multipart/x-mixed-replace keeps the underlying\n // TCP connection alive even after the element is unmounted from the DOM —\n // there's no FIN until something else cleans up. Each subsequent mount\n // (Overview ↔ Device re-entry, React StrictMode double-mount, retry\n // timer) opens a new socket without releasing the old one. Studio only\n // ever has a single visible preview, so before attaching the new\n // subscriber we end any stale ones — this both releases server-side\n // resources and unblocks Chromium's per-origin connection slot quota\n // (6 for HTTP/1.1). Without this, after a handful of re-mounts the\n // browser cannot open any further /mjpeg request and shows a permanent\n // blank canvas.\n //\n // Use `res.end()` (not `res.destroy()`) so the chunked-transfer\n // terminator (`0\\r\\n\\r\\n`) is flushed before close. With `destroy()`\n // Chromium reports ERR_INCOMPLETE_CHUNKED_ENCODING on the previous\n // request *and on the next* — apparently because the connection-pool\n // entry is poisoned — leaving the new <img>.naturalWidth at 0.\n for (const [oldSubscriber, oldRes] of producer.responses) {\n producer.subscribers.delete(oldSubscriber);\n producer.responses.delete(oldSubscriber);\n try {\n oldRes.end();\n } catch {\n /* response already closed */\n }\n }\n\n producer.subscribers.add(subscriber);\n producer.responses.set(subscriber, res);\n if (producer.stopTimer) {\n clearTimeout(producer.stopTimer);\n producer.stopTimer = undefined;\n }\n req.on('close', closeResponse);\n\n res.setHeader(\n 'Content-Type',\n `multipart/x-mixed-replace; boundary=${boundary}`,\n );\n res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');\n res.setHeader('Connection', 'keep-alive');\n this.flushInitialFrame(subscriber, producer.lastFrame as MjpegStreamFrame);\n\n this.debug('streaming via shared interface frame producer');\n }\n\n /**\n * Push the producer's cached frame to a freshly-attached subscriber.\n *\n * Why two writes:\n *\n * Chromium's `<img>` with `multipart/x-mixed-replace` only commits a part\n * to display once it sees the *next* part's boundary delimiter — the\n * boundary is what tells the decoder that the previous part's body is\n * complete. When the CDP screencast is idle (page is past\n * `waitForNetworkIdle`, no animation) the producer only ever pushes one\n * frame: the cached `lastFrame`. With a single write, the browser holds\n * onto the bytes but never paints them (`<img>.naturalWidth === 0`) — a\n * permanent blank canvas while waiting for a frame that never arrives.\n *\n * The duplicate write below is a sentinel: the second part's leading\n * boundary is exactly what unblocks the first part's commit. The second\n * part itself never gets displayed (multipart only ever shows the latest\n * committed part, and any subsequent real CDP frame overrides it), so\n * the cost is one extra frame on the wire per subscriber attach. Without\n * this, Overview → Device re-entry consistently leaves the user staring\n * at white.\n */\n private flushInitialFrame(\n subscriber: Subscriber,\n lastFrame: MjpegStreamFrame,\n ): void {\n subscriber(lastFrame);\n subscriber(lastFrame);\n }\n\n private getOrCreateProducer(\n activeInterface: ActiveInterface,\n ): InternalProducer | null {\n const startMjpegStream = activeInterface.startMjpegStream;\n if (typeof startMjpegStream !== 'function') return null;\n\n if (this.producer?.source === activeInterface) {\n if (this.producer.stopTimer) {\n clearTimeout(this.producer.stopTimer);\n this.producer.stopTimer = undefined;\n }\n return this.producer;\n }\n\n this.stopProducerInternal(this.producer);\n\n const controller = new AbortController();\n let resolveInitialFrame: ((hasFrame: boolean) => void) | undefined;\n let initialFrameTimer: ReturnType<typeof setTimeout> | undefined;\n\n const resolveInitialFrameOnce = (hasFrame: boolean) => {\n if (!resolveInitialFrame) return;\n if (initialFrameTimer) {\n clearTimeout(initialFrameTimer);\n initialFrameTimer = undefined;\n }\n resolveInitialFrame(hasFrame);\n resolveInitialFrame = undefined;\n };\n\n const initialFrameReady = new Promise<boolean>((resolve) => {\n resolveInitialFrame = resolve;\n initialFrameTimer = setTimeout(() => {\n resolveInitialFrameOnce(false);\n }, this.opts.initialFrameTimeoutMs);\n });\n\n const producer: InternalProducer = {\n source: activeInterface,\n controller,\n firstFrameReady: initialFrameReady,\n subscribers: new Set(),\n responses: new Map(),\n };\n this.producer = producer;\n\n void (async () => {\n try {\n producer.handle =\n (await startMjpegStream.call(activeInterface, {\n signal: controller.signal,\n onFrame: (frame) => {\n if (controller.signal.aborted) return;\n producer.lastFrame = frame;\n resolveInitialFrameOnce(true);\n for (const subscriber of producer.subscribers) {\n subscriber(frame);\n }\n },\n onError: (error) => {\n this.debug('interface stream producer error: %s', error);\n // Tear down the dead producer so the next /mjpeg request\n // (triggered by the <img> onError → retry) constructs a\n // fresh one. Without this, the dead producer is reused\n // forever — explaining why even page.reload() can't\n // recover the preview after an in-flight CDP screencast\n // dies during a task run.\n this.stopProducerInternal(producer);\n },\n })) ?? undefined;\n } catch (error) {\n this.debug('interface frame producer unavailable: %s', error);\n producer.startupError = error;\n resolveInitialFrameOnce(false);\n this.stopProducerInternal(producer);\n }\n })();\n\n return producer;\n }\n\n private stopProducerInternal(producer?: InternalProducer): void {\n if (!producer) return;\n if (producer.stopTimer) {\n clearTimeout(producer.stopTimer);\n producer.stopTimer = undefined;\n }\n // Hard-close any subscriber sockets we still own so the browser <img>\n // does not hang on a half-open multipart response.\n for (const [, res] of producer.responses) {\n try {\n res.destroy();\n } catch {\n /* socket already closed */\n }\n }\n producer.responses.clear();\n producer.subscribers.clear();\n producer.controller.abort();\n Promise.resolve(producer.handle?.stop?.()).catch((error) => {\n this.debug('interface stream stop failed: %s', error);\n });\n if (this.producer === producer) {\n this.producer = undefined;\n }\n }\n\n private releaseSubscriber(\n producer: InternalProducer,\n subscriber: Subscriber,\n ): void {\n producer.subscribers.delete(subscriber);\n producer.responses.delete(subscriber);\n if (producer.subscribers.size > 0 || producer.stopTimer) return;\n producer.stopTimer = setTimeout(() => {\n producer.stopTimer = undefined;\n if (producer.subscribers.size === 0) {\n this.stopProducerInternal(producer);\n }\n }, this.opts.idleStopMs);\n }\n}\n\n/**\n * Convenience constructor that wires up a debug logger derived from the\n * `web:mjpeg` namespace so server logs are consistent with other modules.\n */\nexport function createInterfaceMjpegHub(\n opts: Omit<InterfaceMjpegHubOptions, 'debug'> & { debug?: DebugFunction },\n): InterfaceMjpegHub {\n return new InterfaceMjpegHub({\n ...opts,\n debug: opts.debug ?? getDebug('playground:mjpeg-hub'),\n });\n}\n"],"names":["DATA_URL_BASE64_PREFIX","noopDebug","writeMjpegFrame","res","boundary","frame","raw","buf","Buffer","writable","InterfaceMjpegHub","req","activeInterface","recoverActiveAgent","producer","subscriber","allowRecovery","hasInitialFrame","startupError","recoveredInterface","closed","dropping","closeResponse","error","oldSubscriber","oldRes","clearTimeout","undefined","lastFrame","startMjpegStream","controller","AbortController","resolveInitialFrame","initialFrameTimer","resolveInitialFrameOnce","hasFrame","initialFrameReady","Promise","resolve","setTimeout","Set","Map","opts","createInterfaceMjpegHub","getDebug"],"mappings":"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"}
1
+ {"version":3,"file":"mjpeg-hub.mjs","sources":["../../src/mjpeg-hub.ts"],"sourcesContent":["import type { Agent as PageAgent } from '@midscene/core/agent';\nimport type {\n MjpegStreamFrame,\n MjpegStreamHandle,\n} from '@midscene/core/device';\nimport { type DebugFunction, getDebug } from '@midscene/shared/logger';\nimport type { Request, Response } from 'express';\n\nconst DATA_URL_BASE64_PREFIX = /^data:image\\/\\w+;base64,/;\n\nconst noopDebug: DebugFunction = () => {};\n\ntype ActiveInterface = PageAgent['interface'];\n\ntype Subscriber = (frame: MjpegStreamFrame) => void;\n\ninterface InternalProducer {\n source: ActiveInterface;\n controller: AbortController;\n handle?: MjpegStreamHandle;\n lastFrame?: MjpegStreamFrame;\n startupError?: unknown;\n firstFrameReady: Promise<boolean>;\n subscribers: Set<Subscriber>;\n /** Tracks `res` instances per subscriber so the hub can hard-close them. */\n responses: Map<Subscriber, Response>;\n stopTimer?: ReturnType<typeof setTimeout>;\n}\n\nexport interface InterfaceMjpegHubOptions {\n /** Time the hub waits for the first producer frame before falling back. */\n initialFrameTimeoutMs: number;\n /** Idle window after the last subscriber leaves before tearing the producer down. */\n idleStopMs: number;\n /** Optional debug logger for hub internals. Defaults to a no-op. */\n debug?: DebugFunction;\n}\n\n/**\n * Recovery hook supplied by the server. When the producer fails to start\n * because the underlying page session was closed, the hub asks the server to\n * rebuild the agent and returns the new interface; otherwise the hub gives up\n * and lets `streamRequest` resolve to false.\n */\nexport type RecoverActiveAgent = (\n error: unknown,\n) => Promise<ActiveInterface | null>;\n\n/**\n * Writes one MJPEG part to `res`, preferring backpressure-safe writes.\n *\n * Returns `true` when the chunk has been accepted by the socket buffer and\n * `false` when the kernel buffer is full. Callers SHOULD drop frames or wait\n * for `drain` instead of pushing more data when this returns `false`.\n *\n * `frame.data` may either be raw base64 or a `data:image/...;base64,...` URL;\n * the function strips the prefix defensively. New producers should already\n * normalize to bare base64.\n */\nexport function writeMjpegFrame(\n res: Response,\n boundary: string,\n frame: MjpegStreamFrame,\n): boolean {\n const raw = frame.data.replace(DATA_URL_BASE64_PREFIX, '');\n const buf = Buffer.from(raw, 'base64');\n\n // Each `res.write` returns false when the kernel buffer is full. We\n // surface the worst result so the caller can react to backpressure on the\n // first chunk that exceeds the high water mark.\n let writable = res.write(`--${boundary}\\r\\n`);\n writable =\n res.write(`Content-Type: ${frame.contentType || 'image/jpeg'}\\r\\n`) &&\n writable;\n writable = res.write(`Content-Length: ${buf.length}\\r\\n\\r\\n`) && writable;\n writable = res.write(buf) && writable;\n writable = res.write('\\r\\n') && writable;\n return writable;\n}\n\nfunction endMjpegResponse(res: Response): void {\n try {\n res.end();\n } catch {\n /* response already closed */\n }\n}\n\n/**\n * Owns the lifecycle of an in-process MJPEG frame producer (e.g. Chromium\n * CDP `Page.startScreencast`) and fans frames out to all currently connected\n * HTTP MJPEG clients.\n *\n * Why this is its own class:\n * - CDP screencasts are page-scoped, so multiple concurrent producers would\n * steal frames from each other. Keeping a single producer + N subscribers\n * here prevents the playground server from accidentally racing against\n * itself.\n * - Producer creation, idle teardown, recovery after page-session loss and\n * backpressure handling are all naturally co-located with the producer\n * state. Moving them out of `PlaygroundServer` keeps that class focused on\n * HTTP routing.\n */\nexport class InterfaceMjpegHub {\n private producer?: InternalProducer;\n private readonly debug: DebugFunction;\n\n constructor(private readonly opts: InterfaceMjpegHubOptions) {\n this.debug = opts.debug ?? noopDebug;\n }\n\n /**\n * Streams the active interface's MJPEG frames to `res`. Returns true once\n * the response is committed to streaming, false if the interface has no\n * frame producer or the initial frame never arrived.\n */\n async streamRequest(\n req: Request,\n res: Response,\n activeInterface: ActiveInterface,\n recoverActiveAgent: RecoverActiveAgent,\n ): Promise<boolean> {\n return this.streamRequestInternal(\n req,\n res,\n activeInterface,\n recoverActiveAgent,\n true,\n );\n }\n\n /**\n * Tears down the current producer (used when the server replaces an agent\n * out-of-band, e.g. after a recoverable page-session error during /interact).\n */\n stopProducer(): void {\n this.stopProducerInternal(this.producer);\n }\n\n /**\n * Best-effort shutdown for server.close(). Aborts any active producer and\n * forcibly closes attached subscriber sockets.\n */\n shutdown(): void {\n const producer = this.producer;\n if (!producer) return;\n for (const [subscriber, res] of producer.responses) {\n producer.subscribers.delete(subscriber);\n try {\n res.destroy();\n } catch {\n /* socket already closed */\n }\n }\n producer.responses.clear();\n this.stopProducerInternal(producer);\n }\n\n getLastFrame(): MjpegStreamFrame | undefined {\n return this.producer?.lastFrame;\n }\n\n private async streamRequestInternal(\n req: Request,\n res: Response,\n activeInterface: ActiveInterface,\n recoverActiveAgent: RecoverActiveAgent,\n allowRecovery: boolean,\n ): Promise<boolean> {\n const producer = this.getOrCreateProducer(activeInterface);\n if (!producer) return false;\n\n const hasInitialFrame = await producer.firstFrameReady;\n if (!hasInitialFrame || !producer.lastFrame) {\n this.debug(\n 'interface frame producer did not emit an initial frame, falling back to polling',\n );\n const startupError = producer.startupError;\n this.stopProducerInternal(producer);\n if (allowRecovery && startupError) {\n const recoveredInterface = await recoverActiveAgent(startupError);\n if (recoveredInterface) {\n return this.streamRequestInternal(\n req,\n res,\n recoveredInterface,\n recoverActiveAgent,\n false,\n );\n }\n }\n return false;\n }\n\n this.attachSubscriber(req, res, producer);\n return true;\n }\n\n private attachSubscriber(\n req: Request,\n res: Response,\n producer: InternalProducer,\n ): void {\n const boundary = 'mjpeg-boundary';\n let closed = false;\n let dropping = false;\n\n const closeResponse = () => {\n if (closed) return;\n closed = true;\n this.releaseSubscriber(producer, subscriber);\n };\n\n const subscriber: Subscriber = (frame) => {\n if (closed) return;\n // Drop frames while the socket buffer is full instead of letting the\n // node internal buffer balloon. CDP screencasts can run at 60Hz and a\n // slow client would otherwise OOM the server.\n if (dropping) return;\n try {\n const writable = writeMjpegFrame(res, boundary, frame);\n if (!writable) {\n dropping = true;\n res.once('drain', () => {\n dropping = false;\n });\n }\n } catch (error) {\n this.debug('interface frame write failed: %s', error);\n closeResponse();\n try {\n res.destroy();\n } catch {\n /* socket already closed */\n }\n }\n };\n\n // Chromium's <img> with multipart/x-mixed-replace keeps the underlying\n // TCP connection alive even after the element is unmounted from the DOM —\n // there's no FIN until something else cleans up. Each subsequent mount\n // (Overview ↔ Device re-entry, React StrictMode double-mount, retry\n // timer) opens a new socket without releasing the old one. Studio only\n // ever has a single visible preview, so before attaching the new\n // subscriber we end any stale ones — this both releases server-side\n // resources and unblocks Chromium's per-origin connection slot quota\n // (6 for HTTP/1.1). Without this, after a handful of re-mounts the\n // browser cannot open any further /mjpeg request and shows a permanent\n // blank canvas.\n //\n // Use graceful end for MJPEG responses so Chromium does not poison the\n // connection pool with ERR_INCOMPLETE_CHUNKED_ENCODING and leave the next\n // <img>.naturalWidth at 0.\n for (const [oldSubscriber, oldRes] of producer.responses) {\n producer.subscribers.delete(oldSubscriber);\n producer.responses.delete(oldSubscriber);\n endMjpegResponse(oldRes);\n }\n\n producer.subscribers.add(subscriber);\n producer.responses.set(subscriber, res);\n if (producer.stopTimer) {\n clearTimeout(producer.stopTimer);\n producer.stopTimer = undefined;\n }\n req.on('close', closeResponse);\n\n res.setHeader(\n 'Content-Type',\n `multipart/x-mixed-replace; boundary=${boundary}`,\n );\n res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');\n res.setHeader('Connection', 'keep-alive');\n this.flushInitialFrame(subscriber, producer.lastFrame as MjpegStreamFrame);\n\n this.debug('streaming via shared interface frame producer');\n }\n\n /**\n * Push the producer's cached frame to a freshly-attached subscriber.\n *\n * Why two writes:\n *\n * Chromium's `<img>` with `multipart/x-mixed-replace` only commits a part\n * to display once it sees the *next* part's boundary delimiter — the\n * boundary is what tells the decoder that the previous part's body is\n * complete. When the CDP screencast is idle (page is past\n * `waitForNetworkIdle`, no animation) the producer only ever pushes one\n * frame: the cached `lastFrame`. With a single write, the browser holds\n * onto the bytes but never paints them (`<img>.naturalWidth === 0`) — a\n * permanent blank canvas while waiting for a frame that never arrives.\n *\n * The duplicate write below is a sentinel: the second part's leading\n * boundary is exactly what unblocks the first part's commit. The second\n * part itself never gets displayed (multipart only ever shows the latest\n * committed part, and any subsequent real CDP frame overrides it), so\n * the cost is one extra frame on the wire per subscriber attach. Without\n * this, Overview → Device re-entry consistently leaves the user staring\n * at white.\n */\n private flushInitialFrame(\n subscriber: Subscriber,\n lastFrame: MjpegStreamFrame,\n ): void {\n subscriber(lastFrame);\n subscriber(lastFrame);\n }\n\n private getOrCreateProducer(\n activeInterface: ActiveInterface,\n ): InternalProducer | null {\n const startMjpegStream = activeInterface.startMjpegStream;\n if (typeof startMjpegStream !== 'function') return null;\n\n if (this.producer?.source === activeInterface) {\n if (this.producer.stopTimer) {\n clearTimeout(this.producer.stopTimer);\n this.producer.stopTimer = undefined;\n }\n return this.producer;\n }\n\n this.stopProducerInternal(this.producer);\n\n const controller = new AbortController();\n let resolveInitialFrame: ((hasFrame: boolean) => void) | undefined;\n let initialFrameTimer: ReturnType<typeof setTimeout> | undefined;\n\n const resolveInitialFrameOnce = (hasFrame: boolean) => {\n if (!resolveInitialFrame) return;\n if (initialFrameTimer) {\n clearTimeout(initialFrameTimer);\n initialFrameTimer = undefined;\n }\n resolveInitialFrame(hasFrame);\n resolveInitialFrame = undefined;\n };\n\n const initialFrameReady = new Promise<boolean>((resolve) => {\n resolveInitialFrame = resolve;\n initialFrameTimer = setTimeout(() => {\n resolveInitialFrameOnce(false);\n }, this.opts.initialFrameTimeoutMs);\n });\n\n const producer: InternalProducer = {\n source: activeInterface,\n controller,\n firstFrameReady: initialFrameReady,\n subscribers: new Set(),\n responses: new Map(),\n };\n this.producer = producer;\n\n void (async () => {\n try {\n producer.handle =\n (await startMjpegStream.call(activeInterface, {\n signal: controller.signal,\n onFrame: (frame) => {\n if (controller.signal.aborted) return;\n producer.lastFrame = frame;\n resolveInitialFrameOnce(true);\n for (const subscriber of producer.subscribers) {\n subscriber(frame);\n }\n },\n onError: (error) => {\n this.debug('interface stream producer error: %s', error);\n // Tear down the dead producer so the next /mjpeg request\n // (triggered by the <img> onError → retry) constructs a\n // fresh one. Without this, the dead producer is reused\n // forever — explaining why even page.reload() can't\n // recover the preview after an in-flight CDP screencast\n // dies during a task run.\n this.stopProducerInternal(producer);\n },\n })) ?? undefined;\n } catch (error) {\n this.debug('interface frame producer unavailable: %s', error);\n producer.startupError = error;\n resolveInitialFrameOnce(false);\n this.stopProducerInternal(producer);\n }\n })();\n\n return producer;\n }\n\n private stopProducerInternal(producer?: InternalProducer): void {\n if (!producer) return;\n if (producer.stopTimer) {\n clearTimeout(producer.stopTimer);\n producer.stopTimer = undefined;\n }\n // End any subscriber responses we still own. A hard destroy here can leave\n // Chromium's multipart image loader stuck on a blank cached connection\n // after the producer dies during navigation.\n for (const [, res] of producer.responses) {\n endMjpegResponse(res);\n }\n producer.responses.clear();\n producer.subscribers.clear();\n producer.controller.abort();\n Promise.resolve(producer.handle?.stop?.()).catch((error) => {\n this.debug('interface stream stop failed: %s', error);\n });\n if (this.producer === producer) {\n this.producer = undefined;\n }\n }\n\n private releaseSubscriber(\n producer: InternalProducer,\n subscriber: Subscriber,\n ): void {\n producer.subscribers.delete(subscriber);\n producer.responses.delete(subscriber);\n if (producer.subscribers.size > 0 || producer.stopTimer) return;\n producer.stopTimer = setTimeout(() => {\n producer.stopTimer = undefined;\n if (producer.subscribers.size === 0) {\n this.stopProducerInternal(producer);\n }\n }, this.opts.idleStopMs);\n }\n}\n\n/**\n * Convenience constructor that wires up a debug logger derived from the\n * `web:mjpeg` namespace so server logs are consistent with other modules.\n */\nexport function createInterfaceMjpegHub(\n opts: Omit<InterfaceMjpegHubOptions, 'debug'> & { debug?: DebugFunction },\n): InterfaceMjpegHub {\n return new InterfaceMjpegHub({\n ...opts,\n debug: opts.debug ?? getDebug('playground:mjpeg-hub'),\n });\n}\n"],"names":["DATA_URL_BASE64_PREFIX","noopDebug","writeMjpegFrame","res","boundary","frame","raw","buf","Buffer","writable","endMjpegResponse","InterfaceMjpegHub","req","activeInterface","recoverActiveAgent","producer","subscriber","allowRecovery","hasInitialFrame","startupError","recoveredInterface","closed","dropping","closeResponse","error","oldSubscriber","oldRes","clearTimeout","undefined","lastFrame","startMjpegStream","controller","AbortController","resolveInitialFrame","initialFrameTimer","resolveInitialFrameOnce","hasFrame","initialFrameReady","Promise","resolve","setTimeout","Set","Map","opts","createInterfaceMjpegHub","getDebug"],"mappings":";;;;;;;;;;;AAQA,MAAMA,yBAAyB;AAE/B,MAAMC,YAA2B,KAAO;AAiDjC,SAASC,gBACdC,GAAa,EACbC,QAAgB,EAChBC,KAAuB;IAEvB,MAAMC,MAAMD,MAAM,IAAI,CAAC,OAAO,CAACL,wBAAwB;IACvD,MAAMO,MAAMC,OAAO,IAAI,CAACF,KAAK;IAK7B,IAAIG,WAAWN,IAAI,KAAK,CAAC,CAAC,EAAE,EAAEC,SAAS,IAAI,CAAC;IAC5CK,WACEN,IAAI,KAAK,CAAC,CAAC,cAAc,EAAEE,MAAM,WAAW,IAAI,aAAa,IAAI,CAAC,KAClEI;IACFA,WAAWN,IAAI,KAAK,CAAC,CAAC,gBAAgB,EAAEI,IAAI,MAAM,CAAC,QAAQ,CAAC,KAAKE;IACjEA,WAAWN,IAAI,KAAK,CAACI,QAAQE;IAC7BA,WAAWN,IAAI,KAAK,CAAC,WAAWM;IAChC,OAAOA;AACT;AAEA,SAASC,iBAAiBP,GAAa;IACrC,IAAI;QACFA,IAAI,GAAG;IACT,EAAE,OAAM,CAER;AACF;AAiBO,MAAMQ;IAaX,MAAM,cACJC,GAAY,EACZT,GAAa,EACbU,eAAgC,EAChCC,kBAAsC,EACpB;QAClB,OAAO,IAAI,CAAC,qBAAqB,CAC/BF,KACAT,KACAU,iBACAC,oBACA;IAEJ;IAMA,eAAqB;QACnB,IAAI,CAAC,oBAAoB,CAAC,IAAI,CAAC,QAAQ;IACzC;IAMA,WAAiB;QACf,MAAMC,WAAW,IAAI,CAAC,QAAQ;QAC9B,IAAI,CAACA,UAAU;QACf,KAAK,MAAM,CAACC,YAAYb,IAAI,IAAIY,SAAS,SAAS,CAAE;YAClDA,SAAS,WAAW,CAAC,MAAM,CAACC;YAC5B,IAAI;gBACFb,IAAI,OAAO;YACb,EAAE,OAAM,CAER;QACF;QACAY,SAAS,SAAS,CAAC,KAAK;QACxB,IAAI,CAAC,oBAAoB,CAACA;IAC5B;IAEA,eAA6C;QAC3C,OAAO,IAAI,CAAC,QAAQ,EAAE;IACxB;IAEA,MAAc,sBACZH,GAAY,EACZT,GAAa,EACbU,eAAgC,EAChCC,kBAAsC,EACtCG,aAAsB,EACJ;QAClB,MAAMF,WAAW,IAAI,CAAC,mBAAmB,CAACF;QAC1C,IAAI,CAACE,UAAU,OAAO;QAEtB,MAAMG,kBAAkB,MAAMH,SAAS,eAAe;QACtD,IAAI,CAACG,mBAAmB,CAACH,SAAS,SAAS,EAAE;YAC3C,IAAI,CAAC,KAAK,CACR;YAEF,MAAMI,eAAeJ,SAAS,YAAY;YAC1C,IAAI,CAAC,oBAAoB,CAACA;YAC1B,IAAIE,iBAAiBE,cAAc;gBACjC,MAAMC,qBAAqB,MAAMN,mBAAmBK;gBACpD,IAAIC,oBACF,OAAO,IAAI,CAAC,qBAAqB,CAC/BR,KACAT,KACAiB,oBACAN,oBACA;YAGN;YACA,OAAO;QACT;QAEA,IAAI,CAAC,gBAAgB,CAACF,KAAKT,KAAKY;QAChC,OAAO;IACT;IAEQ,iBACNH,GAAY,EACZT,GAAa,EACbY,QAA0B,EACpB;QACN,MAAMX,WAAW;QACjB,IAAIiB,SAAS;QACb,IAAIC,WAAW;QAEf,MAAMC,gBAAgB;YACpB,IAAIF,QAAQ;YACZA,SAAS;YACT,IAAI,CAAC,iBAAiB,CAACN,UAAUC;QACnC;QAEA,MAAMA,aAAyB,CAACX;YAC9B,IAAIgB,QAAQ;YAIZ,IAAIC,UAAU;YACd,IAAI;gBACF,MAAMb,WAAWP,gBAAgBC,KAAKC,UAAUC;gBAChD,IAAI,CAACI,UAAU;oBACba,WAAW;oBACXnB,IAAI,IAAI,CAAC,SAAS;wBAChBmB,WAAW;oBACb;gBACF;YACF,EAAE,OAAOE,OAAO;gBACd,IAAI,CAAC,KAAK,CAAC,oCAAoCA;gBAC/CD;gBACA,IAAI;oBACFpB,IAAI,OAAO;gBACb,EAAE,OAAM,CAER;YACF;QACF;QAiBA,KAAK,MAAM,CAACsB,eAAeC,OAAO,IAAIX,SAAS,SAAS,CAAE;YACxDA,SAAS,WAAW,CAAC,MAAM,CAACU;YAC5BV,SAAS,SAAS,CAAC,MAAM,CAACU;YAC1Bf,iBAAiBgB;QACnB;QAEAX,SAAS,WAAW,CAAC,GAAG,CAACC;QACzBD,SAAS,SAAS,CAAC,GAAG,CAACC,YAAYb;QACnC,IAAIY,SAAS,SAAS,EAAE;YACtBY,aAAaZ,SAAS,SAAS;YAC/BA,SAAS,SAAS,GAAGa;QACvB;QACAhB,IAAI,EAAE,CAAC,SAASW;QAEhBpB,IAAI,SAAS,CACX,gBACA,CAAC,oCAAoC,EAAEC,UAAU;QAEnDD,IAAI,SAAS,CAAC,iBAAiB;QAC/BA,IAAI,SAAS,CAAC,cAAc;QAC5B,IAAI,CAAC,iBAAiB,CAACa,YAAYD,SAAS,SAAS;QAErD,IAAI,CAAC,KAAK,CAAC;IACb;IAwBQ,kBACNC,UAAsB,EACtBa,SAA2B,EACrB;QACNb,WAAWa;QACXb,WAAWa;IACb;IAEQ,oBACNhB,eAAgC,EACP;QACzB,MAAMiB,mBAAmBjB,gBAAgB,gBAAgB;QACzD,IAAI,AAA4B,cAA5B,OAAOiB,kBAAiC,OAAO;QAEnD,IAAI,IAAI,CAAC,QAAQ,EAAE,WAAWjB,iBAAiB;YAC7C,IAAI,IAAI,CAAC,QAAQ,CAAC,SAAS,EAAE;gBAC3Bc,aAAa,IAAI,CAAC,QAAQ,CAAC,SAAS;gBACpC,IAAI,CAAC,QAAQ,CAAC,SAAS,GAAGC;YAC5B;YACA,OAAO,IAAI,CAAC,QAAQ;QACtB;QAEA,IAAI,CAAC,oBAAoB,CAAC,IAAI,CAAC,QAAQ;QAEvC,MAAMG,aAAa,IAAIC;QACvB,IAAIC;QACJ,IAAIC;QAEJ,MAAMC,0BAA0B,CAACC;YAC/B,IAAI,CAACH,qBAAqB;YAC1B,IAAIC,mBAAmB;gBACrBP,aAAaO;gBACbA,oBAAoBN;YACtB;YACAK,oBAAoBG;YACpBH,sBAAsBL;QACxB;QAEA,MAAMS,oBAAoB,IAAIC,QAAiB,CAACC;YAC9CN,sBAAsBM;YACtBL,oBAAoBM,WAAW;gBAC7BL,wBAAwB;YAC1B,GAAG,IAAI,CAAC,IAAI,CAAC,qBAAqB;QACpC;QAEA,MAAMpB,WAA6B;YACjC,QAAQF;YACRkB;YACA,iBAAiBM;YACjB,aAAa,IAAII;YACjB,WAAW,IAAIC;QACjB;QACA,IAAI,CAAC,QAAQ,GAAG3B;QAEV;YACJ,IAAI;gBACFA,SAAS,MAAM,GACZ,MAAMe,iBAAiB,IAAI,CAACjB,iBAAiB;oBAC5C,QAAQkB,WAAW,MAAM;oBACzB,SAAS,CAAC1B;wBACR,IAAI0B,WAAW,MAAM,CAAC,OAAO,EAAE;wBAC/BhB,SAAS,SAAS,GAAGV;wBACrB8B,wBAAwB;wBACxB,KAAK,MAAMnB,cAAcD,SAAS,WAAW,CAC3CC,WAAWX;oBAEf;oBACA,SAAS,CAACmB;wBACR,IAAI,CAAC,KAAK,CAAC,uCAAuCA;wBAOlD,IAAI,CAAC,oBAAoB,CAACT;oBAC5B;gBACF,MAAOa;YACX,EAAE,OAAOJ,OAAO;gBACd,IAAI,CAAC,KAAK,CAAC,4CAA4CA;gBACvDT,SAAS,YAAY,GAAGS;gBACxBW,wBAAwB;gBACxB,IAAI,CAAC,oBAAoB,CAACpB;YAC5B;QACF;QAEA,OAAOA;IACT;IAEQ,qBAAqBA,QAA2B,EAAQ;QAC9D,IAAI,CAACA,UAAU;QACf,IAAIA,SAAS,SAAS,EAAE;YACtBY,aAAaZ,SAAS,SAAS;YAC/BA,SAAS,SAAS,GAAGa;QACvB;QAIA,KAAK,MAAM,GAAGzB,IAAI,IAAIY,SAAS,SAAS,CACtCL,iBAAiBP;QAEnBY,SAAS,SAAS,CAAC,KAAK;QACxBA,SAAS,WAAW,CAAC,KAAK;QAC1BA,SAAS,UAAU,CAAC,KAAK;QACzBuB,QAAQ,OAAO,CAACvB,SAAS,MAAM,EAAE,UAAU,KAAK,CAAC,CAACS;YAChD,IAAI,CAAC,KAAK,CAAC,oCAAoCA;QACjD;QACA,IAAI,IAAI,CAAC,QAAQ,KAAKT,UACpB,IAAI,CAAC,QAAQ,GAAGa;IAEpB;IAEQ,kBACNb,QAA0B,EAC1BC,UAAsB,EAChB;QACND,SAAS,WAAW,CAAC,MAAM,CAACC;QAC5BD,SAAS,SAAS,CAAC,MAAM,CAACC;QAC1B,IAAID,SAAS,WAAW,CAAC,IAAI,GAAG,KAAKA,SAAS,SAAS,EAAE;QACzDA,SAAS,SAAS,GAAGyB,WAAW;YAC9BzB,SAAS,SAAS,GAAGa;YACrB,IAAIb,AAA8B,MAA9BA,SAAS,WAAW,CAAC,IAAI,EAC3B,IAAI,CAAC,oBAAoB,CAACA;QAE9B,GAAG,IAAI,CAAC,IAAI,CAAC,UAAU;IACzB;IA9TA,YAA6B4B,IAA8B,CAAE;;QAH7D,uBAAQ,YAAR;QACA,uBAAiB,SAAjB;aAE6BA,IAAI,GAAJA;QAC3B,IAAI,CAAC,KAAK,GAAGA,KAAK,KAAK,IAAI1C;IAC7B;AA6TF;AAMO,SAAS2C,wBACdD,IAAyE;IAEzE,OAAO,IAAIhC,kBAAkB;QAC3B,GAAGgC,IAAI;QACP,OAAOA,KAAK,KAAK,IAAIE,SAAS;IAChC;AACF"}
@@ -22,6 +22,10 @@ const DEFAULT_FPS = 10;
22
22
  const MAX_FPS = 30;
23
23
  const MAX_ERROR_BACKOFF_MS = 3000;
24
24
  const ERROR_LOG_THRESHOLD = 3;
25
+ function toMjpegFrameDataUrl(data, contentType) {
26
+ if (data.startsWith('data:')) return data;
27
+ return `data:${contentType || 'image/jpeg'};base64,${data}`;
28
+ }
25
29
  class MjpegStreamHandler {
26
30
  reset() {
27
31
  this.nativeAvailable = null;
@@ -33,7 +37,9 @@ class MjpegStreamHandler {
33
37
  this.interfaceMjpegHub.shutdown();
34
38
  }
35
39
  getLastFrameBase64() {
36
- return this.interfaceMjpegHub.getLastFrame()?.data || this.lastPollingFrame;
40
+ const interfaceFrame = this.interfaceMjpegHub.getLastFrame();
41
+ if (interfaceFrame) return toMjpegFrameDataUrl(interfaceFrame.data, interfaceFrame.contentType);
42
+ return this.lastPollingFrame ? toMjpegFrameDataUrl(this.lastPollingFrame) : void 0;
37
43
  }
38
44
  async serve(req, res) {
39
45
  const nativeUrl = this.source.getNativeUrl();
@@ -1 +1 @@
1
- {"version":3,"file":"mjpeg-stream-handler.mjs","sources":["../../src/mjpeg-stream-handler.ts"],"sourcesContent":["import http from 'node:http';\nimport type { Agent as PageAgent } from '@midscene/core/agent';\nimport { getDebug } from '@midscene/shared/logger';\nimport type { Request, Response } from 'express';\nimport {\n type InterfaceMjpegHub,\n createInterfaceMjpegHub,\n writeMjpegFrame,\n} from './mjpeg-hub';\n\nconst debugMjpeg = getDebug('playground:mjpeg', { console: true });\n\nconst NEGATIVE_CACHE_MS = 10_000;\nconst NATIVE_PROBE_INTERVAL_MS = 3000;\nconst INTERFACE_MJPEG_INITIAL_FRAME_TIMEOUT_MS = 1500;\nconst INTERFACE_MJPEG_IDLE_STOP_MS = 2000;\n\nconst DEFAULT_FPS = 10;\nconst MAX_FPS = 30;\nconst MAX_ERROR_BACKOFF_MS = 3000;\nconst ERROR_LOG_THRESHOLD = 3;\n\ntype ActiveInterface = PageAgent['interface'];\n\n/**\n * Inputs the handler reads on every request, late-bound through callbacks\n * so a single handler instance can survive across device reconnects without\n * the server having to swap it.\n */\nexport interface MjpegStreamSource {\n /** Native MJPEG URL of the current device, or undefined if it has none. */\n getNativeUrl(): string | undefined;\n /** Active interface, used for in-process MJPEG producers such as CDP screencast. */\n getActiveInterface(): ActiveInterface | null;\n /** Polling fallback. Throws if no agent is connected. */\n takeScreenshot(): Promise<string>;\n /** Returns true when polling fallback can capture screenshots. */\n canTakeScreenshot(): boolean;\n /** Returns false while the agent is being recreated. */\n isAgentReady(): boolean;\n /** Optional recovery hook for page-session loss during preview streaming. */\n recoverFromPreviewError?(\n error: unknown,\n reason: string,\n ): Promise<ActiveInterface | null>;\n}\n\n/**\n * Owns all of the MJPEG streaming logic that used to live inline on\n * `PlaygroundServer`:\n * - Tries the device's native MJPEG URL (e.g. WDA's `iproxy 9100`).\n * - Caches a negative probe for {@link NEGATIVE_CACHE_MS} so a transient\n * unavailable WDA does not lock us into polling forever.\n * - Falls back to polling `screenshotBase64()` and emitting multipart frames.\n * - While polling, periodically re-probes the native URL and tears down\n * the polling socket the moment native comes back, so the client\n * `<img>` reconnects onto the native stream.\n *\n * State lives on the handler instance, so callers can `reset()` on device\n * reconnect to drop the cached probe result.\n */\nexport class MjpegStreamHandler {\n private nativeAvailable: boolean | null = null;\n private nativeFailedAt: number | null = null;\n private lastPollingFrame?: string;\n private readonly interfaceMjpegHub: InterfaceMjpegHub =\n createInterfaceMjpegHub({\n initialFrameTimeoutMs: INTERFACE_MJPEG_INITIAL_FRAME_TIMEOUT_MS,\n idleStopMs: INTERFACE_MJPEG_IDLE_STOP_MS,\n debug: debugMjpeg,\n });\n\n constructor(private readonly source: MjpegStreamSource) {}\n\n /** Drop the cached probe result — call this when the agent reconnects. */\n reset(): void {\n this.nativeAvailable = null;\n this.nativeFailedAt = null;\n this.lastPollingFrame = undefined;\n this.interfaceMjpegHub.stopProducer();\n }\n\n shutdown(): void {\n this.interfaceMjpegHub.shutdown();\n }\n\n getLastFrameBase64(): string | undefined {\n return this.interfaceMjpegHub.getLastFrame()?.data || this.lastPollingFrame;\n }\n\n async serve(req: Request, res: Response): Promise<void> {\n const nativeUrl = this.source.getNativeUrl();\n const recentlyFailed =\n this.nativeAvailable === false &&\n this.nativeFailedAt !== null &&\n Date.now() - this.nativeFailedAt < NEGATIVE_CACHE_MS;\n\n if (nativeUrl && !recentlyFailed) {\n const proxied = await this.probeAndProxyNative(nativeUrl, req, res);\n if (proxied) return;\n }\n\n const activeInterface = this.source.getActiveInterface();\n if (activeInterface) {\n const interfaceStreamStarted = await this.interfaceMjpegHub.streamRequest(\n req,\n res,\n activeInterface,\n async (startupError) =>\n (await this.source.recoverFromPreviewError?.(\n startupError,\n 'interface MJPEG startup',\n )) ?? null,\n );\n if (interfaceStreamStarted) return;\n }\n\n if (!this.source.canTakeScreenshot()) {\n res.status(500).json({\n error: 'Screenshot method not available on current interface',\n });\n return;\n }\n\n await this.streamPolling(req, res);\n }\n\n private probeAndProxyNative(\n nativeUrl: string,\n req: Request,\n res: Response,\n ): Promise<boolean> {\n return new Promise<boolean>((resolve) => {\n debugMjpeg(`trying native stream from ${nativeUrl}`);\n const proxyReq = http.get(nativeUrl, (proxyRes) => {\n const statusCode = proxyRes.statusCode ?? 0;\n if (statusCode >= 400) {\n this.nativeAvailable = false;\n this.nativeFailedAt = Date.now();\n proxyRes.resume();\n debugMjpeg(\n `native stream returned HTTP ${statusCode}, using polling mode`,\n );\n resolve(false);\n return;\n }\n this.nativeAvailable = true;\n this.nativeFailedAt = null;\n debugMjpeg('streaming via native WDA MJPEG server');\n const contentType = proxyRes.headers['content-type'];\n if (contentType) res.setHeader('Content-Type', contentType);\n res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');\n res.setHeader('Connection', 'keep-alive');\n proxyRes.pipe(res);\n req.on('close', () => proxyReq.destroy());\n resolve(true);\n });\n proxyReq.on('error', (err) => {\n this.nativeAvailable = false;\n this.nativeFailedAt = Date.now();\n debugMjpeg(\n `native stream unavailable (${err.message}), using polling mode`,\n );\n resolve(false);\n });\n });\n }\n\n private probeNativeLiveness(nativeUrl: string): Promise<boolean> {\n return new Promise<boolean>((resolve) => {\n const probe = http.get(nativeUrl, (probeRes) => {\n const statusCode = probeRes.statusCode ?? 0;\n const reachable = statusCode >= 200 && statusCode < 400;\n probeRes.destroy();\n resolve(reachable);\n });\n probe.setTimeout(1000, () => {\n probe.destroy();\n resolve(false);\n });\n probe.on('error', () => resolve(false));\n });\n }\n\n private async streamPolling(req: Request, res: Response): Promise<void> {\n const parsedFps = Number(req.query.fps);\n const fps = Math.min(\n Math.max(Number.isNaN(parsedFps) ? DEFAULT_FPS : parsedFps, 1),\n MAX_FPS,\n );\n const interval = Math.round(1000 / fps);\n const boundary = 'mjpeg-boundary';\n debugMjpeg(`streaming via polling mode (${fps}fps)`);\n\n res.setHeader(\n 'Content-Type',\n `multipart/x-mixed-replace; boundary=${boundary}`,\n );\n res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');\n res.setHeader('Connection', 'keep-alive');\n\n let stopped = false;\n let consecutiveErrors = 0;\n\n // While in polling mode, periodically re-probe the native URL. As soon\n // as it becomes reachable, destroy this socket so the client's <img>\n // fires onError and reconnects onto the native stream. (res.end() leaves\n // the multipart frame visually frozen in some browsers.)\n const nativeUrl = this.source.getNativeUrl();\n let probeTimer: ReturnType<typeof setInterval> | undefined;\n if (nativeUrl) {\n probeTimer = setInterval(async () => {\n if (stopped) return;\n const reachable = await this.probeNativeLiveness(nativeUrl);\n if (reachable && !stopped) {\n debugMjpeg(\n 'native stream came online, ending polling so client reconnects',\n );\n this.nativeAvailable = true;\n this.nativeFailedAt = null;\n stopped = true;\n try {\n res.destroy();\n } catch {\n /* socket already closed */\n }\n }\n }, NATIVE_PROBE_INTERVAL_MS);\n }\n req.on('close', () => {\n stopped = true;\n if (probeTimer) clearInterval(probeTimer);\n });\n\n while (!stopped) {\n if (!this.source.isAgentReady()) {\n await new Promise((r) => setTimeout(r, 200));\n continue;\n }\n\n const frameStart = Date.now();\n try {\n const base64 = await this.source.takeScreenshot();\n if (stopped) break;\n consecutiveErrors = 0;\n this.lastPollingFrame = base64;\n\n writeMjpegFrame(res, boundary, {\n data: base64,\n contentType: 'image/jpeg',\n });\n } catch (err) {\n if (stopped) break;\n const recoveredInterface = await this.source.recoverFromPreviewError?.(\n err,\n 'polling MJPEG frame capture',\n );\n if (recoveredInterface) {\n consecutiveErrors = 0;\n continue;\n }\n consecutiveErrors++;\n if (consecutiveErrors <= ERROR_LOG_THRESHOLD) {\n console.error('MJPEG frame error:', err);\n } else if (consecutiveErrors === ERROR_LOG_THRESHOLD + 1) {\n console.error(\n 'MJPEG: suppressing further errors, retrying silently...',\n );\n }\n const backoff = Math.min(\n 1000 * consecutiveErrors,\n MAX_ERROR_BACKOFF_MS,\n );\n await new Promise((r) => setTimeout(r, backoff));\n continue;\n }\n\n const elapsed = Date.now() - frameStart;\n const remaining = interval - elapsed;\n if (remaining > 0) await new Promise((r) => setTimeout(r, remaining));\n }\n if (probeTimer) clearInterval(probeTimer);\n }\n}\n"],"names":["debugMjpeg","getDebug","NEGATIVE_CACHE_MS","NATIVE_PROBE_INTERVAL_MS","INTERFACE_MJPEG_INITIAL_FRAME_TIMEOUT_MS","INTERFACE_MJPEG_IDLE_STOP_MS","DEFAULT_FPS","MAX_FPS","MAX_ERROR_BACKOFF_MS","ERROR_LOG_THRESHOLD","MjpegStreamHandler","undefined","req","res","nativeUrl","recentlyFailed","Date","proxied","activeInterface","interfaceStreamStarted","startupError","Promise","resolve","proxyReq","http","proxyRes","statusCode","contentType","err","probe","probeRes","reachable","parsedFps","Number","fps","Math","interval","boundary","stopped","consecutiveErrors","probeTimer","setInterval","clearInterval","r","setTimeout","frameStart","base64","writeMjpegFrame","recoveredInterface","console","backoff","elapsed","remaining","source","createInterfaceMjpegHub"],"mappings":"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"}
1
+ {"version":3,"file":"mjpeg-stream-handler.mjs","sources":["../../src/mjpeg-stream-handler.ts"],"sourcesContent":["import http from 'node:http';\nimport type { Agent as PageAgent } from '@midscene/core/agent';\nimport { getDebug } from '@midscene/shared/logger';\nimport type { Request, Response } from 'express';\nimport {\n type InterfaceMjpegHub,\n createInterfaceMjpegHub,\n writeMjpegFrame,\n} from './mjpeg-hub';\n\nconst debugMjpeg = getDebug('playground:mjpeg', { console: true });\n\nconst NEGATIVE_CACHE_MS = 10_000;\nconst NATIVE_PROBE_INTERVAL_MS = 3000;\nconst INTERFACE_MJPEG_INITIAL_FRAME_TIMEOUT_MS = 1500;\nconst INTERFACE_MJPEG_IDLE_STOP_MS = 2000;\n\nconst DEFAULT_FPS = 10;\nconst MAX_FPS = 30;\nconst MAX_ERROR_BACKOFF_MS = 3000;\nconst ERROR_LOG_THRESHOLD = 3;\n\ntype ActiveInterface = PageAgent['interface'];\n\nfunction toMjpegFrameDataUrl(data: string, contentType?: string) {\n if (data.startsWith('data:')) {\n return data;\n }\n return `data:${contentType || 'image/jpeg'};base64,${data}`;\n}\n\n/**\n * Inputs the handler reads on every request, late-bound through callbacks\n * so a single handler instance can survive across device reconnects without\n * the server having to swap it.\n */\nexport interface MjpegStreamSource {\n /** Native MJPEG URL of the current device, or undefined if it has none. */\n getNativeUrl(): string | undefined;\n /** Active interface, used for in-process MJPEG producers such as CDP screencast. */\n getActiveInterface(): ActiveInterface | null;\n /** Polling fallback. Throws if no agent is connected. */\n takeScreenshot(): Promise<string>;\n /** Returns true when polling fallback can capture screenshots. */\n canTakeScreenshot(): boolean;\n /** Returns false while the agent is being recreated. */\n isAgentReady(): boolean;\n /** Optional recovery hook for page-session loss during preview streaming. */\n recoverFromPreviewError?(\n error: unknown,\n reason: string,\n ): Promise<ActiveInterface | null>;\n}\n\n/**\n * Owns all of the MJPEG streaming logic that used to live inline on\n * `PlaygroundServer`:\n * - Tries the device's native MJPEG URL (e.g. WDA's `iproxy 9100`).\n * - Caches a negative probe for {@link NEGATIVE_CACHE_MS} so a transient\n * unavailable WDA does not lock us into polling forever.\n * - Falls back to polling `screenshotBase64()` and emitting multipart frames.\n * - While polling, periodically re-probes the native URL and tears down\n * the polling socket the moment native comes back, so the client\n * `<img>` reconnects onto the native stream.\n *\n * State lives on the handler instance, so callers can `reset()` on device\n * reconnect to drop the cached probe result.\n */\nexport class MjpegStreamHandler {\n private nativeAvailable: boolean | null = null;\n private nativeFailedAt: number | null = null;\n private lastPollingFrame?: string;\n private readonly interfaceMjpegHub: InterfaceMjpegHub =\n createInterfaceMjpegHub({\n initialFrameTimeoutMs: INTERFACE_MJPEG_INITIAL_FRAME_TIMEOUT_MS,\n idleStopMs: INTERFACE_MJPEG_IDLE_STOP_MS,\n debug: debugMjpeg,\n });\n\n constructor(private readonly source: MjpegStreamSource) {}\n\n /** Drop the cached probe result — call this when the agent reconnects. */\n reset(): void {\n this.nativeAvailable = null;\n this.nativeFailedAt = null;\n this.lastPollingFrame = undefined;\n this.interfaceMjpegHub.stopProducer();\n }\n\n shutdown(): void {\n this.interfaceMjpegHub.shutdown();\n }\n\n getLastFrameBase64(): string | undefined {\n const interfaceFrame = this.interfaceMjpegHub.getLastFrame();\n if (interfaceFrame) {\n return toMjpegFrameDataUrl(\n interfaceFrame.data,\n interfaceFrame.contentType,\n );\n }\n return this.lastPollingFrame\n ? toMjpegFrameDataUrl(this.lastPollingFrame)\n : undefined;\n }\n\n async serve(req: Request, res: Response): Promise<void> {\n const nativeUrl = this.source.getNativeUrl();\n const recentlyFailed =\n this.nativeAvailable === false &&\n this.nativeFailedAt !== null &&\n Date.now() - this.nativeFailedAt < NEGATIVE_CACHE_MS;\n\n if (nativeUrl && !recentlyFailed) {\n const proxied = await this.probeAndProxyNative(nativeUrl, req, res);\n if (proxied) return;\n }\n\n const activeInterface = this.source.getActiveInterface();\n if (activeInterface) {\n const interfaceStreamStarted = await this.interfaceMjpegHub.streamRequest(\n req,\n res,\n activeInterface,\n async (startupError) =>\n (await this.source.recoverFromPreviewError?.(\n startupError,\n 'interface MJPEG startup',\n )) ?? null,\n );\n if (interfaceStreamStarted) return;\n }\n\n if (!this.source.canTakeScreenshot()) {\n res.status(500).json({\n error: 'Screenshot method not available on current interface',\n });\n return;\n }\n\n await this.streamPolling(req, res);\n }\n\n private probeAndProxyNative(\n nativeUrl: string,\n req: Request,\n res: Response,\n ): Promise<boolean> {\n return new Promise<boolean>((resolve) => {\n debugMjpeg(`trying native stream from ${nativeUrl}`);\n const proxyReq = http.get(nativeUrl, (proxyRes) => {\n const statusCode = proxyRes.statusCode ?? 0;\n if (statusCode >= 400) {\n this.nativeAvailable = false;\n this.nativeFailedAt = Date.now();\n proxyRes.resume();\n debugMjpeg(\n `native stream returned HTTP ${statusCode}, using polling mode`,\n );\n resolve(false);\n return;\n }\n this.nativeAvailable = true;\n this.nativeFailedAt = null;\n debugMjpeg('streaming via native WDA MJPEG server');\n const contentType = proxyRes.headers['content-type'];\n if (contentType) res.setHeader('Content-Type', contentType);\n res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');\n res.setHeader('Connection', 'keep-alive');\n proxyRes.pipe(res);\n req.on('close', () => proxyReq.destroy());\n resolve(true);\n });\n proxyReq.on('error', (err) => {\n this.nativeAvailable = false;\n this.nativeFailedAt = Date.now();\n debugMjpeg(\n `native stream unavailable (${err.message}), using polling mode`,\n );\n resolve(false);\n });\n });\n }\n\n private probeNativeLiveness(nativeUrl: string): Promise<boolean> {\n return new Promise<boolean>((resolve) => {\n const probe = http.get(nativeUrl, (probeRes) => {\n const statusCode = probeRes.statusCode ?? 0;\n const reachable = statusCode >= 200 && statusCode < 400;\n probeRes.destroy();\n resolve(reachable);\n });\n probe.setTimeout(1000, () => {\n probe.destroy();\n resolve(false);\n });\n probe.on('error', () => resolve(false));\n });\n }\n\n private async streamPolling(req: Request, res: Response): Promise<void> {\n const parsedFps = Number(req.query.fps);\n const fps = Math.min(\n Math.max(Number.isNaN(parsedFps) ? DEFAULT_FPS : parsedFps, 1),\n MAX_FPS,\n );\n const interval = Math.round(1000 / fps);\n const boundary = 'mjpeg-boundary';\n debugMjpeg(`streaming via polling mode (${fps}fps)`);\n\n res.setHeader(\n 'Content-Type',\n `multipart/x-mixed-replace; boundary=${boundary}`,\n );\n res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');\n res.setHeader('Connection', 'keep-alive');\n\n let stopped = false;\n let consecutiveErrors = 0;\n\n // While in polling mode, periodically re-probe the native URL. As soon\n // as it becomes reachable, destroy this socket so the client's <img>\n // fires onError and reconnects onto the native stream. (res.end() leaves\n // the multipart frame visually frozen in some browsers.)\n const nativeUrl = this.source.getNativeUrl();\n let probeTimer: ReturnType<typeof setInterval> | undefined;\n if (nativeUrl) {\n probeTimer = setInterval(async () => {\n if (stopped) return;\n const reachable = await this.probeNativeLiveness(nativeUrl);\n if (reachable && !stopped) {\n debugMjpeg(\n 'native stream came online, ending polling so client reconnects',\n );\n this.nativeAvailable = true;\n this.nativeFailedAt = null;\n stopped = true;\n try {\n res.destroy();\n } catch {\n /* socket already closed */\n }\n }\n }, NATIVE_PROBE_INTERVAL_MS);\n }\n req.on('close', () => {\n stopped = true;\n if (probeTimer) clearInterval(probeTimer);\n });\n\n while (!stopped) {\n if (!this.source.isAgentReady()) {\n await new Promise((r) => setTimeout(r, 200));\n continue;\n }\n\n const frameStart = Date.now();\n try {\n const base64 = await this.source.takeScreenshot();\n if (stopped) break;\n consecutiveErrors = 0;\n this.lastPollingFrame = base64;\n\n writeMjpegFrame(res, boundary, {\n data: base64,\n contentType: 'image/jpeg',\n });\n } catch (err) {\n if (stopped) break;\n const recoveredInterface = await this.source.recoverFromPreviewError?.(\n err,\n 'polling MJPEG frame capture',\n );\n if (recoveredInterface) {\n consecutiveErrors = 0;\n continue;\n }\n consecutiveErrors++;\n if (consecutiveErrors <= ERROR_LOG_THRESHOLD) {\n console.error('MJPEG frame error:', err);\n } else if (consecutiveErrors === ERROR_LOG_THRESHOLD + 1) {\n console.error(\n 'MJPEG: suppressing further errors, retrying silently...',\n );\n }\n const backoff = Math.min(\n 1000 * consecutiveErrors,\n MAX_ERROR_BACKOFF_MS,\n );\n await new Promise((r) => setTimeout(r, backoff));\n continue;\n }\n\n const elapsed = Date.now() - frameStart;\n const remaining = interval - elapsed;\n if (remaining > 0) await new Promise((r) => setTimeout(r, remaining));\n }\n if (probeTimer) clearInterval(probeTimer);\n }\n}\n"],"names":["debugMjpeg","getDebug","NEGATIVE_CACHE_MS","NATIVE_PROBE_INTERVAL_MS","INTERFACE_MJPEG_INITIAL_FRAME_TIMEOUT_MS","INTERFACE_MJPEG_IDLE_STOP_MS","DEFAULT_FPS","MAX_FPS","MAX_ERROR_BACKOFF_MS","ERROR_LOG_THRESHOLD","toMjpegFrameDataUrl","data","contentType","MjpegStreamHandler","undefined","interfaceFrame","req","res","nativeUrl","recentlyFailed","Date","proxied","activeInterface","interfaceStreamStarted","startupError","Promise","resolve","proxyReq","http","proxyRes","statusCode","err","probe","probeRes","reachable","parsedFps","Number","fps","Math","interval","boundary","stopped","consecutiveErrors","probeTimer","setInterval","clearInterval","r","setTimeout","frameStart","base64","writeMjpegFrame","recoveredInterface","console","backoff","elapsed","remaining","source","createInterfaceMjpegHub"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"platform.mjs","sources":["../../src/platform.ts"],"sourcesContent":["import type { Agent } from '@midscene/core/agent';\nimport type {\n MidsceneRecorderEvent,\n MidsceneRecorderEventType,\n MidsceneRecorderSourceKind,\n} from '@midscene/shared/recorder';\nimport type { LaunchPlaygroundOptions } from './launcher';\nimport type { AgentFactory } from './types';\n\nexport type PlaygroundPreviewKind =\n | 'none'\n | 'screenshot'\n | 'mjpeg'\n | 'scrcpy'\n | 'custom';\n\nexport interface PlaygroundPreviewCapability {\n kind: PlaygroundPreviewKind;\n label?: string;\n live?: boolean;\n}\n\nexport interface PlaygroundPreviewDescriptor {\n kind: PlaygroundPreviewKind;\n title?: string;\n capabilities?: PlaygroundPreviewCapability[];\n screenshotPath?: string;\n mjpegPath?: string;\n custom?: Record<string, unknown>;\n}\n\nexport interface PlaygroundSessionTarget {\n id: string;\n label: string;\n description?: string;\n status?: string;\n isDefault?: boolean;\n metadata?: Record<string, unknown>;\n}\n\nexport interface PlaygroundSessionFieldOption {\n label: string;\n value: string | number | boolean;\n description?: string;\n}\n\nexport interface PlaygroundPlatformRegistration {\n id: string;\n label: string;\n description?: string;\n unavailableReason?: string;\n supportsStandalone?: boolean;\n metadata?: Record<string, unknown>;\n}\n\nexport interface PlaygroundPlatformSelectorConfig {\n fieldKey: string;\n variant?: 'cards' | 'select';\n}\n\nexport interface PlaygroundSessionField {\n key: string;\n label: string;\n type: 'text' | 'number' | 'select';\n required?: boolean;\n defaultValue?: string | number | boolean;\n options?: PlaygroundSessionFieldOption[];\n placeholder?: string;\n description?: string;\n}\n\nexport interface PlaygroundSessionNotice {\n type: 'info' | 'warning' | 'error';\n message: string;\n description?: string;\n}\n\nexport interface PlaygroundSessionSetup {\n title?: string;\n description?: string;\n primaryActionLabel?: string;\n autoSubmitWhenReady?: boolean;\n fields: PlaygroundSessionField[];\n targets?: PlaygroundSessionTarget[];\n platformRegistry?: PlaygroundPlatformRegistration[];\n platformSelector?: PlaygroundPlatformSelectorConfig;\n notice?: PlaygroundSessionNotice;\n}\n\nexport interface PlaygroundExecutionHooks {\n beforeExecute?: () => void | Promise<void>;\n afterExecute?: () => void | Promise<void>;\n}\n\nexport interface PlaygroundSidecar {\n id: string;\n start(): void | Promise<void>;\n stop?(): void | Promise<void>;\n}\n\nexport type PlaygroundRecorderSourceKind = MidsceneRecorderSourceKind;\n\nexport type PlaygroundRecorderEventType = MidsceneRecorderEventType;\n\nexport type PlaygroundRecorderEvent = MidsceneRecorderEvent;\n\nexport interface PlaygroundRecorderCapabilitiesResult {\n supported: boolean;\n source: PlaygroundRecorderSourceKind;\n platformId?: string;\n error?: string;\n}\n\nexport interface PlaygroundRecorderStartResult {\n ok: boolean;\n supported?: boolean;\n source?: PlaygroundRecorderSourceKind;\n platformId?: string;\n error?: string;\n}\n\nexport interface PlaygroundRecorderEventsResult {\n events: PlaygroundRecorderEvent[];\n nextIndex: number;\n}\n\nexport interface PlaygroundRecorderDescribeTrace {\n traceId: string;\n eventHashId?: string;\n eventType?: string;\n actionType?: string;\n eventSummary?: {\n hashId?: string;\n mergedHashIds?: string[];\n type?: string;\n source?: string;\n actionType?: string;\n timestamp?: number;\n url?: string;\n title?: string;\n valueLength?: number;\n rawPayloadSummary?: Record<string, unknown>;\n elementRect?: {\n left?: number;\n top?: number;\n width?: number;\n height?: number;\n x?: number;\n y?: number;\n };\n pageInfo?: { width: number; height: number };\n };\n status: 'ready' | 'failed' | 'skipped';\n error?: string;\n startedAt: string;\n durationMs: number;\n modelCallDurationMs?: number;\n point?: [number, number];\n pageInfo?: { width: number; height: number };\n screenshotBytes?: number;\n screenshotRef?: {\n path: string;\n sha256: string;\n bytes: number;\n mimeType?: string;\n };\n annotatedScreenshotRef?: {\n path: string;\n sha256: string;\n bytes: number;\n mimeType?: string;\n };\n screenshotAnnotation?: {\n inputPoint?: {\n logical: [number, number];\n screenshot: [number, number];\n };\n sourceTargetRect?: {\n left: number;\n top: number;\n width: number;\n height: number;\n };\n locateRect?: {\n left: number;\n top: number;\n width: number;\n height: number;\n };\n centerDelta?: {\n x: number;\n y: number;\n distance: number;\n };\n distanceOutsideRect?: {\n x: number;\n y: number;\n distance: number;\n };\n };\n screenshotPersistError?: string;\n annotatedScreenshotPersistError?: string;\n elementDescription?: string;\n verifyPassed?: boolean;\n centerDistance?: number;\n verifyResult?: {\n pass?: boolean;\n rect?: {\n left: number;\n top: number;\n width: number;\n height: number;\n };\n center?: [number, number];\n centerDistance?: number;\n includedInRect?: boolean;\n };\n}\n\nexport interface PlaygroundRecorderDescribeResult {\n ok: boolean;\n event?: PlaygroundRecorderEvent;\n trace?: PlaygroundRecorderDescribeTrace;\n error?: string;\n}\n\nexport interface PlaygroundSessionState {\n connected: boolean;\n displayName?: string;\n metadata?: Record<string, unknown>;\n setupState?: 'required' | 'ready' | 'blocked';\n setupBlockingReason?: string;\n}\n\nexport interface PlaygroundCreatedSession {\n agent?: Agent;\n agentFactory?: AgentFactory;\n preview?: PlaygroundPreviewDescriptor;\n metadata?: Record<string, unknown>;\n displayName?: string;\n platformId?: string;\n title?: string;\n platformDescription?: string;\n executionHooks?: PlaygroundExecutionHooks;\n sidecars?: PlaygroundSidecar[];\n}\n\nexport interface PlaygroundSessionManager {\n getSetupSchema?(\n input?: Record<string, unknown>,\n ): Promise<PlaygroundSessionSetup>;\n listTargets?(): Promise<PlaygroundSessionTarget[]>;\n createSession(\n input?: Record<string, unknown>,\n ): Promise<PlaygroundCreatedSession>;\n destroySession?(session?: PlaygroundSessionState): Promise<void>;\n}\n\nexport interface PreparedPlaygroundPlatform {\n platformId: string;\n title: string;\n description?: string;\n agent?: Agent;\n agentFactory?: AgentFactory;\n sessionManager?: PlaygroundSessionManager;\n executionHooks?: PlaygroundExecutionHooks;\n launchOptions?: LaunchPlaygroundOptions;\n preview?: PlaygroundPreviewDescriptor;\n metadata?: Record<string, unknown>;\n sidecars?: PlaygroundSidecar[];\n}\n\nexport interface PlaygroundPlatformDescriptor<TOptions = void> {\n id: string;\n title: string;\n description?: string;\n prepare(options: TOptions): Promise<PreparedPlaygroundPlatform>;\n}\n\nexport function definePlaygroundPlatform<TOptions>(\n descriptor: PlaygroundPlatformDescriptor<TOptions>,\n): PlaygroundPlatformDescriptor<TOptions> {\n return descriptor;\n}\n\nexport function createScreenshotPreviewDescriptor(\n overrides: Partial<PlaygroundPreviewDescriptor> = {},\n): PlaygroundPreviewDescriptor {\n return {\n kind: 'screenshot',\n screenshotPath: '/screenshot',\n capabilities: [\n {\n kind: 'screenshot',\n label: 'Screenshot polling',\n live: false,\n },\n ],\n ...overrides,\n };\n}\n\nexport function createMjpegPreviewDescriptor(\n overrides: Partial<PlaygroundPreviewDescriptor> = {},\n): PlaygroundPreviewDescriptor {\n return {\n kind: 'mjpeg',\n screenshotPath: '/screenshot',\n mjpegPath: '/mjpeg',\n capabilities: [\n {\n kind: 'mjpeg',\n label: 'MJPEG streaming',\n live: true,\n },\n {\n kind: 'screenshot',\n label: 'Screenshot fallback',\n live: false,\n },\n ],\n ...overrides,\n };\n}\n\nexport function createScrcpyPreviewDescriptor(\n custom: Record<string, unknown> = {},\n overrides: Partial<PlaygroundPreviewDescriptor> = {},\n): PlaygroundPreviewDescriptor {\n return {\n kind: 'scrcpy',\n screenshotPath: '/screenshot',\n capabilities: [\n {\n kind: 'scrcpy',\n label: 'scrcpy streaming',\n live: true,\n },\n {\n kind: 'screenshot',\n label: 'Screenshot fallback',\n live: false,\n },\n ],\n custom,\n ...overrides,\n };\n}\n\nexport function resolvePreparedLaunchOptions(\n prepared: PreparedPlaygroundPlatform,\n overrides: LaunchPlaygroundOptions = {},\n): LaunchPlaygroundOptions {\n return {\n ...(prepared.launchOptions || {}),\n ...overrides,\n };\n}\n"],"names":["definePlaygroundPlatform","descriptor","createScreenshotPreviewDescriptor","overrides","createMjpegPreviewDescriptor","createScrcpyPreviewDescriptor","custom","resolvePreparedLaunchOptions","prepared"],"mappings":"AAuRO,SAASA,yBACdC,UAAkD;IAElD,OAAOA;AACT;AAEO,SAASC,kCACdC,YAAkD,CAAC,CAAC;IAEpD,OAAO;QACL,MAAM;QACN,gBAAgB;QAChB,cAAc;YACZ;gBACE,MAAM;gBACN,OAAO;gBACP,MAAM;YACR;SACD;QACD,GAAGA,SAAS;IACd;AACF;AAEO,SAASC,6BACdD,YAAkD,CAAC,CAAC;IAEpD,OAAO;QACL,MAAM;QACN,gBAAgB;QAChB,WAAW;QACX,cAAc;YACZ;gBACE,MAAM;gBACN,OAAO;gBACP,MAAM;YACR;YACA;gBACE,MAAM;gBACN,OAAO;gBACP,MAAM;YACR;SACD;QACD,GAAGA,SAAS;IACd;AACF;AAEO,SAASE,8BACdC,SAAkC,CAAC,CAAC,EACpCH,YAAkD,CAAC,CAAC;IAEpD,OAAO;QACL,MAAM;QACN,gBAAgB;QAChB,cAAc;YACZ;gBACE,MAAM;gBACN,OAAO;gBACP,MAAM;YACR;YACA;gBACE,MAAM;gBACN,OAAO;gBACP,MAAM;YACR;SACD;QACDG;QACA,GAAGH,SAAS;IACd;AACF;AAEO,SAASI,6BACdC,QAAoC,EACpCL,YAAqC,CAAC,CAAC;IAEvC,OAAO;QACL,GAAIK,SAAS,aAAa,IAAI,CAAC,CAAC;QAChC,GAAGL,SAAS;IACd;AACF"}
1
+ {"version":3,"file":"platform.mjs","sources":["../../src/platform.ts"],"sourcesContent":["import type { Agent } from '@midscene/core/agent';\nimport type {\n MidsceneRecorderEvent,\n MidsceneRecorderEventType,\n MidsceneRecorderSourceKind,\n} from '@midscene/shared/recorder';\nimport type { LaunchPlaygroundOptions } from './launcher';\nimport type { AgentFactory } from './types';\n\nexport type PlaygroundPreviewKind =\n | 'none'\n | 'screenshot'\n | 'mjpeg'\n | 'scrcpy'\n | 'custom';\n\nexport interface PlaygroundPreviewCapability {\n kind: PlaygroundPreviewKind;\n label?: string;\n live?: boolean;\n}\n\nexport interface PlaygroundPreviewDescriptor {\n kind: PlaygroundPreviewKind;\n title?: string;\n capabilities?: PlaygroundPreviewCapability[];\n screenshotPath?: string;\n mjpegPath?: string;\n custom?: Record<string, unknown>;\n}\n\nexport interface PlaygroundSessionTarget {\n id: string;\n label: string;\n description?: string;\n status?: string;\n isDefault?: boolean;\n metadata?: Record<string, unknown>;\n}\n\nexport interface PlaygroundSessionFieldOption {\n label: string;\n value: string | number | boolean;\n description?: string;\n}\n\nexport interface PlaygroundPlatformRegistration {\n id: string;\n label: string;\n description?: string;\n unavailableReason?: string;\n supportsStandalone?: boolean;\n metadata?: Record<string, unknown>;\n}\n\nexport interface PlaygroundPlatformSelectorConfig {\n fieldKey: string;\n variant?: 'cards' | 'select';\n}\n\nexport interface PlaygroundSessionField {\n key: string;\n label: string;\n type: 'text' | 'number' | 'select';\n required?: boolean;\n defaultValue?: string | number | boolean;\n options?: PlaygroundSessionFieldOption[];\n placeholder?: string;\n description?: string;\n}\n\nexport interface PlaygroundSessionNotice {\n type: 'info' | 'warning' | 'error';\n message: string;\n description?: string;\n}\n\nexport interface PlaygroundSessionSetup {\n title?: string;\n description?: string;\n primaryActionLabel?: string;\n autoSubmitWhenReady?: boolean;\n fields: PlaygroundSessionField[];\n targets?: PlaygroundSessionTarget[];\n platformRegistry?: PlaygroundPlatformRegistration[];\n platformSelector?: PlaygroundPlatformSelectorConfig;\n notice?: PlaygroundSessionNotice;\n}\n\nexport interface PlaygroundExecutionHooks {\n beforeExecute?: () => void | Promise<void>;\n afterExecute?: () => void | Promise<void>;\n}\n\nexport interface PlaygroundSidecar {\n id: string;\n start(): void | Promise<void>;\n stop?(): void | Promise<void>;\n}\n\nexport type PlaygroundRecorderSourceKind = MidsceneRecorderSourceKind;\n\nexport type PlaygroundRecorderEventType = MidsceneRecorderEventType;\n\nexport type PlaygroundRecorderEvent = MidsceneRecorderEvent;\n\nexport interface PlaygroundRecorderCapabilitiesResult {\n supported: boolean;\n source: PlaygroundRecorderSourceKind;\n platformId?: string;\n error?: string;\n}\n\nexport interface PlaygroundRecorderStartResult {\n ok: boolean;\n supported?: boolean;\n source?: PlaygroundRecorderSourceKind;\n platformId?: string;\n error?: string;\n}\n\nexport interface PlaygroundRecorderEventsResult {\n events: PlaygroundRecorderEvent[];\n nextIndex: number;\n}\n\nexport interface PlaygroundRecorderDescribeTrace {\n traceId: string;\n eventHashId?: string;\n eventType?: string;\n actionType?: string;\n eventSummary?: {\n hashId?: string;\n mergedHashIds?: string[];\n type?: string;\n source?: string;\n actionType?: string;\n timestamp?: number;\n url?: string;\n title?: string;\n valueLength?: number;\n rawPayloadSummary?: Record<string, unknown>;\n elementRect?: {\n left?: number;\n top?: number;\n width?: number;\n height?: number;\n x?: number;\n y?: number;\n };\n pageInfo?: { width: number; height: number };\n };\n status: 'ready' | 'failed' | 'skipped';\n error?: string;\n startedAt: string;\n durationMs: number;\n modelCallDurationMs?: number;\n point?: [number, number];\n pageInfo?: { width: number; height: number };\n screenshotBytes?: number;\n screenshotRef?: {\n path: string;\n sha256: string;\n bytes: number;\n mimeType?: string;\n };\n annotatedScreenshotRef?: {\n path: string;\n sha256: string;\n bytes: number;\n mimeType?: string;\n };\n screenshotAnnotation?: {\n inputPoint?: {\n logical: [number, number];\n screenshot: [number, number];\n };\n sourceTargetRect?: {\n left: number;\n top: number;\n width: number;\n height: number;\n };\n locateRect?: {\n left: number;\n top: number;\n width: number;\n height: number;\n };\n centerDelta?: {\n x: number;\n y: number;\n distance: number;\n };\n distanceOutsideRect?: {\n x: number;\n y: number;\n distance: number;\n };\n };\n screenshotPersistError?: string;\n annotatedScreenshotPersistError?: string;\n elementDescription?: string;\n verifyPrompt?: boolean;\n verifyPassed?: boolean;\n centerDistance?: number;\n verifyResult?: {\n pass?: boolean;\n rect?: {\n left: number;\n top: number;\n width: number;\n height: number;\n };\n center?: [number, number];\n centerDistance?: number;\n includedInRect?: boolean;\n };\n}\n\nexport interface PlaygroundRecorderDescribeResult {\n ok: boolean;\n event?: PlaygroundRecorderEvent;\n trace?: PlaygroundRecorderDescribeTrace;\n error?: string;\n}\n\nexport interface PlaygroundSessionState {\n connected: boolean;\n displayName?: string;\n metadata?: Record<string, unknown>;\n setupState?: 'required' | 'ready' | 'blocked';\n setupBlockingReason?: string;\n}\n\nexport interface PlaygroundCreatedSession {\n agent?: Agent;\n agentFactory?: AgentFactory;\n preview?: PlaygroundPreviewDescriptor;\n metadata?: Record<string, unknown>;\n displayName?: string;\n platformId?: string;\n title?: string;\n platformDescription?: string;\n executionHooks?: PlaygroundExecutionHooks;\n sidecars?: PlaygroundSidecar[];\n}\n\nexport interface PlaygroundSessionManager {\n getSetupSchema?(\n input?: Record<string, unknown>,\n ): Promise<PlaygroundSessionSetup>;\n listTargets?(): Promise<PlaygroundSessionTarget[]>;\n createSession(\n input?: Record<string, unknown>,\n ): Promise<PlaygroundCreatedSession>;\n destroySession?(session?: PlaygroundSessionState): Promise<void>;\n}\n\nexport interface PreparedPlaygroundPlatform {\n platformId: string;\n title: string;\n description?: string;\n agent?: Agent;\n agentFactory?: AgentFactory;\n sessionManager?: PlaygroundSessionManager;\n executionHooks?: PlaygroundExecutionHooks;\n launchOptions?: LaunchPlaygroundOptions;\n preview?: PlaygroundPreviewDescriptor;\n metadata?: Record<string, unknown>;\n sidecars?: PlaygroundSidecar[];\n}\n\nexport interface PlaygroundPlatformDescriptor<TOptions = void> {\n id: string;\n title: string;\n description?: string;\n prepare(options: TOptions): Promise<PreparedPlaygroundPlatform>;\n}\n\nexport function definePlaygroundPlatform<TOptions>(\n descriptor: PlaygroundPlatformDescriptor<TOptions>,\n): PlaygroundPlatformDescriptor<TOptions> {\n return descriptor;\n}\n\nexport function createScreenshotPreviewDescriptor(\n overrides: Partial<PlaygroundPreviewDescriptor> = {},\n): PlaygroundPreviewDescriptor {\n return {\n kind: 'screenshot',\n screenshotPath: '/screenshot',\n capabilities: [\n {\n kind: 'screenshot',\n label: 'Screenshot polling',\n live: false,\n },\n ],\n ...overrides,\n };\n}\n\nexport function createMjpegPreviewDescriptor(\n overrides: Partial<PlaygroundPreviewDescriptor> = {},\n): PlaygroundPreviewDescriptor {\n return {\n kind: 'mjpeg',\n screenshotPath: '/screenshot',\n mjpegPath: '/mjpeg',\n capabilities: [\n {\n kind: 'mjpeg',\n label: 'MJPEG streaming',\n live: true,\n },\n {\n kind: 'screenshot',\n label: 'Screenshot fallback',\n live: false,\n },\n ],\n ...overrides,\n };\n}\n\nexport function createScrcpyPreviewDescriptor(\n custom: Record<string, unknown> = {},\n overrides: Partial<PlaygroundPreviewDescriptor> = {},\n): PlaygroundPreviewDescriptor {\n return {\n kind: 'scrcpy',\n screenshotPath: '/screenshot',\n capabilities: [\n {\n kind: 'scrcpy',\n label: 'scrcpy streaming',\n live: true,\n },\n {\n kind: 'screenshot',\n label: 'Screenshot fallback',\n live: false,\n },\n ],\n custom,\n ...overrides,\n };\n}\n\nexport function resolvePreparedLaunchOptions(\n prepared: PreparedPlaygroundPlatform,\n overrides: LaunchPlaygroundOptions = {},\n): LaunchPlaygroundOptions {\n return {\n ...(prepared.launchOptions || {}),\n ...overrides,\n };\n}\n"],"names":["definePlaygroundPlatform","descriptor","createScreenshotPreviewDescriptor","overrides","createMjpegPreviewDescriptor","createScrcpyPreviewDescriptor","custom","resolvePreparedLaunchOptions","prepared"],"mappings":"AAwRO,SAASA,yBACdC,UAAkD;IAElD,OAAOA;AACT;AAEO,SAASC,kCACdC,YAAkD,CAAC,CAAC;IAEpD,OAAO;QACL,MAAM;QACN,gBAAgB;QAChB,cAAc;YACZ;gBACE,MAAM;gBACN,OAAO;gBACP,MAAM;YACR;SACD;QACD,GAAGA,SAAS;IACd;AACF;AAEO,SAASC,6BACdD,YAAkD,CAAC,CAAC;IAEpD,OAAO;QACL,MAAM;QACN,gBAAgB;QAChB,WAAW;QACX,cAAc;YACZ;gBACE,MAAM;gBACN,OAAO;gBACP,MAAM;YACR;YACA;gBACE,MAAM;gBACN,OAAO;gBACP,MAAM;YACR;SACD;QACD,GAAGA,SAAS;IACd;AACF;AAEO,SAASE,8BACdC,SAAkC,CAAC,CAAC,EACpCH,YAAkD,CAAC,CAAC;IAEpD,OAAO;QACL,MAAM;QACN,gBAAgB;QAChB,cAAc;YACZ;gBACE,MAAM;gBACN,OAAO;gBACP,MAAM;YACR;YACA;gBACE,MAAM;gBACN,OAAO;gBACP,MAAM;YACR;SACD;QACDG;QACA,GAAGH,SAAS;IACd;AACF;AAEO,SAASI,6BACdC,QAAoC,EACpCL,YAAqC,CAAC,CAAC;IAEvC,OAAO;QACL,GAAIK,SAAS,aAAa,IAAI,CAAC,CAAC;QAChC,GAAGL,SAAS;IACd;AACF"}
@@ -151,6 +151,16 @@ class PlaygroundSDK {
151
151
  error: 'Recorder aiDescribe requires remote execution'
152
152
  };
153
153
  }
154
+ async getRecorderScreenshotAsset(assetId) {
155
+ if (this.adapter instanceof RemoteExecutionAdapter) return this.adapter.getRecorderScreenshotAsset(assetId);
156
+ return null;
157
+ }
158
+ async clearRecorderScreenshotAssets(sessionId) {
159
+ if (this.adapter instanceof RemoteExecutionAdapter) await this.adapter.clearRecorderScreenshotAssets(sessionId);
160
+ }
161
+ async pruneRecorderScreenshotAssets(sessionId, assetIds) {
162
+ if (this.adapter instanceof RemoteExecutionAdapter) await this.adapter.pruneRecorderScreenshotAssets(sessionId, assetIds);
163
+ }
154
164
  async getInterfaceInfo() {
155
165
  const adapter = this.runtimeMetadataAdapter();
156
166
  if (!adapter) return null;