@midscene/android 1.10.6-beta-20260716090839.0 → 1.10.6

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.
package/dist/es/cli.mjs CHANGED
@@ -14,7 +14,7 @@ import { createDefaultMobileActions, defineAction } from "@midscene/core/device"
14
14
  import { getTmpFile, sleep } from "@midscene/core/utils";
15
15
  import { MIDSCENE_ADB_PATH, MIDSCENE_ADB_REMOTE_HOST, MIDSCENE_ADB_REMOTE_PORT, MIDSCENE_ANDROID_IME_STRATEGY, globalConfigManager } from "@midscene/shared/env";
16
16
  import { createImgBase64ByFormat, validateScreenshotBuffer } from "@midscene/shared/img";
17
- import { ADB as external_appium_adb_ADB } from "appium-adb";
17
+ import { ADB, getSdkRootFromEnv } from "appium-adb";
18
18
  var __webpack_modules__ = {
19
19
  "./src/scrcpy-manager.ts" (__unused_rspack_module, __webpack_exports__, __webpack_require__) {
20
20
  __webpack_require__.d(__webpack_exports__, {
@@ -52,6 +52,8 @@ var __webpack_modules__ = {
52
52
  const KEYFRAME_POLL_INTERVAL_MS = 200;
53
53
  const MAX_SCAN_BYTES = 1000;
54
54
  const CONNECTION_WAIT_MS = 1000;
55
+ const MAX_SERVER_OUTPUT_LINES = 100;
56
+ const SERVER_OUTPUT_DRAIN_TIMEOUT_MS = 500;
55
57
  const BUSY_LOOP_WINDOW_MS = 1000;
56
58
  const BUSY_LOOP_MAX_READS = 500;
57
59
  const BUSY_LOOP_COOLDOWN_MS = 50;
@@ -92,6 +94,8 @@ var __webpack_modules__ = {
92
94
  if (this.scrcpyClient && this.videoStream) return void this.resetIdleTimer();
93
95
  throw new Error('Scrcpy connection failed: another connection attempt did not complete in time');
94
96
  }
97
+ const serverOutput = [];
98
+ let serverOutputTask = null;
95
99
  try {
96
100
  this.isConnecting = true;
97
101
  debugScrcpy('Starting scrcpy connection...');
@@ -110,6 +114,7 @@ var __webpack_modules__ = {
110
114
  videoCodecOptions: 'i-frame-interval=0,bitrate-mode=2'
111
115
  });
112
116
  this.scrcpyClient = await AdbScrcpyClient.start(this.adb, DefaultServerPath, scrcpyOptions);
117
+ serverOutputTask = this.collectServerOutput(this.scrcpyClient.output, serverOutput);
113
118
  const videoStreamPromise = this.scrcpyClient.videoStream;
114
119
  if (!videoStreamPromise) throw new Error('Scrcpy client did not provide video stream');
115
120
  this.videoStream = await videoStreamPromise;
@@ -126,11 +131,50 @@ var __webpack_modules__ = {
126
131
  } catch (error) {
127
132
  debugScrcpy(`Failed to connect scrcpy: ${error}`);
128
133
  await this.disconnect();
129
- throw error;
134
+ if (serverOutputTask) await Promise.race([
135
+ serverOutputTask,
136
+ new Promise((resolve)=>setTimeout(resolve, SERVER_OUTPUT_DRAIN_TIMEOUT_MS))
137
+ ]);
138
+ throw this.createConnectionError(error, serverOutput);
130
139
  } finally{
131
140
  this.isConnecting = false;
132
141
  }
133
142
  }
143
+ async collectServerOutput(output, lines) {
144
+ const reader = output.getReader();
145
+ try {
146
+ while(true){
147
+ const { done, value } = await reader.read();
148
+ if (done) break;
149
+ lines.push(value);
150
+ if (lines.length > MAX_SERVER_OUTPUT_LINES) lines.splice(0, lines.length - MAX_SERVER_OUTPUT_LINES);
151
+ }
152
+ } catch (error) {
153
+ debugScrcpy(`Failed to read scrcpy server output: ${error}`);
154
+ } finally{
155
+ reader.releaseLock();
156
+ }
157
+ }
158
+ createConnectionError(error, serverOutput) {
159
+ const errorOutput = this.getErrorOutput(error);
160
+ const output = [
161
+ ...new Set([
162
+ ...errorOutput,
163
+ ...serverOutput
164
+ ])
165
+ ].filter((line)=>line.trim().length > 0);
166
+ const message = error instanceof Error ? error.message : String(error);
167
+ const outputDetails = output.length > 0 ? `\nScrcpy server output:\n${output.join('\n')}` : '';
168
+ return new Error(`Failed to connect scrcpy: ${message}${outputDetails}`, {
169
+ cause: error
170
+ });
171
+ }
172
+ getErrorOutput(error) {
173
+ if ('object' != typeof error || null === error || !('output' in error)) return [];
174
+ const output = error.output;
175
+ if (!Array.isArray(output)) return [];
176
+ return output.filter((line)=>'string' == typeof line);
177
+ }
134
178
  resolveServerBinPath() {
135
179
  const androidPkgJson = (0, node_module__rspack_import_1.createRequire)(import.meta.url).resolve('@midscene/android/package.json');
136
180
  return node_path__rspack_import_2["default"].join(node_path__rspack_import_2["default"].dirname(androidPkgJson), 'bin', 'scrcpy-server');
@@ -157,6 +201,7 @@ var __webpack_modules__ = {
157
201
  let windowStart = Date.now();
158
202
  let lastBusyWarn = 0;
159
203
  let totalReads = 0;
204
+ let endReason = 'stream closed';
160
205
  try {
161
206
  while(true){
162
207
  const { done, value } = await reader.read();
@@ -180,10 +225,11 @@ var __webpack_modules__ = {
180
225
  this.processFrame(value);
181
226
  }
182
227
  } catch (error) {
228
+ endReason = 'stream error';
183
229
  debugScrcpy(`Frame consumer error (total reads: ${totalReads}): ${error}`);
184
- await this.disconnect();
185
230
  }
186
- debugScrcpy(`Frame consumer loop ended normally (total reads: ${totalReads})`);
231
+ if (this.streamReader === reader) await this.disconnect();
232
+ debugScrcpy(`Frame consumer loop ended (${endReason}, total reads: ${totalReads})`);
187
233
  }
188
234
  processFrame(packet) {
189
235
  if ('configuration' === packet.type) {
@@ -417,8 +463,10 @@ var __webpack_modules__ = {
417
463
  this.keyframeResolvers = [];
418
464
  this.keyframeListeners.clear();
419
465
  if (reader) try {
420
- reader.cancel();
421
- } catch {}
466
+ await reader.cancel();
467
+ } catch (error) {
468
+ debugScrcpy(`Error cancelling scrcpy stream reader: ${error}`);
469
+ }
422
470
  if (client) try {
423
471
  await client.close();
424
472
  } catch (error) {
@@ -630,6 +678,35 @@ const defaultAppNameMapping = {
630
678
  var external_node_fs_ = __webpack_require__("node:fs");
631
679
  var external_node_module_ = __webpack_require__("node:module");
632
680
  var external_node_path_ = __webpack_require__("node:path");
681
+ const warnAdb = (0, logger_.getDebug)('android:adb', {
682
+ console: true
683
+ });
684
+ async function adb_createAndroidAdb({ adbExecTimeout, deviceId, deviceOptions }) {
685
+ const androidAdbPath = deviceOptions?.androidAdbPath || globalConfigManager.getEnvConfigValue(MIDSCENE_ADB_PATH);
686
+ const remoteAdbHost = deviceOptions?.remoteAdbHost || globalConfigManager.getEnvConfigValue(MIDSCENE_ADB_REMOTE_HOST);
687
+ const remoteAdbPort = deviceOptions?.remoteAdbPort || globalConfigManager.getEnvConfigValue(MIDSCENE_ADB_REMOTE_PORT);
688
+ const adbOptions = {
689
+ udid: deviceId,
690
+ adbExecTimeout,
691
+ remoteAdbHost: remoteAdbHost || void 0,
692
+ remoteAdbPort: remoteAdbPort ? Number(remoteAdbPort) : void 0
693
+ };
694
+ if (androidAdbPath) return new ADB({
695
+ ...adbOptions,
696
+ executable: {
697
+ path: androidAdbPath,
698
+ defaultArgs: []
699
+ }
700
+ });
701
+ const sdkRoot = getSdkRootFromEnv();
702
+ if (sdkRoot) try {
703
+ return await ADB.createADB(adbOptions);
704
+ } catch (error) {
705
+ const message = error instanceof Error ? error.message : String(error);
706
+ warnAdb(`Unable to initialize adb from Android SDK at "${sdkRoot}", falling back to adb from PATH: ${message}`);
707
+ }
708
+ return new ADB(adbOptions);
709
+ }
633
710
  var scrcpy_manager = __webpack_require__("./src/scrcpy-manager.ts");
634
711
  function _define_property(obj, key, value) {
635
712
  if (key in obj) Object.defineProperty(obj, key, {
@@ -642,27 +719,56 @@ function _define_property(obj, key, value) {
642
719
  return obj;
643
720
  }
644
721
  const debugAdapter = (0, logger_.getDebug)('android:scrcpy-adapter');
722
+ const SCRCPY_RETRY_COOLDOWN_MS = 5000;
723
+ const DEFAULT_ADB_SERVER_ENDPOINT = {
724
+ host: '127.0.0.1',
725
+ port: 5037
726
+ };
645
727
  class ScrcpyDeviceAdapter {
646
728
  isEnabled() {
647
- if (this.initFailed) return false;
729
+ if (!this.isConfigured()) return false;
730
+ return null === this.retryAfter || Date.now() >= this.retryAfter;
731
+ }
732
+ getStatus() {
733
+ return {
734
+ enabled: this.isConfigured(),
735
+ connected: this.manager?.isConnected() ?? false,
736
+ lastError: this.lastError,
737
+ retryAfter: this.retryAfter
738
+ };
739
+ }
740
+ isConfigured() {
648
741
  return this.scrcpyConfig?.enabled ?? scrcpy_manager.o.enabled;
649
742
  }
650
743
  async initialize(deviceInfo) {
651
744
  try {
652
745
  const manager = await this.ensureManager(deviceInfo);
653
746
  await manager.ensureConnected();
747
+ this.clearFailure();
654
748
  } catch (error) {
655
- this.initFailed = true;
749
+ this.recordFailure(error);
656
750
  throw error;
657
751
  }
658
752
  }
753
+ recordFailure(error) {
754
+ this.lastError = error instanceof Error ? error.message : String(error);
755
+ this.retryAfter = Date.now() + SCRCPY_RETRY_COOLDOWN_MS;
756
+ }
757
+ clearFailure() {
758
+ this.lastError = null;
759
+ this.retryAfter = null;
760
+ }
761
+ ensureRetryReady() {
762
+ if (null === this.retryAfter || Date.now() >= this.retryAfter) return;
763
+ throw new Error(`scrcpy retry is cooling down until ${new Date(this.retryAfter).toISOString()}. Last error: ${this.lastError}`);
764
+ }
659
765
  resolveConfig(deviceInfo) {
660
766
  if (this.resolvedConfig) return this.resolvedConfig;
661
767
  const config = this.scrcpyConfig;
662
768
  const maxSize = config?.maxSize ?? scrcpy_manager.o.maxSize;
663
769
  const videoBitRate = config?.videoBitRate ?? scrcpy_manager.o.videoBitRate;
664
770
  this.resolvedConfig = {
665
- enabled: this.isEnabled(),
771
+ enabled: this.isConfigured(),
666
772
  maxSize,
667
773
  idleTimeoutMs: config?.idleTimeoutMs ?? scrcpy_manager.o.idleTimeoutMs,
668
774
  videoBitRate
@@ -676,10 +782,8 @@ class ScrcpyDeviceAdapter {
676
782
  const { Adb, AdbServerClient } = await import("@yume-chan/adb");
677
783
  const { AdbServerNodeTcpConnector } = await import("@yume-chan/adb-server-node-tcp");
678
784
  const { ScrcpyScreenshotManager: ScrcpyManager } = await Promise.resolve().then(__webpack_require__.bind(__webpack_require__, "./src/scrcpy-manager.ts"));
679
- const adbClient = new AdbServerClient(new AdbServerNodeTcpConnector({
680
- host: '127.0.0.1',
681
- port: 5037
682
- }));
785
+ const adbServerEndpoint = await this.resolveAdbServerEndpoint();
786
+ const adbClient = new AdbServerClient(new AdbServerNodeTcpConnector(adbServerEndpoint));
683
787
  const adb = new Adb(await adbClient.createTransport({
684
788
  serial: this.deviceId
685
789
  }));
@@ -699,14 +803,28 @@ class ScrcpyDeviceAdapter {
699
803
  }
700
804
  }
701
805
  async screenshotBase64(deviceInfo) {
702
- const manager = await this.ensureManager(deviceInfo);
703
- const screenshotBuffer = await manager.getScreenshotJpeg();
704
- return createImgBase64ByFormat('jpeg', screenshotBuffer.toString('base64'));
806
+ this.ensureRetryReady();
807
+ try {
808
+ const manager = await this.ensureManager(deviceInfo);
809
+ const screenshotBuffer = await manager.getScreenshotJpeg();
810
+ this.clearFailure();
811
+ return createImgBase64ByFormat('jpeg', screenshotBuffer.toString('base64'));
812
+ } catch (error) {
813
+ this.recordFailure(error);
814
+ throw error;
815
+ }
705
816
  }
706
817
  async subscribeKeyframes(deviceInfo, listener) {
707
- const manager = await this.ensureManager(deviceInfo);
708
- await manager.ensureConnected();
709
- return manager.subscribeKeyframes(listener);
818
+ this.ensureRetryReady();
819
+ try {
820
+ const manager = await this.ensureManager(deviceInfo);
821
+ await manager.ensureConnected();
822
+ this.clearFailure();
823
+ return manager.subscribeKeyframes(listener);
824
+ } catch (error) {
825
+ this.recordFailure(error);
826
+ throw error;
827
+ }
710
828
  }
711
829
  getLatestRawKeyframe() {
712
830
  return this.manager?.getLatestRawKeyframe() ?? null;
@@ -743,18 +861,23 @@ class ScrcpyDeviceAdapter {
743
861
  this.manager = null;
744
862
  }
745
863
  this.resolvedConfig = null;
864
+ this.clearFailure();
746
865
  }
747
- constructor(deviceId, scrcpyConfig){
866
+ constructor(deviceId, scrcpyConfig, resolveAdbServerEndpoint = ()=>DEFAULT_ADB_SERVER_ENDPOINT){
748
867
  _define_property(this, "deviceId", void 0);
749
868
  _define_property(this, "scrcpyConfig", void 0);
869
+ _define_property(this, "resolveAdbServerEndpoint", void 0);
750
870
  _define_property(this, "manager", void 0);
751
871
  _define_property(this, "resolvedConfig", void 0);
752
- _define_property(this, "initFailed", void 0);
872
+ _define_property(this, "lastError", void 0);
873
+ _define_property(this, "retryAfter", void 0);
753
874
  this.deviceId = deviceId;
754
875
  this.scrcpyConfig = scrcpyConfig;
876
+ this.resolveAdbServerEndpoint = resolveAdbServerEndpoint;
755
877
  this.manager = null;
756
878
  this.resolvedConfig = null;
757
- this.initFailed = false;
879
+ this.lastError = null;
880
+ this.retryAfter = null;
758
881
  }
759
882
  }
760
883
  function device_define_property(obj, key, value) {
@@ -881,7 +1004,7 @@ class AndroidDevice {
881
1004
  console.log(`[midscene] Using scrcpy for screenshots (device: ${this.deviceId})`);
882
1005
  } catch (error) {
883
1006
  const msg = error instanceof Error ? error.message : String(error);
884
- warnDevice(`[midscene] Scrcpy unavailable, using ADB fallback (device: ${this.deviceId}): ${msg}`);
1007
+ warnDevice(`[midscene] Scrcpy unavailable, using ADB fallback (device: ${this.deviceId}): ${msg}. Call retryScrcpy() to retry immediately.`);
885
1008
  }
886
1009
  return adb;
887
1010
  }
@@ -893,18 +1016,10 @@ class AndroidDevice {
893
1016
  let error = null;
894
1017
  debugDevice(`Initializing ADB with device ID: ${this.deviceId}`);
895
1018
  try {
896
- const androidAdbPath = this.options?.androidAdbPath || globalConfigManager.getEnvConfigValue(MIDSCENE_ADB_PATH);
897
- const remoteAdbHost = this.options?.remoteAdbHost || globalConfigManager.getEnvConfigValue(MIDSCENE_ADB_REMOTE_HOST);
898
- const remoteAdbPort = this.options?.remoteAdbPort || globalConfigManager.getEnvConfigValue(MIDSCENE_ADB_REMOTE_PORT);
899
- this.adb = new external_appium_adb_ADB({
900
- udid: this.deviceId,
1019
+ this.adb = await adb_createAndroidAdb({
901
1020
  adbExecTimeout: 60000,
902
- executable: androidAdbPath ? {
903
- path: androidAdbPath,
904
- defaultArgs: []
905
- } : void 0,
906
- remoteAdbHost: remoteAdbHost || void 0,
907
- remoteAdbPort: remoteAdbPort ? Number(remoteAdbPort) : void 0
1021
+ deviceId: this.deviceId,
1022
+ deviceOptions: this.options
908
1023
  });
909
1024
  const size = await this.getScreenSize();
910
1025
  this.description = `
@@ -939,8 +1054,9 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
939
1054
  } catch (error) {
940
1055
  const methodName = String(prop);
941
1056
  const deviceId = this.deviceId;
942
- debugDevice(`ADB error with device ${deviceId} when calling ${methodName}: ${error}`);
943
- throw new Error(`ADB error with device ${deviceId} when calling ${methodName}, please check https://midscenejs.com/integrate-with-android.html#faq : ${error.message}`, {
1057
+ const adbExecutable = target.executable.path;
1058
+ debugDevice(`ADB error with device ${deviceId} when calling ${methodName} (ADB executable: ${adbExecutable}): ${error}`);
1059
+ throw new Error(`ADB error with device ${deviceId} when calling ${methodName} (ADB executable: ${adbExecutable}), please check https://midscenejs.com/integrate-with-android.html#faq : ${error.message}`, {
944
1060
  cause: error
945
1061
  });
946
1062
  }
@@ -948,8 +1064,24 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
948
1064
  }
949
1065
  });
950
1066
  }
1067
+ getScrcpyStatus() {
1068
+ return this.getScrcpyAdapter().getStatus();
1069
+ }
1070
+ async retryScrcpy() {
1071
+ const adapter = this.getScrcpyAdapter();
1072
+ if (!adapter.getStatus().enabled) throw new Error('scrcpy is disabled in AndroidDevice options');
1073
+ const deviceInfo = await this.getDevicePhysicalInfo();
1074
+ await adapter.initialize(deviceInfo);
1075
+ return adapter.getStatus();
1076
+ }
951
1077
  getScrcpyAdapter() {
952
- if (!this.scrcpyAdapter) this.scrcpyAdapter = new ScrcpyDeviceAdapter(this.deviceId, this.options?.scrcpyConfig);
1078
+ if (!this.scrcpyAdapter) this.scrcpyAdapter = new ScrcpyDeviceAdapter(this.deviceId, this.options?.scrcpyConfig, async ()=>{
1079
+ const adb = await this.getAdb();
1080
+ return {
1081
+ host: adb.adbHost ?? '127.0.0.1',
1082
+ port: adb.adbPort ?? 5037
1083
+ };
1084
+ });
953
1085
  return this.scrcpyAdapter;
954
1086
  }
955
1087
  async openScrcpyFrameSource() {
@@ -1981,17 +2113,21 @@ const createPlatformActions = (device)=>({
1981
2113
  })
1982
2114
  });
1983
2115
  const debugUtils = (0, logger_.getDebug)('android:utils');
1984
- async function getConnectedDevices() {
2116
+ async function getConnectedDevices(deviceOptions) {
2117
+ let adbExecutable;
1985
2118
  try {
1986
- const adb = await external_appium_adb_ADB.createADB({
1987
- adbExecTimeout: 60000
2119
+ const adb = await adb_createAndroidAdb({
2120
+ adbExecTimeout: 60000,
2121
+ deviceOptions
1988
2122
  });
2123
+ adbExecutable = adb.executable.path;
1989
2124
  const devices = await adb.getConnectedDevices();
1990
2125
  debugUtils(`Found ${devices.length} connected devices: `, devices);
1991
2126
  return devices;
1992
2127
  } catch (error) {
1993
2128
  console.error('Failed to get device list:', error);
1994
- throw new Error(`Unable to get connected Android device list, please check https://midscenejs.com/integrate-with-android.html#faq : ${error.message}`, {
2129
+ const adbExecutableContext = adbExecutable ? ` (ADB executable: ${adbExecutable})` : '';
2130
+ throw new Error(`Unable to get connected Android device list${adbExecutableContext}, please check https://midscenejs.com/integrate-with-android.html#faq : ${error.message}`, {
1995
2131
  cause: error
1996
2132
  });
1997
2133
  }
@@ -2047,7 +2183,7 @@ class AndroidAgent extends Agent {
2047
2183
  }
2048
2184
  async function agentFromAdbDevice(deviceId, opts) {
2049
2185
  if (!deviceId) {
2050
- const devices = await getConnectedDevices();
2186
+ const devices = await getConnectedDevices(opts);
2051
2187
  if (0 === devices.length) throw new Error('No Android devices found. Please connect an Android device and ensure ADB is properly configured. Run `adb devices` to verify device connection.');
2052
2188
  deviceId = devices[0].udid;
2053
2189
  debugAgent('deviceId not specified, will use the first device (id = %s)', deviceId);
@@ -2176,7 +2312,7 @@ class AndroidMidsceneTools extends BaseMidsceneTools {
2176
2312
  const tools = new AndroidMidsceneTools();
2177
2313
  runToolsCLI(tools, 'midscene-android', {
2178
2314
  stripPrefix: 'android_',
2179
- version: "1.10.6-beta-20260716090839.0",
2315
+ version: "1.10.6",
2180
2316
  extraCommands: createReportCliCommands()
2181
2317
  }).catch((e)=>{
2182
2318
  process.exit(reportCLIError(e));