@midscene/playground 1.9.6 → 1.9.7

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 (55) hide show
  1. package/dist/es/adapters/local-execution.mjs +3 -3
  2. package/dist/es/adapters/local-execution.mjs.map +1 -1
  3. package/dist/es/adapters/remote-execution.mjs +35 -0
  4. package/dist/es/adapters/remote-execution.mjs.map +1 -1
  5. package/dist/es/common.mjs +30 -3
  6. package/dist/es/common.mjs.map +1 -1
  7. package/dist/es/index.browser.mjs.map +1 -1
  8. package/dist/es/mjpeg-hub.mjs +3 -0
  9. package/dist/es/mjpeg-hub.mjs.map +1 -1
  10. package/dist/es/mjpeg-stream-handler.mjs +6 -0
  11. package/dist/es/mjpeg-stream-handler.mjs.map +1 -1
  12. package/dist/es/platform.mjs.map +1 -1
  13. package/dist/es/sdk/index.mjs +7 -0
  14. package/dist/es/sdk/index.mjs.map +1 -1
  15. package/dist/es/server.mjs +563 -42
  16. package/dist/es/server.mjs.map +1 -1
  17. package/dist/lib/adapters/local-execution.js +3 -3
  18. package/dist/lib/adapters/local-execution.js.map +1 -1
  19. package/dist/lib/adapters/remote-execution.js +35 -0
  20. package/dist/lib/adapters/remote-execution.js.map +1 -1
  21. package/dist/lib/common.js +30 -3
  22. package/dist/lib/common.js.map +1 -1
  23. package/dist/lib/index.browser.js.map +1 -1
  24. package/dist/lib/mjpeg-hub.js +3 -0
  25. package/dist/lib/mjpeg-hub.js.map +1 -1
  26. package/dist/lib/mjpeg-stream-handler.js +6 -0
  27. package/dist/lib/mjpeg-stream-handler.js.map +1 -1
  28. package/dist/lib/platform.js.map +1 -1
  29. package/dist/lib/sdk/index.js +7 -0
  30. package/dist/lib/sdk/index.js.map +1 -1
  31. package/dist/lib/server.js +560 -39
  32. package/dist/lib/server.js.map +1 -1
  33. package/dist/types/adapters/local-execution.d.ts +3 -2
  34. package/dist/types/adapters/remote-execution.d.ts +2 -1
  35. package/dist/types/index.browser.d.ts +1 -1
  36. package/dist/types/index.d.ts +1 -1
  37. package/dist/types/mjpeg-hub.d.ts +1 -0
  38. package/dist/types/mjpeg-stream-handler.d.ts +2 -0
  39. package/dist/types/platform.d.ts +104 -0
  40. package/dist/types/sdk/index.d.ts +3 -2
  41. package/dist/types/server.d.ts +9 -0
  42. package/dist/types/types.d.ts +5 -0
  43. package/package.json +3 -3
  44. package/static/index.html +1 -1
  45. package/static/static/css/{index.ab28104a.css → index.1293237f.css} +2 -2
  46. package/static/static/css/{index.ab28104a.css.map → index.1293237f.css.map} +1 -1
  47. package/static/static/js/{596.5426be9e.js → 905.8d12588e.js} +17 -17
  48. package/static/static/js/905.8d12588e.js.map +1 -0
  49. package/static/static/js/index.50e06ed5.js +948 -0
  50. package/static/static/js/index.50e06ed5.js.map +1 -0
  51. package/static/static/js/596.5426be9e.js.map +0 -1
  52. package/static/static/js/index.134d5099.js +0 -940
  53. package/static/static/js/index.134d5099.js.map +0 -1
  54. /package/static/static/js/{596.5426be9e.js.LICENSE.txt → 905.8d12588e.js.LICENSE.txt} +0 -0
  55. /package/static/static/js/{index.134d5099.js.LICENSE.txt → index.50e06ed5.js.LICENSE.txt} +0 -0
