@midscene/playground 1.8.0 → 1.8.1

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.
@@ -44,8 +44,6 @@ __webpack_require__.d(__webpack_exports__, {
44
44
  buildInteractParams: ()=>buildInteractParams
45
45
  });
46
46
  const external_node_fs_namespaceObject = require("node:fs");
47
- const external_node_http_namespaceObject = require("node:http");
48
- var external_node_http_default = /*#__PURE__*/ __webpack_require__.n(external_node_http_namespaceObject);
49
47
  const external_node_path_namespaceObject = require("node:path");
50
48
  const external_node_url_namespaceObject = require("node:url");
51
49
  const core_namespaceObject = require("@midscene/core");
@@ -58,7 +56,8 @@ const shared_utils_namespaceObject = require("@midscene/shared/utils");
58
56
  const external_express_namespaceObject = require("express");
59
57
  var external_express_default = /*#__PURE__*/ __webpack_require__.n(external_express_namespaceObject);
60
58
  const external_common_js_namespaceObject = require("./common.js");
61
- const external_mjpeg_hub_js_namespaceObject = require("./mjpeg-hub.js");
59
+ const external_mjpeg_stream_handler_js_namespaceObject = require("./mjpeg-stream-handler.js");
60
+ const external_pointer_dispatch_js_namespaceObject = require("./pointer-dispatch.js");
62
61
  const external_runtime_metadata_js_namespaceObject = require("./runtime-metadata.js");
63
62
  require("dotenv/config");
64
63
  function _define_property(obj, key, value) {
@@ -128,6 +127,19 @@ function locateFromPoint(x, y, fieldX, fieldY, description) {
128
127
  Math.round(requireNumber(y, fieldY))
129
128
  ], description);
130
129
  }