@@ -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 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\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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"}
@@ -26,11 +26,15 @@ class MjpegStreamHandler {
26
26
  reset() {
27
27
  this.nativeAvailable = null;
28
28
  this.nativeFailedAt = null;
29
+ this.lastPollingFrame = void 0;
29
30
  this.interfaceMjpegHub.stopProducer();
30
31
  }
31
32
  shutdown() {
32
33
  this.interfaceMjpegHub.shutdown();
33
34
  }
35
+ getLastFrameBase64() {
36
+ return this.interfaceMjpegHub.getLastFrame()?.data || this.lastPollingFrame;
37
+ }
34
38
  async serve(req, res) {
35
39
  const nativeUrl = this.source.getNativeUrl();
36
40
  const recentlyFailed = false === this.nativeAvailable && null !== this.nativeFailedAt && Date.now() - this.nativeFailedAt < NEGATIVE_CACHE_MS;
@@ -135,6 +139,7 @@ class MjpegStreamHandler {
135
139
  const base64 = await this.source.takeScreenshot();
136
140
  if (stopped) break;
137
141
  consecutiveErrors = 0;
142
+ this.lastPollingFrame = base64;
138
143
  writeMjpegFrame(res, boundary, {
139
144
  data: base64,
140
145
  contentType: 'image/jpeg'
@@ -163,6 +168,7 @@ class MjpegStreamHandler {
163
168
  _define_property(this, "source", void 0);
164
169
  _define_property(this, "nativeAvailable", void 0);
165
170
  _define_property(this, "nativeFailedAt", void 0);
171
+ _define_property(this, "lastPollingFrame", void 0);
166
172
  _define_property(this, "interfaceMjpegHub", void 0);
167
173
  this.source = source;
168
174
  this.nativeAvailable = null;
@@ -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 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.interfaceMjpegHub.stopProducer();\n }\n\n shutdown(): void {\n this.interfaceMjpegHub.shutdown();\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\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","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\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 +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 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":"AAmLO,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 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"}
@@ -144,6 +144,13 @@ class PlaygroundSDK {
144
144
  nextIndex: since
145
145
  };
146
146
  }
147
+ async describeRecorderEventAtPoint(event) {
148
+ if (this.adapter instanceof RemoteExecutionAdapter) return this.adapter.describeRecorderEventAtPoint(event);
149
+ return {
150
+ ok: false,
151
+ error: 'Recorder aiDescribe requires remote execution'
152
+ };
153
+ }
147
154
  async getInterfaceInfo() {
148
155
  const adapter = this.runtimeMetadataAdapter();
149
156
  if (!adapter) return null;
@@ -1 +1 @@
1
- {"version":3,"file":"sdk/index.mjs","sources":["../../../src/sdk/index.ts"],"sourcesContent":["import type { ConnectivityTestResult, DeviceAction } from '@midscene/core';\nimport { PLAYGROUND_SERVER_PORT } from '@midscene/shared/constants';\nimport type { BasePlaygroundAdapter } from '../adapters/base';\nimport { LocalExecutionAdapter } from '../adapters/local-execution';\nimport { RemoteExecutionAdapter } from '../adapters/remote-execution';\nimport type {\n PlaygroundRecorderCapabilitiesResult,\n PlaygroundRecorderEvent,\n PlaygroundRecorderEventsResult,\n PlaygroundRecorderStartResult,\n PlaygroundSessionSetup,\n PlaygroundSessionState,\n PlaygroundSessionTarget,\n} from '../platform';\nimport type { PlaygroundRuntimeInfo } from '../runtime-metadata';\nimport type {\n AgentFactory,\n BeforeActionHook,\n ExecutionOptions,\n FormValue,\n PlaygroundAgent,\n PlaygroundConfig,\n ValidationResult,\n} from '../types';\n\nexport type PlaygroundInteractPayload = {\n actionType: string;\n} & Record<string, unknown>;\n\nexport interface PlaygroundInteractResult {\n ok: boolean;\n error?: string;\n}\n\nexport type PlaygroundPageRecordedEvent = PlaygroundRecorderEvent;\nexport type {\n PlaygroundRecorderCapabilitiesResult,\n PlaygroundRecorderEventsResult,\n PlaygroundRecorderStartResult,\n};\n\nexport class PlaygroundSDK {\n private adapter: BasePlaygroundAdapter;\n private beforeActionHook?: BeforeActionHook;\n\n constructor(config: PlaygroundConfig) {\n this.adapter = this.createAdapter(\n config.type,\n config.serverUrl,\n config.agent,\n config.agentFactory,\n );\n }\n\n private createAdapter(\n type: string,\n serverUrl?: string,\n agent?: PlaygroundAgent,\n agentFactory?: AgentFactory,\n ): BasePlaygroundAdapter {\n switch (type) {\n case 'local-execution':\n if (!agent && !agentFactory) {\n throw new Error(\n 'Agent or agentFactory is required for local execution',\n );\n }\n return new LocalExecutionAdapter(agent, agentFactory);\n case 'remote-execution': {\n // Use provided serverUrl first, then fallback to localhost if current page origin is file:// or default\n const finalServerUrl =\n serverUrl ||\n (typeof window !== 'undefined' &&\n window.location.protocol.includes('http')\n ? window.location.origin\n : `http://localhost:${PLAYGROUND_SERVER_PORT}`);\n\n return new RemoteExecutionAdapter(finalServerUrl);\n }\n default:\n throw new Error(`Unsupported execution type: ${type}`);\n }\n }\n\n private runtimeMetadataAdapter():\n | LocalExecutionAdapter\n | RemoteExecutionAdapter\n | null {\n if (\n this.adapter instanceof LocalExecutionAdapter ||\n this.adapter instanceof RemoteExecutionAdapter\n ) {\n return this.adapter;\n }\n\n return null;\n }\n\n async executeAction(\n actionType: string,\n value: FormValue,\n options: ExecutionOptions,\n ): Promise<unknown> {\n await this.beforeActionHook?.(actionType, value, options);\n const result = await this.adapter.executeAction(actionType, value, options);\n return result;\n }\n\n setBeforeActionHook(hook?: BeforeActionHook): void {\n this.beforeActionHook = hook;\n }\n\n async getActionSpace(context?: unknown): Promise<DeviceAction<unknown>[]> {\n // Both adapters now accept context parameter\n // Local will prioritize internal agent, Remote will use server + fallback\n return this.adapter.getActionSpace(context);\n }\n\n validateStructuredParams(\n value: FormValue,\n action: DeviceAction<unknown> | undefined,\n ): ValidationResult {\n return this.adapter.validateParams(value, action);\n }\n\n formatErrorMessage(error: any): string {\n return this.adapter.formatErrorMessage(error);\n }\n\n createDisplayContent(\n value: FormValue,\n needsStructuredParams: boolean,\n action: DeviceAction<unknown> | undefined,\n ): string {\n return this.adapter.createDisplayContent(\n value,\n needsStructuredParams,\n action,\n );\n }\n\n // Get adapter ID (works for both remote and local execution)\n get id(): string | undefined {\n if (this.adapter instanceof RemoteExecutionAdapter) {\n return this.adapter.id;\n }\n if (this.adapter instanceof LocalExecutionAdapter) {\n return this.adapter.id;\n }\n return undefined;\n }\n\n // Server communication methods (for remote execution)\n async checkStatus(): Promise<boolean> {\n if (this.adapter instanceof RemoteExecutionAdapter) {\n return this.adapter.checkStatus();\n }\n return true; // For local execution, always return true\n }\n\n async overrideConfig(aiConfig: any): Promise<void> {\n return this.adapter.overrideConfig(aiConfig);\n }\n\n async runConnectivityTest(): Promise<ConnectivityTestResult> {\n return this.adapter.runConnectivityTest();\n }\n\n // Get task progress (for remote execution)\n async getTaskProgress(requestId: string): Promise<{ executionDump?: any }> {\n if (this.adapter instanceof RemoteExecutionAdapter) {\n return this.adapter.getTaskProgress(requestId);\n }\n // For local execution, progress is handled via onDumpUpdate callback\n return {};\n }\n\n // Cancel task (for remote execution)\n async cancelTask(requestId: string): Promise<any> {\n if (this.adapter instanceof RemoteExecutionAdapter) {\n return this.adapter.cancelTask(requestId);\n }\n return { error: 'Cancel task not supported in local execution mode' };\n }\n\n // Dump update callback management\n onDumpUpdate(callback: (dump: string, executionDump?: any) => void): void {\n if (this.adapter instanceof LocalExecutionAdapter) {\n this.adapter.onDumpUpdate(callback);\n } else if (this.adapter instanceof RemoteExecutionAdapter) {\n this.adapter.onDumpUpdate(callback);\n }\n }\n\n // Progress update callback management\n onProgressUpdate(callback: (tip: string) => void): void {\n if (this.adapter instanceof LocalExecutionAdapter) {\n this.adapter.setProgressCallback(callback);\n }\n // RemoteExecutionAdapter uses polling mechanism via onDumpUpdate, no separate progress callback needed\n }\n\n // Cancel execution - supports both remote and local\n async cancelExecution(requestId: string): Promise<{\n dump: any | null;\n reportHTML: string | null;\n } | null> {\n if (this.adapter instanceof RemoteExecutionAdapter) {\n const result = await this.adapter.cancelTask(requestId);\n // Return dump and reportHTML if available from cancellation\n if (result.success) {\n return {\n dump: (result as any).dump || null,\n reportHTML: (result as any).reportHTML || null,\n };\n }\n } else if (this.adapter instanceof LocalExecutionAdapter) {\n // Invoke adapter cancellation to destroy the agent and block further actions\n const result = await this.adapter.cancelTask(requestId);\n if (result.success) {\n return {\n dump: (result as any).dump || null,\n reportHTML: (result as any).reportHTML || null,\n };\n }\n }\n return null;\n }\n\n // Get current execution data (dump and report)\n async getCurrentExecutionData(): Promise<{\n dump: any | null;\n reportHTML: string | null;\n }> {\n if (\n this.adapter instanceof LocalExecutionAdapter &&\n this.adapter.getCurrentExecutionData\n ) {\n return await this.adapter.getCurrentExecutionData();\n }\n // For remote execution or if method not available, return empty data\n return { dump: null, reportHTML: null };\n }\n\n // Screenshot method for remote execution\n async getScreenshot(): Promise<{\n screenshot: string;\n timestamp: number;\n } | null> {\n if (this.adapter instanceof RemoteExecutionAdapter) {\n return this.adapter.getScreenshot();\n }\n return null; // For local execution, not supported yet\n }\n\n // Direct device manipulation – mouse/keyboard input from UI overlays.\n async interact(\n payload: PlaygroundInteractPayload,\n ): Promise<PlaygroundInteractResult> {\n if (this.adapter instanceof RemoteExecutionAdapter) {\n return this.adapter.interact(payload);\n }\n return { ok: false, error: 'Direct interaction requires remote execution' };\n }\n\n async startRecorderSession(\n sessionId: string,\n ): Promise<PlaygroundRecorderStartResult> {\n if (this.adapter instanceof RemoteExecutionAdapter) {\n return this.adapter.startRecorderSession(sessionId);\n }\n return {\n ok: false,\n supported: false,\n error: 'Recorder requires remote execution',\n };\n }\n\n async getRecorderCapabilities(): Promise<PlaygroundRecorderCapabilitiesResult> {\n if (this.adapter instanceof RemoteExecutionAdapter) {\n return this.adapter.getRecorderCapabilities();\n }\n return {\n supported: false,\n source: 'unsupported',\n error: 'Recorder requires remote execution',\n };\n }\n\n async stopRecorderSession(): Promise<PlaygroundInteractResult> {\n if (this.adapter instanceof RemoteExecutionAdapter) {\n return this.adapter.stopRecorderSession();\n }\n return { ok: true };\n }\n\n async getRecorderEvents(since = 0): Promise<PlaygroundRecorderEventsResult> {\n if (this.adapter instanceof RemoteExecutionAdapter) {\n return this.adapter.getRecorderEvents(since);\n }\n return { events: [], nextIndex: since };\n }\n\n // Get interface information (type and description)\n async getInterfaceInfo(): Promise<{\n type: string;\n description?: string;\n size?: { width: number; height: number };\n navigationState?: { isLoading: boolean };\n /** Action names exposed by the connected device's actionSpace. */\n actionTypes?: string[];\n } | null> {\n const adapter = this.runtimeMetadataAdapter();\n if (!adapter) {\n return null;\n }\n\n return adapter.getInterfaceInfo();\n }\n\n async getRuntimeInfo(): Promise<PlaygroundRuntimeInfo | null> {\n const adapter = this.runtimeMetadataAdapter();\n if (!adapter) {\n return null;\n }\n\n return adapter.getRuntimeInfo();\n }\n\n async getSessionInfo(): Promise<PlaygroundSessionState | null> {\n if (this.adapter instanceof RemoteExecutionAdapter) {\n return this.adapter.getSessionInfo();\n }\n\n return null;\n }\n\n async getSessionSetup(\n input?: Record<string, unknown>,\n ): Promise<PlaygroundSessionSetup | null> {\n if (this.adapter instanceof RemoteExecutionAdapter) {\n return this.adapter.getSessionSetup(input);\n }\n\n return null;\n }\n\n async listSessionTargets(): Promise<PlaygroundSessionTarget[]> {\n if (this.adapter instanceof RemoteExecutionAdapter) {\n return this.adapter.listSessionTargets();\n }\n\n return [];\n }\n\n async createSession(input?: Record<string, unknown>): Promise<{\n session: PlaygroundSessionState;\n runtimeInfo: PlaygroundRuntimeInfo;\n }> {\n if (this.adapter instanceof RemoteExecutionAdapter) {\n return this.adapter.createSession(input);\n }\n\n throw new Error('Session creation is only supported in server mode');\n }\n\n async destroySession(): Promise<{\n session: PlaygroundSessionState;\n runtimeInfo: PlaygroundRuntimeInfo;\n }> {\n if (this.adapter instanceof RemoteExecutionAdapter) {\n return this.adapter.destroySession();\n }\n\n throw new Error('Session destruction is only supported in server mode');\n }\n\n // Get service mode based on adapter type\n getServiceMode(): 'In-Browser-Extension' | 'Server' {\n if (this.adapter instanceof LocalExecutionAdapter) {\n return 'In-Browser-Extension';\n }\n return 'Server';\n }\n}\n"],"names":["PlaygroundSDK","type","serverUrl","agent","agentFactory","Error","LocalExecutionAdapter","finalServerUrl","window","PLAYGROUND_SERVER_PORT","RemoteExecutionAdapter","actionType","value","options","result","hook","context","action","error","needsStructuredParams","aiConfig","requestId","callback","payload","sessionId","since","adapter","input","config"],"mappings":"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"}
1
+ {"version":3,"file":"sdk/index.mjs","sources":["../../../src/sdk/index.ts"],"sourcesContent":["import type { ConnectivityTestResult, DeviceAction } from '@midscene/core';\nimport { PLAYGROUND_SERVER_PORT } from '@midscene/shared/constants';\nimport type { BasePlaygroundAdapter } from '../adapters/base';\nimport { LocalExecutionAdapter } from '../adapters/local-execution';\nimport { RemoteExecutionAdapter } from '../adapters/remote-execution';\nimport type {\n PlaygroundRecorderCapabilitiesResult,\n PlaygroundRecorderDescribeResult,\n PlaygroundRecorderEvent,\n PlaygroundRecorderEventsResult,\n PlaygroundRecorderStartResult,\n PlaygroundSessionSetup,\n PlaygroundSessionState,\n PlaygroundSessionTarget,\n} from '../platform';\nimport type { PlaygroundRuntimeInfo } from '../runtime-metadata';\nimport type {\n AgentFactory,\n BeforeActionHook,\n ExecutionOptions,\n FormValue,\n PlaygroundAgent,\n PlaygroundConfig,\n ValidationResult,\n} from '../types';\n\nexport type PlaygroundInteractPayload = {\n actionType: string;\n} & Record<string, unknown>;\n\nexport interface PlaygroundInteractResult {\n ok: boolean;\n error?: string;\n}\n\nexport type PlaygroundPageRecordedEvent = PlaygroundRecorderEvent;\nexport type {\n PlaygroundRecorderCapabilitiesResult,\n PlaygroundRecorderDescribeResult,\n PlaygroundRecorderEventsResult,\n PlaygroundRecorderStartResult,\n};\n\nexport class PlaygroundSDK {\n private adapter: BasePlaygroundAdapter;\n private beforeActionHook?: BeforeActionHook;\n\n constructor(config: PlaygroundConfig) {\n this.adapter = this.createAdapter(\n config.type,\n config.serverUrl,\n config.agent,\n config.agentFactory,\n );\n }\n\n private createAdapter(\n type: string,\n serverUrl?: string,\n agent?: PlaygroundAgent,\n agentFactory?: AgentFactory,\n ): BasePlaygroundAdapter {\n switch (type) {\n case 'local-execution':\n if (!agent && !agentFactory) {\n throw new Error(\n 'Agent or agentFactory is required for local execution',\n );\n }\n return new LocalExecutionAdapter(agent, agentFactory);\n case 'remote-execution': {\n // Use provided serverUrl first, then fallback to localhost if current page origin is file:// or default\n const finalServerUrl =\n serverUrl ||\n (typeof window !== 'undefined' &&\n window.location.protocol.includes('http')\n ? window.location.origin\n : `http://localhost:${PLAYGROUND_SERVER_PORT}`);\n\n return new RemoteExecutionAdapter(finalServerUrl);\n }\n default:\n throw new Error(`Unsupported execution type: ${type}`);\n }\n }\n\n private runtimeMetadataAdapter():\n | LocalExecutionAdapter\n | RemoteExecutionAdapter\n | null {\n if (\n this.adapter instanceof LocalExecutionAdapter ||\n this.adapter instanceof RemoteExecutionAdapter\n ) {\n return this.adapter;\n }\n\n return null;\n }\n\n async executeAction(\n actionType: string,\n value: FormValue,\n options: ExecutionOptions,\n ): Promise<unknown> {\n await this.beforeActionHook?.(actionType, value, options);\n const result = await this.adapter.executeAction(actionType, value, options);\n return result;\n }\n\n setBeforeActionHook(hook?: BeforeActionHook): void {\n this.beforeActionHook = hook;\n }\n\n async getActionSpace(context?: unknown): Promise<DeviceAction<unknown>[]> {\n // Both adapters now accept context parameter\n // Local will prioritize internal agent, Remote will use server + fallback\n return this.adapter.getActionSpace(context);\n }\n\n validateStructuredParams(\n value: FormValue,\n action: DeviceAction<unknown> | undefined,\n ): ValidationResult {\n return this.adapter.validateParams(value, action);\n }\n\n formatErrorMessage(error: any): string {\n return this.adapter.formatErrorMessage(error);\n }\n\n createDisplayContent(\n value: FormValue,\n needsStructuredParams: boolean,\n action: DeviceAction<unknown> | undefined,\n ): string {\n return this.adapter.createDisplayContent(\n value,\n needsStructuredParams,\n action,\n );\n }\n\n // Get adapter ID (works for both remote and local execution)\n get id(): string | undefined {\n if (this.adapter instanceof RemoteExecutionAdapter) {\n return this.adapter.id;\n }\n if (this.adapter instanceof LocalExecutionAdapter) {\n return this.adapter.id;\n }\n return undefined;\n }\n\n // Server communication methods (for remote execution)\n async checkStatus(): Promise<boolean> {\n if (this.adapter instanceof RemoteExecutionAdapter) {\n return this.adapter.checkStatus();\n }\n return true; // For local execution, always return true\n }\n\n async overrideConfig(aiConfig: any): Promise<void> {\n return this.adapter.overrideConfig(aiConfig);\n }\n\n async runConnectivityTest(): Promise<ConnectivityTestResult> {\n return this.adapter.runConnectivityTest();\n }\n\n // Get task progress (for remote execution)\n async getTaskProgress(requestId: string): Promise<{ executionDump?: any }> {\n if (this.adapter instanceof RemoteExecutionAdapter) {\n return this.adapter.getTaskProgress(requestId);\n }\n // For local execution, progress is handled via onDumpUpdate callback\n return {};\n }\n\n // Cancel task (for remote execution)\n async cancelTask(requestId: string): Promise<any> {\n if (this.adapter instanceof RemoteExecutionAdapter) {\n return this.adapter.cancelTask(requestId);\n }\n return { error: 'Cancel task not supported in local execution mode' };\n }\n\n // Dump update callback management\n onDumpUpdate(callback: (dump: string, executionDump?: any) => void): void {\n if (this.adapter instanceof LocalExecutionAdapter) {\n this.adapter.onDumpUpdate(callback);\n } else if (this.adapter instanceof RemoteExecutionAdapter) {\n this.adapter.onDumpUpdate(callback);\n }\n }\n\n // Progress update callback management\n onProgressUpdate(callback: (tip: string) => void): void {\n if (this.adapter instanceof LocalExecutionAdapter) {\n this.adapter.setProgressCallback(callback);\n }\n // RemoteExecutionAdapter uses polling mechanism via onDumpUpdate, no separate progress callback needed\n }\n\n // Cancel execution - supports both remote and local\n async cancelExecution(requestId: string): Promise<{\n dump: any | null;\n reportHTML: string | null;\n } | null> {\n if (this.adapter instanceof RemoteExecutionAdapter) {\n const result = await this.adapter.cancelTask(requestId);\n // Return dump and reportHTML if available from cancellation\n if (result.success) {\n return {\n dump: (result as any).dump || null,\n reportHTML: (result as any).reportHTML || null,\n };\n }\n } else if (this.adapter instanceof LocalExecutionAdapter) {\n // Invoke adapter cancellation to destroy the agent and block further actions\n const result = await this.adapter.cancelTask(requestId);\n if (result.success) {\n return {\n dump: (result as any).dump || null,\n reportHTML: (result as any).reportHTML || null,\n };\n }\n }\n return null;\n }\n\n // Get current execution data (dump and report)\n async getCurrentExecutionData(): Promise<{\n dump: any | null;\n reportHTML: string | null;\n }> {\n if (\n this.adapter instanceof LocalExecutionAdapter &&\n this.adapter.getCurrentExecutionData\n ) {\n return await this.adapter.getCurrentExecutionData();\n }\n // For remote execution or if method not available, return empty data\n return { dump: null, reportHTML: null };\n }\n\n // Screenshot method for remote execution\n async getScreenshot(): Promise<{\n screenshot: string;\n timestamp: number;\n } | null> {\n if (this.adapter instanceof RemoteExecutionAdapter) {\n return this.adapter.getScreenshot();\n }\n return null; // For local execution, not supported yet\n }\n\n // Direct device manipulation – mouse/keyboard input from UI overlays.\n async interact(\n payload: PlaygroundInteractPayload,\n ): Promise<PlaygroundInteractResult> {\n if (this.adapter instanceof RemoteExecutionAdapter) {\n return this.adapter.interact(payload);\n }\n return { ok: false, error: 'Direct interaction requires remote execution' };\n }\n\n async startRecorderSession(\n sessionId: string,\n ): Promise<PlaygroundRecorderStartResult> {\n if (this.adapter instanceof RemoteExecutionAdapter) {\n return this.adapter.startRecorderSession(sessionId);\n }\n return {\n ok: false,\n supported: false,\n error: 'Recorder requires remote execution',\n };\n }\n\n async getRecorderCapabilities(): Promise<PlaygroundRecorderCapabilitiesResult> {\n if (this.adapter instanceof RemoteExecutionAdapter) {\n return this.adapter.getRecorderCapabilities();\n }\n return {\n supported: false,\n source: 'unsupported',\n error: 'Recorder requires remote execution',\n };\n }\n\n async stopRecorderSession(): Promise<PlaygroundInteractResult> {\n if (this.adapter instanceof RemoteExecutionAdapter) {\n return this.adapter.stopRecorderSession();\n }\n return { ok: true };\n }\n\n async getRecorderEvents(since = 0): Promise<PlaygroundRecorderEventsResult> {\n if (this.adapter instanceof RemoteExecutionAdapter) {\n return this.adapter.getRecorderEvents(since);\n }\n return { events: [], nextIndex: since };\n }\n\n async describeRecorderEventAtPoint(\n event: PlaygroundRecorderEvent,\n ): Promise<PlaygroundRecorderDescribeResult> {\n if (this.adapter instanceof RemoteExecutionAdapter) {\n return this.adapter.describeRecorderEventAtPoint(event);\n }\n return {\n ok: false,\n error: 'Recorder aiDescribe requires remote execution',\n };\n }\n\n // Get interface information (type and description)\n async getInterfaceInfo(): Promise<{\n type: string;\n description?: string;\n size?: { width: number; height: number };\n navigationState?: { isLoading: boolean };\n /** Action names exposed by the connected device's actionSpace. */\n actionTypes?: string[];\n } | null> {\n const adapter = this.runtimeMetadataAdapter();\n if (!adapter) {\n return null;\n }\n\n return adapter.getInterfaceInfo();\n }\n\n async getRuntimeInfo(): Promise<PlaygroundRuntimeInfo | null> {\n const adapter = this.runtimeMetadataAdapter();\n if (!adapter) {\n return null;\n }\n\n return adapter.getRuntimeInfo();\n }\n\n async getSessionInfo(): Promise<PlaygroundSessionState | null> {\n if (this.adapter instanceof RemoteExecutionAdapter) {\n return this.adapter.getSessionInfo();\n }\n\n return null;\n }\n\n async getSessionSetup(\n input?: Record<string, unknown>,\n ): Promise<PlaygroundSessionSetup | null> {\n if (this.adapter instanceof RemoteExecutionAdapter) {\n return this.adapter.getSessionSetup(input);\n }\n\n return null;\n }\n\n async listSessionTargets(): Promise<PlaygroundSessionTarget[]> {\n if (this.adapter instanceof RemoteExecutionAdapter) {\n return this.adapter.listSessionTargets();\n }\n\n return [];\n }\n\n async createSession(input?: Record<string, unknown>): Promise<{\n session: PlaygroundSessionState;\n runtimeInfo: PlaygroundRuntimeInfo;\n }> {\n if (this.adapter instanceof RemoteExecutionAdapter) {\n return this.adapter.createSession(input);\n }\n\n throw new Error('Session creation is only supported in server mode');\n }\n\n async destroySession(): Promise<{\n session: PlaygroundSessionState;\n runtimeInfo: PlaygroundRuntimeInfo;\n }> {\n if (this.adapter instanceof RemoteExecutionAdapter) {\n return this.adapter.destroySession();\n }\n\n throw new Error('Session destruction is only supported in server mode');\n }\n\n // Get service mode based on adapter type\n getServiceMode(): 'In-Browser-Extension' | 'Server' {\n if (this.adapter instanceof LocalExecutionAdapter) {\n return 'In-Browser-Extension';\n }\n return 'Server';\n }\n}\n"],"names":["PlaygroundSDK","type","serverUrl","agent","agentFactory","Error","LocalExecutionAdapter","finalServerUrl","window","PLAYGROUND_SERVER_PORT","RemoteExecutionAdapter","actionType","value","options","result","hook","context","action","error","needsStructuredParams","aiConfig","requestId","callback","payload","sessionId","since","event","adapter","input","config"],"mappings":";;;;;;;;;;;;;AA2CO,MAAMA;IAaH,cACNC,IAAY,EACZC,SAAkB,EAClBC,KAAuB,EACvBC,YAA2B,EACJ;QACvB,OAAQH;YACN,KAAK;gBACH,IAAI,CAACE,SAAS,CAACC,cACb,MAAM,IAAIC,MACR;gBAGJ,OAAO,IAAIC,sBAAsBH,OAAOC;YAC1C,KAAK;gBAAoB;oBAEvB,MAAMG,iBACJL,aACC,CAAkB,eAAlB,OAAOM,UACRA,OAAO,QAAQ,CAAC,QAAQ,CAAC,QAAQ,CAAC,UAC9BA,OAAO,QAAQ,CAAC,MAAM,GACtB,CAAC,iBAAiB,EAAEC,wBAAuB;oBAEjD,OAAO,IAAIC,uBAAuBH;gBACpC;YACA;gBACE,MAAM,IAAIF,MAAM,CAAC,4BAA4B,EAAEJ,MAAM;QACzD;IACF;IAEQ,yBAGC;QACP,IACE,IAAI,CAAC,OAAO,YAAYK,yBACxB,IAAI,CAAC,OAAO,YAAYI,wBAExB,OAAO,IAAI,CAAC,OAAO;QAGrB,OAAO;IACT;IAEA,MAAM,cACJC,UAAkB,EAClBC,KAAgB,EAChBC,OAAyB,EACP;QAClB,MAAM,IAAI,CAAC,gBAAgB,GAAGF,YAAYC,OAAOC;QACjD,MAAMC,SAAS,MAAM,IAAI,CAAC,OAAO,CAAC,aAAa,CAACH,YAAYC,OAAOC;QACnE,OAAOC;IACT;IAEA,oBAAoBC,IAAuB,EAAQ;QACjD,IAAI,CAAC,gBAAgB,GAAGA;IAC1B;IAEA,MAAM,eAAeC,OAAiB,EAAoC;QAGxE,OAAO,IAAI,CAAC,OAAO,CAAC,cAAc,CAACA;IACrC;IAEA,yBACEJ,KAAgB,EAChBK,MAAyC,EACvB;QAClB,OAAO,IAAI,CAAC,OAAO,CAAC,cAAc,CAACL,OAAOK;IAC5C;IAEA,mBAAmBC,KAAU,EAAU;QACrC,OAAO,IAAI,CAAC,OAAO,CAAC,kBAAkB,CAACA;IACzC;IAEA,qBACEN,KAAgB,EAChBO,qBAA8B,EAC9BF,MAAyC,EACjC;QACR,OAAO,IAAI,CAAC,OAAO,CAAC,oBAAoB,CACtCL,OACAO,uBACAF;IAEJ;IAGA,IAAI,KAAyB;QAC3B,IAAI,IAAI,CAAC,OAAO,YAAYP,wBAC1B,OAAO,IAAI,CAAC,OAAO,CAAC,EAAE;QAExB,IAAI,IAAI,CAAC,OAAO,YAAYJ,uBAC1B,OAAO,IAAI,CAAC,OAAO,CAAC,EAAE;IAG1B;IAGA,MAAM,cAAgC;QACpC,IAAI,IAAI,CAAC,OAAO,YAAYI,wBAC1B,OAAO,IAAI,CAAC,OAAO,CAAC,WAAW;QAEjC,OAAO;IACT;IAEA,MAAM,eAAeU,QAAa,EAAiB;QACjD,OAAO,IAAI,CAAC,OAAO,CAAC,cAAc,CAACA;IACrC;IAEA,MAAM,sBAAuD;QAC3D,OAAO,IAAI,CAAC,OAAO,CAAC,mBAAmB;IACzC;IAGA,MAAM,gBAAgBC,SAAiB,EAAoC;QACzE,IAAI,IAAI,CAAC,OAAO,YAAYX,wBAC1B,OAAO,IAAI,CAAC,OAAO,CAAC,eAAe,CAACW;QAGtC,OAAO,CAAC;IACV;IAGA,MAAM,WAAWA,SAAiB,EAAgB;QAChD,IAAI,IAAI,CAAC,OAAO,YAAYX,wBAC1B,OAAO,IAAI,CAAC,OAAO,CAAC,UAAU,CAACW;QAEjC,OAAO;YAAE,OAAO;QAAoD;IACtE;IAGA,aAAaC,QAAqD,EAAQ;QACxE,IAAI,IAAI,CAAC,OAAO,YAAYhB,uBAC1B,IAAI,CAAC,OAAO,CAAC,YAAY,CAACgB;aACrB,IAAI,IAAI,CAAC,OAAO,YAAYZ,wBACjC,IAAI,CAAC,OAAO,CAAC,YAAY,CAACY;IAE9B;IAGA,iBAAiBA,QAA+B,EAAQ;QACtD,IAAI,IAAI,CAAC,OAAO,YAAYhB,uBAC1B,IAAI,CAAC,OAAO,CAAC,mBAAmB,CAACgB;IAGrC;IAGA,MAAM,gBAAgBD,SAAiB,EAG7B;QACR,IAAI,IAAI,CAAC,OAAO,YAAYX,wBAAwB;YAClD,MAAMI,SAAS,MAAM,IAAI,CAAC,OAAO,CAAC,UAAU,CAACO;YAE7C,IAAIP,OAAO,OAAO,EAChB,OAAO;gBACL,MAAOA,OAAe,IAAI,IAAI;gBAC9B,YAAaA,OAAe,UAAU,IAAI;YAC5C;QAEJ,OAAO,IAAI,IAAI,CAAC,OAAO,YAAYR,uBAAuB;YAExD,MAAMQ,SAAS,MAAM,IAAI,CAAC,OAAO,CAAC,UAAU,CAACO;YAC7C,IAAIP,OAAO,OAAO,EAChB,OAAO;gBACL,MAAOA,OAAe,IAAI,IAAI;gBAC9B,YAAaA,OAAe,UAAU,IAAI;YAC5C;QAEJ;QACA,OAAO;IACT;IAGA,MAAM,0BAGH;QACD,IACE,IAAI,CAAC,OAAO,YAAYR,yBACxB,IAAI,CAAC,OAAO,CAAC,uBAAuB,EAEpC,OAAO,MAAM,IAAI,CAAC,OAAO,CAAC,uBAAuB;QAGnD,OAAO;YAAE,MAAM;YAAM,YAAY;QAAK;IACxC;IAGA,MAAM,gBAGI;QACR,IAAI,IAAI,CAAC,OAAO,YAAYI,wBAC1B,OAAO,IAAI,CAAC,OAAO,CAAC,aAAa;QAEnC,OAAO;IACT;IAGA,MAAM,SACJa,OAAkC,EACC;QACnC,IAAI,IAAI,CAAC,OAAO,YAAYb,wBAC1B,OAAO,IAAI,CAAC,OAAO,CAAC,QAAQ,CAACa;QAE/B,OAAO;YAAE,IAAI;YAAO,OAAO;QAA+C;IAC5E;IAEA,MAAM,qBACJC,SAAiB,EACuB;QACxC,IAAI,IAAI,CAAC,OAAO,YAAYd,wBAC1B,OAAO,IAAI,CAAC,OAAO,CAAC,oBAAoB,CAACc;QAE3C,OAAO;YACL,IAAI;YACJ,WAAW;YACX,OAAO;QACT;IACF;IAEA,MAAM,0BAAyE;QAC7E,IAAI,IAAI,CAAC,OAAO,YAAYd,wBAC1B,OAAO,IAAI,CAAC,OAAO,CAAC,uBAAuB;QAE7C,OAAO;YACL,WAAW;YACX,QAAQ;YACR,OAAO;QACT;IACF;IAEA,MAAM,sBAAyD;QAC7D,IAAI,IAAI,CAAC,OAAO,YAAYA,wBAC1B,OAAO,IAAI,CAAC,OAAO,CAAC,mBAAmB;QAEzC,OAAO;YAAE,IAAI;QAAK;IACpB;IAEA,MAAM,kBAAkBe,QAAQ,CAAC,EAA2C;QAC1E,IAAI,IAAI,CAAC,OAAO,YAAYf,wBAC1B,OAAO,IAAI,CAAC,OAAO,CAAC,iBAAiB,CAACe;QAExC,OAAO;YAAE,QAAQ,EAAE;YAAE,WAAWA;QAAM;IACxC;IAEA,MAAM,6BACJC,KAA8B,EACa;QAC3C,IAAI,IAAI,CAAC,OAAO,YAAYhB,wBAC1B,OAAO,IAAI,CAAC,OAAO,CAAC,4BAA4B,CAACgB;QAEnD,OAAO;YACL,IAAI;YACJ,OAAO;QACT;IACF;IAGA,MAAM,mBAOI;QACR,MAAMC,UAAU,IAAI,CAAC,sBAAsB;QAC3C,IAAI,CAACA,SACH,OAAO;QAGT,OAAOA,QAAQ,gBAAgB;IACjC;IAEA,MAAM,iBAAwD;QAC5D,MAAMA,UAAU,IAAI,CAAC,sBAAsB;QAC3C,IAAI,CAACA,SACH,OAAO;QAGT,OAAOA,QAAQ,cAAc;IAC/B;IAEA,MAAM,iBAAyD;QAC7D,IAAI,IAAI,CAAC,OAAO,YAAYjB,wBAC1B,OAAO,IAAI,CAAC,OAAO,CAAC,cAAc;QAGpC,OAAO;IACT;IAEA,MAAM,gBACJkB,KAA+B,EACS;QACxC,IAAI,IAAI,CAAC,OAAO,YAAYlB,wBAC1B,OAAO,IAAI,CAAC,OAAO,CAAC,eAAe,CAACkB;QAGtC,OAAO;IACT;IAEA,MAAM,qBAAyD;QAC7D,IAAI,IAAI,CAAC,OAAO,YAAYlB,wBAC1B,OAAO,IAAI,CAAC,OAAO,CAAC,kBAAkB;QAGxC,OAAO,EAAE;IACX;IAEA,MAAM,cAAckB,KAA+B,EAGhD;QACD,IAAI,IAAI,CAAC,OAAO,YAAYlB,wBAC1B,OAAO,IAAI,CAAC,OAAO,CAAC,aAAa,CAACkB;QAGpC,MAAM,IAAIvB,MAAM;IAClB;IAEA,MAAM,iBAGH;QACD,IAAI,IAAI,CAAC,OAAO,YAAYK,wBAC1B,OAAO,IAAI,CAAC,OAAO,CAAC,cAAc;QAGpC,MAAM,IAAIL,MAAM;IAClB;IAGA,iBAAoD;QAClD,IAAI,IAAI,CAAC,OAAO,YAAYC,uBAC1B,OAAO;QAET,OAAO;IACT;IA9VA,YAAYuB,MAAwB,CAAE;QAHtC,uBAAQ,WAAR;QACA,uBAAQ,oBAAR;QAGE,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,aAAa,CAC/BA,OAAO,IAAI,EACXA,OAAO,SAAS,EAChBA,OAAO,KAAK,EACZA,OAAO,YAAY;IAEvB;AAwVF"}