130
+ const POINTER_INTERACT_ACTIONS = new Set([
131
+ 'Tap',
132
+ 'DoubleClick',
133
+ 'LongPress',
134
+ 'Swipe',
135
+ 'DragAndDrop',
136
+ 'KeyboardPress',
137
+ 'Input',
138
+ 'Pinch'
139
+ ]);
140
+ function isPointerInteractActionType(actionType) {
141
+ return POINTER_INTERACT_ACTIONS.has(actionType);
142
+ }
131
143
  const buildLocateActionParams = (body, actionType)=>{
132
144
  const params = {
133
145
  locate: locateFromPoint(body.x, body.y, 'x', 'y', `manual ${actionType}`)
@@ -218,6 +230,10 @@ function isRecoverablePageSessionError(error) {
218
230
  return RECOVERABLE_PAGE_SESSION_ERROR_PATTERN.test(message);
219
231
  }
220
232
  class PlaygroundServer {
233
+ setActiveAgent(agent) {
234
+ this._activeConnection.agent = agent;
235
+ this._mjpegHandler.reset();
236
+ }
221
237
  get agent() {
222
238
  return this._activeConnection.agent;
223
239
  }
@@ -270,6 +286,7 @@ class PlaygroundServer {
270
286
  executionHooks: this._baseExecutionHooks,
271
287
  sidecars: this._baseSidecars
272
288
  };
289
+ this._mjpegHandler.reset();
273
290
  this.syncRuntimeState();
274
291
  }
275
292
  setPreparedPlatform(prepared) {
@@ -365,7 +382,7 @@ class PlaygroundServer {
365
382
  } catch (error) {
366
383
  console.warn('Failed to destroy old agent:', error);
367
384
  } finally{
368
- this._activeConnection.agent = null;
385
+ this.setActiveAgent(null);
369
386
  this._configDirty = false;
370
387
  }
371
388
  }
@@ -404,6 +421,7 @@ class PlaygroundServer {
404
421
  executionHooks: session.executionHooks || this._baseExecutionHooks,
405
422
  sidecars: sessionSidecars
406
423
  };
424
+ this._mjpegHandler.reset();
407
425
  this.sessionSetupState = 'ready';
408
426
  this.sessionSetupBlockingReason = void 0;
409
427
  this.syncRuntimeState();
@@ -461,7 +479,7 @@ class PlaygroundServer {
461
479
  console.log('Recreating agent to cancel current task...');
462
480
  await this.destroyCurrentAgent();
463
481
  if (this._activeConnection.agentFactory) try {
464
- this._activeConnection.agent = await this._activeConnection.agentFactory();
482
+ this.setActiveAgent(await this._activeConnection.agentFactory());
465
483
  this._agentReady = true;
466
484
  console.log('Agent recreated successfully');
467
485
  } catch (error) {
@@ -478,7 +496,7 @@ class PlaygroundServer {
478
496
  if (!this._activeConnection.agentFactory || !isRecoverablePageSessionError(error)) return null;
479
497
  debugMjpeg(`Recovering active agent after ${reason}:`, error);
480
498
  try {
481
- this._interfaceMjpegHub.stopProducer();
499
+ this._mjpegHandler.reset();
482
500
  await this.recreateAgent();
483
501
  return this._activeConnection.agent;
484
502
  } catch (recreateError) {
@@ -552,7 +570,7 @@ class PlaygroundServer {
552
570
  await this.destroyCurrentSession();
553
571
  const created = await this.sessionManager.createSession(req.body || {});
554
572
  await this.applyCreatedSession(created);
555
- if (!this._activeConnection.agent && this._activeConnection.agentFactory) this._activeConnection.agent = await this._activeConnection.agentFactory();
573
+ if (!this._activeConnection.agent && this._activeConnection.agentFactory) this.setActiveAgent(await this._activeConnection.agentFactory());
556
574
  if (this._configDirty && this._activeConnection.agentFactory) {
557
575
  this._configDirty = false;
558
576
  await this.recreateAgent();
@@ -684,7 +702,7 @@ class PlaygroundServer {
684
702
  console.log('AI config changed, recreating agent...');
685
703
  try {
686
704
  await this.destroyCurrentAgent();
687
- this._activeConnection.agent = await this._activeConnection.agentFactory();
705
+ this.setActiveAgent(await this._activeConnection.agentFactory());
688
706
  agent = this.getActiveAgentOrThrow();
689
707
  this._agentReady = true;
690
708
  console.log('Agent recreated with new config');
@@ -854,19 +872,7 @@ class PlaygroundServer {
854
872
  if (!agent) return res.status(409).json({
855
873
  error: 'No active session'
856
874
  });
857
- const nativeUrl = agent.interface?.mjpegStreamUrl;
858
- const recentlyFailed = false === this._nativeMjpegAvailable && null !== this._nativeMjpegFailedAt && Date.now() - this._nativeMjpegFailedAt < PlaygroundServer.MJPEG_NEGATIVE_CACHE_MS;
859
- if (nativeUrl && !recentlyFailed) {
860
- const proxyOk = await this.probeAndProxyNativeMjpeg(nativeUrl, req, res);
861
- if (proxyOk) return;
862
- }
863
- const interfaceStreamStarted = await this._interfaceMjpegHub.streamRequest(req, res, agent.interface, async (startupError)=>(await this.recoverActiveAgentAfterPreviewError(startupError, 'interface MJPEG startup'))?.interface ?? null);
864
- if (interfaceStreamStarted) return;
865
- const fallbackAgent = this._activeConnection.agent;
866
- if ('function' != typeof fallbackAgent?.interface?.screenshotBase64) return res.status(500).json({
867
- error: 'Screenshot method not available on current interface'
868
- });
869
- await this.startPollingMjpegStream(req, res);
875
+ await this._mjpegHandler.serve(req, res);
870
876
  });
871
877
  this._app.get('/interface-info', async (_req, res)=>{
872
878
  try {
@@ -925,26 +931,26 @@ class PlaygroundServer {
925
931
  if ('string' != typeof actionType || !actionType) return res.status(400).json({
926
932
  error: 'actionType is required'
927
933
  });
928
- if (!this.findInteractAction(agent, actionType) && !this.canRunBrowserChromeInteractAction(agent, actionType)) return res.status(404).json({
929
- error: `Action "${actionType}" is not available on the current device`
930
- });
931
- let params;
932
934
  try {
933
- params = buildInteractParams(actionType, req.body ?? {});
934
- } catch (error) {
935
- if (error instanceof InteractParamsValidationError) return res.status(400).json({
936
- error: error.message
937
- });
938
- const errorMessage = error instanceof Error ? error.message : 'Unknown error';
939
- console.error(`Failed to build interact params for "${actionType}": ${errorMessage}`);
940
- return res.status(500).json({
941
- error: errorMessage
935
+ const inputPrimitives = agent.interface.inputPrimitives;
936
+ if (inputPrimitives) {
937
+ await (0, external_pointer_dispatch_js_namespaceObject.dispatchPointer)(inputPrimitives, req.body ?? {}, ()=>agent.interface.size());
938
+ res.json({});
939
+ return;
940
+ }
941
+ if (!this.findInteractAction(agent, actionType) && !this.canRunBrowserChromeInteractAction(agent, actionType)) return res.status(404).json({
942
+ error: isPointerInteractActionType(actionType) ? 'Manual control is not supported on this device' : `Action "${actionType}" is not available on the current device`
942
943
  });
943
- }
944
- try {
944
+ const params = buildInteractParams(actionType, req.body ?? {});
945
945
  await this.runInteractAction(agent, actionType, params);
946
946
  res.json({});
947
947
  } catch (error) {
948
+ if (error instanceof external_pointer_dispatch_js_namespaceObject.PointerInputError) return res.status(error.statusCode).json({
949
+ error: error.message
950
+ });
951
+ if (error instanceof InteractParamsValidationError) return res.status(400).json({
952
+ error: error.message
953
+ });
948
954
  const recoveredAgent = await this.recoverActiveAgentAfterPreviewError(error, `manual interact action "${actionType}"`);
949
955
  if (recoveredAgent) return res.status(409).json({
950
956
  error: 'The page session was closed and has been recreated. Please retry the action.'
@@ -1026,123 +1032,6 @@ class PlaygroundServer {
1026
1032
  }
1027
1033
  });
1028
1034
  }
1029
- probeAndProxyNativeMjpeg(nativeUrl, req, res) {
1030
- return new Promise((resolve)=>{
1031
- console.log(`MJPEG: trying native stream from ${nativeUrl}`);
1032
- const proxyReq = external_node_http_default().get(nativeUrl, (proxyRes)=>{
1033
- const statusCode = proxyRes.statusCode ?? 0;
1034
- if (statusCode >= 400) {
1035
- this._nativeMjpegAvailable = false;
1036
- this._nativeMjpegFailedAt = Date.now();
1037
- proxyRes.resume();
1038
- debugMjpeg(`native stream returned HTTP ${statusCode}, using polling mode`);
1039
- resolve(false);
1040
- return;
1041
- }
1042
- this._nativeMjpegAvailable = true;
1043
- this._nativeMjpegFailedAt = null;
1044
- console.log('MJPEG: streaming via native WDA MJPEG server');
1045
- const contentType = proxyRes.headers['content-type'];
1046
- if (contentType) res.setHeader('Content-Type', contentType);
1047
- res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
1048
- res.setHeader('Connection', 'keep-alive');
1049
- proxyRes.pipe(res);
1050
- req.on('close', ()=>proxyReq.destroy());
1051
- resolve(true);
1052
- });
1053
- proxyReq.on('error', (err)=>{
1054
- this._nativeMjpegAvailable = false;
1055
- this._nativeMjpegFailedAt = Date.now();
1056
- debugMjpeg(`MJPEG: native stream unavailable (${err.message}), using polling mode`);
1057
- resolve(false);
1058
- });
1059
- });
1060
- }
1061
- probeNativeMjpegLiveness(nativeUrl) {
1062
- return new Promise((resolve)=>{
1063
- const probe = external_node_http_default().get(nativeUrl, (probeRes)=>{
1064
- const statusCode = probeRes.statusCode ?? 0;
1065
- const reachable = statusCode >= 200 && statusCode < 400;
1066
- probeRes.destroy();
1067
- resolve(reachable);
1068
- });
1069
- probe.setTimeout(1000, ()=>{
1070
- probe.destroy();
1071
- resolve(false);
1072
- });
1073
- probe.on('error', ()=>resolve(false));
1074
- });
1075
- }
1076
- async startPollingMjpegStream(req, res) {
1077
- const defaultMjpegFps = 10;
1078
- const maxMjpegFps = 30;
1079
- const maxErrorBackoffMs = 3000;
1080
- const errorLogThreshold = 3;
1081
- const nativeProbeIntervalMs = 3000;
1082
- const parsedFps = Number(req.query.fps);
1083
- const fps = Math.min(Math.max(Number.isNaN(parsedFps) ? defaultMjpegFps : parsedFps, 1), maxMjpegFps);
1084
- const interval = Math.round(1000 / fps);
1085
- const boundary = 'mjpeg-boundary';
1086
- console.log(`MJPEG: streaming via polling mode (${fps}fps)`);
1087
- res.setHeader('Content-Type', `multipart/x-mixed-replace; boundary=${boundary}`);
1088
- res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
1089
- res.setHeader('Connection', 'keep-alive');
1090
- let stopped = false;
1091
- let consecutiveErrors = 0;
1092
- const nativeUrl = this._activeConnection.agent?.interface?.mjpegStreamUrl;
1093
- let probeTimer;
1094
- if (nativeUrl) probeTimer = setInterval(async ()=>{
1095
- if (stopped) return;
1096
- const reachable = await this.probeNativeMjpegLiveness(nativeUrl);
1097
- if (reachable && !stopped) {
1098
- console.log('MJPEG: native stream came online, ending polling so client reconnects');
1099
- this._nativeMjpegAvailable = true;
1100
- this._nativeMjpegFailedAt = null;
1101
- stopped = true;
1102
- try {
1103
- res.destroy();
1104
- } catch {}
1105
- }
1106
- }, nativeProbeIntervalMs);
1107
- req.on('close', ()=>{
1108
- stopped = true;
1109
- if (probeTimer) clearInterval(probeTimer);
1110
- });
1111
- while(!stopped){
1112
- if (!this._agentReady) {
1113
- await new Promise((r)=>setTimeout(r, 200));
1114
- continue;
1115
- }
1116
- const frameStart = Date.now();
1117
- try {
1118
- const agent = this.getActiveAgentOrThrow();
1119
- const base64 = await agent.interface.screenshotBase64();
1120
- if (stopped) break;
1121
- consecutiveErrors = 0;
1122
- (0, external_mjpeg_hub_js_namespaceObject.writeMjpegFrame)(res, boundary, {
1123
- data: base64,
1124
- contentType: 'image/jpeg'
1125
- });
1126
- } catch (err) {
1127
- if (stopped) break;
1128
- const recoveredAgent = await this.recoverActiveAgentAfterPreviewError(err, 'polling MJPEG frame capture');
1129
- if (recoveredAgent) {
1130
- consecutiveErrors = 0;
1131
- continue;
1132
- }
1133
- consecutiveErrors++;
1134
- if (consecutiveErrors <= errorLogThreshold) console.error('MJPEG frame error:', err);
1135
- else if (consecutiveErrors === errorLogThreshold + 1) console.error('MJPEG: suppressing further errors, retrying silently...');
1136
- const backoff = Math.min(1000 * consecutiveErrors, maxErrorBackoffMs);
1137
- await new Promise((r)=>setTimeout(r, backoff));
1138
- continue;
1139
- }
1140
- const elapsed = Date.now() - frameStart;
1141
- const remaining = interval - elapsed;
1142
- if (remaining > 0) await new Promise((r)=>setTimeout(r, remaining));
1143
- }
1144
- if (probeTimer) clearInterval(probeTimer);
1145
- }
1146
1035
  setupStaticRoutes() {
1147
1036
  this._app.get('/', (_req, res)=>{
1148
1037
  this.serveHtmlWithPorts(res);
@@ -1176,7 +1065,7 @@ class PlaygroundServer {
1176
1065
  async launch(port) {
1177
1066
  if (this._activeConnection.agentFactory && !this.sessionManager) {
1178
1067
  console.log('Initializing agent from factory function...');
1179
- this._activeConnection.agent = await this._activeConnection.agentFactory();
1068
+ this.setActiveAgent(await this._activeConnection.agentFactory());
1180
1069
  this._activeConnection.session = {
1181
1070
  connected: true,
1182
1071
  metadata: {}
@@ -1198,6 +1087,7 @@ class PlaygroundServer {
1198
1087
  await this.destroyCurrentSession().catch((error)=>{
1199
1088
  console.warn('Failed to destroy current session during shutdown:', error);
1200
1089
  });
1090
+ this._mjpegHandler.shutdown();
1201
1091
  return new Promise((resolve, reject)=>{
1202
1092
  if (this.server) {
1203
1093
  this.taskExecutionDumps = {};
@@ -1221,12 +1111,13 @@ class PlaygroundServer {
1221
1111
  _define_property(this, "id", void 0);
1222
1112
  _define_property(this, "scrcpyPort", void 0);
1223
1113
  _define_property(this, "_initialized", false);
1224
- _define_property(this, "_nativeMjpegAvailable", null);
1225
- _define_property(this, "_nativeMjpegFailedAt", null);
1226
- _define_property(this, "_interfaceMjpegHub", (0, external_mjpeg_hub_js_namespaceObject.createInterfaceMjpegHub)({
1227
- initialFrameTimeoutMs: PlaygroundServer.INTERFACE_MJPEG_INITIAL_FRAME_TIMEOUT_MS,
1228
- idleStopMs: PlaygroundServer.INTERFACE_MJPEG_IDLE_STOP_MS,
1229
- debug: debugMjpeg
1114
+ _define_property(this, "_mjpegHandler", new external_mjpeg_stream_handler_js_namespaceObject.MjpegStreamHandler({
1115
+ getNativeUrl: ()=>this._activeConnection.agent?.interface?.mjpegStreamUrl,
1116
+ getActiveInterface: ()=>this._activeConnection.agent?.interface ?? null,
1117
+ takeScreenshot: ()=>this.getActiveAgentOrThrow().interface.screenshotBase64(),
1118
+ canTakeScreenshot: ()=>'function' == typeof this._activeConnection.agent?.interface?.screenshotBase64,
1119
+ isAgentReady: ()=>this._agentReady,
1120
+ recoverFromPreviewError: async (error, reason)=>(await this.recoverActiveAgentAfterPreviewError(error, reason))?.interface ?? null
1230
1121
  }));
1231
1122
  _define_property(this, "sessionManager", void 0);
1232
1123
  _define_property(this, "sessionSetupState", 'ready');
@@ -1253,12 +1144,9 @@ class PlaygroundServer {
1253
1144
  this.taskExecutionDumps = {};
1254
1145
  this.id = id || (0, shared_utils_namespaceObject.uuid)();
1255
1146
  if ('function' == typeof agent) this._activeConnection.agentFactory = agent;
1256
- else this._activeConnection.agent = agent || null;
1147
+ else this.setActiveAgent(agent || null);
1257
1148
  }
1258
1149
  }
1259
- _define_property(PlaygroundServer, "MJPEG_NEGATIVE_CACHE_MS", 10000);
1260
- _define_property(PlaygroundServer, "INTERFACE_MJPEG_INITIAL_FRAME_TIMEOUT_MS", 1500);
1261
- _define_property(PlaygroundServer, "INTERFACE_MJPEG_IDLE_STOP_MS", 2000);
1262
1150
  const server = PlaygroundServer;
1263
1151
  exports.InteractParamsValidationError = __webpack_exports__.InteractParamsValidationError;
1264
1152
  exports.PlaygroundServer = __webpack_exports__.PlaygroundServer;