@midscene/android 1.10.5 → 1.10.6-beta-20260717061640.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/es/cli.mjs CHANGED
@@ -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) {
@@ -642,27 +690,52 @@ function _define_property(obj, key, value) {
642
690
  return obj;
643
691
  }
644
692
  const debugAdapter = (0, logger_.getDebug)('android:scrcpy-adapter');
693
+ const SCRCPY_RETRY_COOLDOWN_MS = 5000;
645
694
  class ScrcpyDeviceAdapter {
646
695
  isEnabled() {
647
- if (this.initFailed) return false;
696
+ if (!this.isConfigured()) return false;
697
+ return null === this.retryAfter || Date.now() >= this.retryAfter;
698
+ }
699
+ getStatus() {
700
+ return {
701
+ enabled: this.isConfigured(),
702
+ connected: this.manager?.isConnected() ?? false,
703
+ lastError: this.lastError,
704
+ retryAfter: this.retryAfter
705
+ };
706
+ }
707
+ isConfigured() {
648
708
  return this.scrcpyConfig?.enabled ?? scrcpy_manager.o.enabled;
649
709
  }
650
710
  async initialize(deviceInfo) {
651
711
  try {
652
712
  const manager = await this.ensureManager(deviceInfo);
653
713
  await manager.ensureConnected();
714
+ this.clearFailure();
654
715
  } catch (error) {
655
- this.initFailed = true;
716
+ this.recordFailure(error);
656
717
  throw error;
657
718
  }
658
719
  }
720
+ recordFailure(error) {
721
+ this.lastError = error instanceof Error ? error.message : String(error);
722
+ this.retryAfter = Date.now() + SCRCPY_RETRY_COOLDOWN_MS;
723
+ }
724
+ clearFailure() {
725
+ this.lastError = null;
726
+ this.retryAfter = null;
727
+ }
728
+ ensureRetryReady() {
729
+ if (null === this.retryAfter || Date.now() >= this.retryAfter) return;
730
+ throw new Error(`scrcpy retry is cooling down until ${new Date(this.retryAfter).toISOString()}. Last error: ${this.lastError}`);
731
+ }
659
732
  resolveConfig(deviceInfo) {
660
733
  if (this.resolvedConfig) return this.resolvedConfig;
661
734
  const config = this.scrcpyConfig;
662
735
  const maxSize = config?.maxSize ?? scrcpy_manager.o.maxSize;
663
736
  const videoBitRate = config?.videoBitRate ?? scrcpy_manager.o.videoBitRate;
664
737
  this.resolvedConfig = {
665
- enabled: this.isEnabled(),
738
+ enabled: this.isConfigured(),
666
739
  maxSize,
667
740
  idleTimeoutMs: config?.idleTimeoutMs ?? scrcpy_manager.o.idleTimeoutMs,
668
741
  videoBitRate
@@ -699,14 +772,28 @@ class ScrcpyDeviceAdapter {
699
772
  }
700
773
  }
701
774
  async screenshotBase64(deviceInfo) {
702
- const manager = await this.ensureManager(deviceInfo);
703
- const screenshotBuffer = await manager.getScreenshotJpeg();
704
- return createImgBase64ByFormat('jpeg', screenshotBuffer.toString('base64'));
775
+ this.ensureRetryReady();
776
+ try {
777
+ const manager = await this.ensureManager(deviceInfo);
778
+ const screenshotBuffer = await manager.getScreenshotJpeg();
779
+ this.clearFailure();
780
+ return createImgBase64ByFormat('jpeg', screenshotBuffer.toString('base64'));
781
+ } catch (error) {
782
+ this.recordFailure(error);
783
+ throw error;
784
+ }
705
785
  }
706
786
  async subscribeKeyframes(deviceInfo, listener) {
707
- const manager = await this.ensureManager(deviceInfo);
708
- await manager.ensureConnected();
709
- return manager.subscribeKeyframes(listener);
787
+ this.ensureRetryReady();
788
+ try {
789
+ const manager = await this.ensureManager(deviceInfo);
790
+ await manager.ensureConnected();
791
+ this.clearFailure();
792
+ return manager.subscribeKeyframes(listener);
793
+ } catch (error) {
794
+ this.recordFailure(error);
795
+ throw error;
796
+ }
710
797
  }
711
798
  getLatestRawKeyframe() {
712
799
  return this.manager?.getLatestRawKeyframe() ?? null;
@@ -743,18 +830,21 @@ class ScrcpyDeviceAdapter {
743
830
  this.manager = null;
744
831
  }
745
832
  this.resolvedConfig = null;
833
+ this.clearFailure();
746
834
  }
747
835
  constructor(deviceId, scrcpyConfig){
748
836
  _define_property(this, "deviceId", void 0);
749
837
  _define_property(this, "scrcpyConfig", void 0);
750
838
  _define_property(this, "manager", void 0);
751
839
  _define_property(this, "resolvedConfig", void 0);
752
- _define_property(this, "initFailed", void 0);
840
+ _define_property(this, "lastError", void 0);
841
+ _define_property(this, "retryAfter", void 0);
753
842
  this.deviceId = deviceId;
754
843
  this.scrcpyConfig = scrcpyConfig;
755
844
  this.manager = null;
756
845
  this.resolvedConfig = null;
757
- this.initFailed = false;
846
+ this.lastError = null;
847
+ this.retryAfter = null;
758
848
  }
759
849
  }
760
850
  function device_define_property(obj, key, value) {
@@ -881,7 +971,7 @@ class AndroidDevice {
881
971
  console.log(`[midscene] Using scrcpy for screenshots (device: ${this.deviceId})`);
882
972
  } catch (error) {
883
973
  const msg = error instanceof Error ? error.message : String(error);
884
- warnDevice(`[midscene] Scrcpy unavailable, using ADB fallback (device: ${this.deviceId}): ${msg}`);
974
+ warnDevice(`[midscene] Scrcpy unavailable, using ADB fallback (device: ${this.deviceId}): ${msg}. Call retryScrcpy() to retry immediately.`);
885
975
  }
886
976
  return adb;
887
977
  }
@@ -948,6 +1038,16 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
948
1038
  }
949
1039
  });
950
1040
  }
1041
+ getScrcpyStatus() {
1042
+ return this.getScrcpyAdapter().getStatus();
1043
+ }
1044
+ async retryScrcpy() {
1045
+ const adapter = this.getScrcpyAdapter();
1046
+ if (!adapter.getStatus().enabled) throw new Error('scrcpy is disabled in AndroidDevice options');
1047
+ const deviceInfo = await this.getDevicePhysicalInfo();
1048
+ await adapter.initialize(deviceInfo);
1049
+ return adapter.getStatus();
1050
+ }
951
1051
  getScrcpyAdapter() {
952
1052
  if (!this.scrcpyAdapter) this.scrcpyAdapter = new ScrcpyDeviceAdapter(this.deviceId, this.options?.scrcpyConfig);
953
1053
  return this.scrcpyAdapter;
@@ -2176,7 +2276,7 @@ class AndroidMidsceneTools extends BaseMidsceneTools {
2176
2276
  const tools = new AndroidMidsceneTools();
2177
2277
  runToolsCLI(tools, 'midscene-android', {
2178
2278
  stripPrefix: 'android_',
2179
- version: "1.10.5",
2279
+ version: "1.10.6-beta-20260717061640.0",
2180
2280
  extraCommands: createReportCliCommands()
2181
2281
  }).catch((e)=>{
2182
2282
  process.exit(reportCLIError(e));
package/dist/es/index.mjs CHANGED
@@ -51,6 +51,8 @@ var __webpack_modules__ = {
51
51
  const KEYFRAME_POLL_INTERVAL_MS = 200;
52
52
  const MAX_SCAN_BYTES = 1000;
53
53
  const CONNECTION_WAIT_MS = 1000;
54
+ const MAX_SERVER_OUTPUT_LINES = 100;
55
+ const SERVER_OUTPUT_DRAIN_TIMEOUT_MS = 500;
54
56
  const BUSY_LOOP_WINDOW_MS = 1000;
55
57
  const BUSY_LOOP_MAX_READS = 500;
56
58
  const BUSY_LOOP_COOLDOWN_MS = 50;
@@ -91,6 +93,8 @@ var __webpack_modules__ = {
91
93
  if (this.scrcpyClient && this.videoStream) return void this.resetIdleTimer();
92
94
  throw new Error('Scrcpy connection failed: another connection attempt did not complete in time');
93
95
  }
96
+ const serverOutput = [];
97
+ let serverOutputTask = null;
94
98
  try {
95
99
  this.isConnecting = true;
96
100
  debugScrcpy('Starting scrcpy connection...');
@@ -109,6 +113,7 @@ var __webpack_modules__ = {
109
113
  videoCodecOptions: 'i-frame-interval=0,bitrate-mode=2'
110
114
  });
111
115
  this.scrcpyClient = await AdbScrcpyClient.start(this.adb, DefaultServerPath, scrcpyOptions);
116
+ serverOutputTask = this.collectServerOutput(this.scrcpyClient.output, serverOutput);
112
117
  const videoStreamPromise = this.scrcpyClient.videoStream;
113
118
  if (!videoStreamPromise) throw new Error('Scrcpy client did not provide video stream');
114
119
  this.videoStream = await videoStreamPromise;
@@ -125,11 +130,50 @@ var __webpack_modules__ = {
125
130
  } catch (error) {
126
131
  debugScrcpy(`Failed to connect scrcpy: ${error}`);
127
132
  await this.disconnect();
128
- throw error;
133
+ if (serverOutputTask) await Promise.race([
134
+ serverOutputTask,
135
+ new Promise((resolve)=>setTimeout(resolve, SERVER_OUTPUT_DRAIN_TIMEOUT_MS))
136
+ ]);
137
+ throw this.createConnectionError(error, serverOutput);
129
138
  } finally{
130
139
  this.isConnecting = false;
131
140
  }
132
141
  }
142
+ async collectServerOutput(output, lines) {
143
+ const reader = output.getReader();
144
+ try {
145
+ while(true){
146
+ const { done, value } = await reader.read();
147
+ if (done) break;
148
+ lines.push(value);
149
+ if (lines.length > MAX_SERVER_OUTPUT_LINES) lines.splice(0, lines.length - MAX_SERVER_OUTPUT_LINES);
150
+ }
151
+ } catch (error) {
152
+ debugScrcpy(`Failed to read scrcpy server output: ${error}`);
153
+ } finally{
154
+ reader.releaseLock();
155
+ }
156
+ }
157
+ createConnectionError(error, serverOutput) {
158
+ const errorOutput = this.getErrorOutput(error);
159
+ const output = [
160
+ ...new Set([
161
+ ...errorOutput,
162
+ ...serverOutput
163
+ ])
164
+ ].filter((line)=>line.trim().length > 0);
165
+ const message = error instanceof Error ? error.message : String(error);
166
+ const outputDetails = output.length > 0 ? `\nScrcpy server output:\n${output.join('\n')}` : '';
167
+ return new Error(`Failed to connect scrcpy: ${message}${outputDetails}`, {
168
+ cause: error
169
+ });
170
+ }
171
+ getErrorOutput(error) {
172
+ if ('object' != typeof error || null === error || !('output' in error)) return [];
173
+ const output = error.output;
174
+ if (!Array.isArray(output)) return [];
175
+ return output.filter((line)=>'string' == typeof line);
176
+ }
133
177
  resolveServerBinPath() {
134
178
  const androidPkgJson = (0, node_module__rspack_import_1.createRequire)(import.meta.url).resolve('@midscene/android/package.json');
135
179
  return node_path__rspack_import_2["default"].join(node_path__rspack_import_2["default"].dirname(androidPkgJson), 'bin', 'scrcpy-server');
@@ -156,6 +200,7 @@ var __webpack_modules__ = {
156
200
  let windowStart = Date.now();
157
201
  let lastBusyWarn = 0;
158
202
  let totalReads = 0;
203
+ let endReason = 'stream closed';
159
204
  try {
160
205
  while(true){
161
206
  const { done, value } = await reader.read();
@@ -179,10 +224,11 @@ var __webpack_modules__ = {
179
224
  this.processFrame(value);
180
225
  }
181
226
  } catch (error) {
227
+ endReason = 'stream error';
182
228
  debugScrcpy(`Frame consumer error (total reads: ${totalReads}): ${error}`);
183
- await this.disconnect();
184
229
  }
185
- debugScrcpy(`Frame consumer loop ended normally (total reads: ${totalReads})`);
230
+ if (this.streamReader === reader) await this.disconnect();
231
+ debugScrcpy(`Frame consumer loop ended (${endReason}, total reads: ${totalReads})`);
186
232
  }
187
233
  processFrame(packet) {
188
234
  if ('configuration' === packet.type) {
@@ -416,8 +462,10 @@ var __webpack_modules__ = {
416
462
  this.keyframeResolvers = [];
417
463
  this.keyframeListeners.clear();
418
464
  if (reader) try {
419
- reader.cancel();
420
- } catch {}
465
+ await reader.cancel();
466
+ } catch (error) {
467
+ debugScrcpy(`Error cancelling scrcpy stream reader: ${error}`);
468
+ }
421
469
  if (client) try {
422
470
  await client.close();
423
471
  } catch (error) {
@@ -545,27 +593,52 @@ function _define_property(obj, key, value) {
545
593
  return obj;
546
594
  }
547
595
  const debugAdapter = (0, logger_.getDebug)('android:scrcpy-adapter');
596
+ const SCRCPY_RETRY_COOLDOWN_MS = 5000;
548
597
  class ScrcpyDeviceAdapter {
549
598
  isEnabled() {
550
- if (this.initFailed) return false;
599
+ if (!this.isConfigured()) return false;
600
+ return null === this.retryAfter || Date.now() >= this.retryAfter;
601
+ }
602
+ getStatus() {
603
+ return {
604
+ enabled: this.isConfigured(),
605
+ connected: this.manager?.isConnected() ?? false,
606
+ lastError: this.lastError,
607
+ retryAfter: this.retryAfter
608
+ };
609
+ }
610
+ isConfigured() {
551
611
  return this.scrcpyConfig?.enabled ?? scrcpy_manager.o.enabled;
552
612
  }
553
613
  async initialize(deviceInfo) {
554
614
  try {
555
615
  const manager = await this.ensureManager(deviceInfo);
556
616
  await manager.ensureConnected();
617
+ this.clearFailure();
557
618
  } catch (error) {
558
- this.initFailed = true;
619
+ this.recordFailure(error);
559
620
  throw error;
560
621
  }
561
622
  }
623
+ recordFailure(error) {
624
+ this.lastError = error instanceof Error ? error.message : String(error);
625
+ this.retryAfter = Date.now() + SCRCPY_RETRY_COOLDOWN_MS;
626
+ }
627
+ clearFailure() {
628
+ this.lastError = null;
629
+ this.retryAfter = null;
630
+ }
631
+ ensureRetryReady() {
632
+ if (null === this.retryAfter || Date.now() >= this.retryAfter) return;
633
+ throw new Error(`scrcpy retry is cooling down until ${new Date(this.retryAfter).toISOString()}. Last error: ${this.lastError}`);
634
+ }
562
635
  resolveConfig(deviceInfo) {
563
636
  if (this.resolvedConfig) return this.resolvedConfig;
564
637
  const config = this.scrcpyConfig;
565
638
  const maxSize = config?.maxSize ?? scrcpy_manager.o.maxSize;
566
639
  const videoBitRate = config?.videoBitRate ?? scrcpy_manager.o.videoBitRate;
567
640
  this.resolvedConfig = {
568
- enabled: this.isEnabled(),
641
+ enabled: this.isConfigured(),
569
642
  maxSize,
570
643
  idleTimeoutMs: config?.idleTimeoutMs ?? scrcpy_manager.o.idleTimeoutMs,
571
644
  videoBitRate
@@ -602,14 +675,28 @@ class ScrcpyDeviceAdapter {
602
675
  }
603
676
  }
604
677
  async screenshotBase64(deviceInfo) {
605
- const manager = await this.ensureManager(deviceInfo);
606
- const screenshotBuffer = await manager.getScreenshotJpeg();
607
- return createImgBase64ByFormat('jpeg', screenshotBuffer.toString('base64'));
678
+ this.ensureRetryReady();
679
+ try {
680
+ const manager = await this.ensureManager(deviceInfo);
681
+ const screenshotBuffer = await manager.getScreenshotJpeg();
682
+ this.clearFailure();
683
+ return createImgBase64ByFormat('jpeg', screenshotBuffer.toString('base64'));
684
+ } catch (error) {
685
+ this.recordFailure(error);
686
+ throw error;
687
+ }
608
688
  }
609
689
  async subscribeKeyframes(deviceInfo, listener) {
610
- const manager = await this.ensureManager(deviceInfo);
611
- await manager.ensureConnected();
612
- return manager.subscribeKeyframes(listener);
690
+ this.ensureRetryReady();
691
+ try {
692
+ const manager = await this.ensureManager(deviceInfo);
693
+ await manager.ensureConnected();
694
+ this.clearFailure();
695
+ return manager.subscribeKeyframes(listener);
696
+ } catch (error) {
697
+ this.recordFailure(error);
698
+ throw error;
699
+ }
613
700
  }
614
701
  getLatestRawKeyframe() {
615
702
  return this.manager?.getLatestRawKeyframe() ?? null;
@@ -646,18 +733,21 @@ class ScrcpyDeviceAdapter {
646
733
  this.manager = null;
647
734
  }
648
735
  this.resolvedConfig = null;
736
+ this.clearFailure();
649
737
  }
650
738
  constructor(deviceId, scrcpyConfig){
651
739
  _define_property(this, "deviceId", void 0);
652
740
  _define_property(this, "scrcpyConfig", void 0);
653
741
  _define_property(this, "manager", void 0);
654
742
  _define_property(this, "resolvedConfig", void 0);
655
- _define_property(this, "initFailed", void 0);
743
+ _define_property(this, "lastError", void 0);
744
+ _define_property(this, "retryAfter", void 0);
656
745
  this.deviceId = deviceId;
657
746
  this.scrcpyConfig = scrcpyConfig;
658
747
  this.manager = null;
659
748
  this.resolvedConfig = null;
660
- this.initFailed = false;
749
+ this.lastError = null;
750
+ this.retryAfter = null;
661
751
  }
662
752
  }
663
753
  function device_define_property(obj, key, value) {
@@ -784,7 +874,7 @@ class AndroidDevice {
784
874
  console.log(`[midscene] Using scrcpy for screenshots (device: ${this.deviceId})`);
785
875
  } catch (error) {
786
876
  const msg = error instanceof Error ? error.message : String(error);
787
- warnDevice(`[midscene] Scrcpy unavailable, using ADB fallback (device: ${this.deviceId}): ${msg}`);
877
+ warnDevice(`[midscene] Scrcpy unavailable, using ADB fallback (device: ${this.deviceId}): ${msg}. Call retryScrcpy() to retry immediately.`);
788
878
  }
789
879
  return adb;
790
880
  }
@@ -851,6 +941,16 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
851
941
  }
852
942
  });
853
943
  }
944
+ getScrcpyStatus() {
945
+ return this.getScrcpyAdapter().getStatus();
946
+ }
947
+ async retryScrcpy() {
948
+ const adapter = this.getScrcpyAdapter();
949
+ if (!adapter.getStatus().enabled) throw new Error('scrcpy is disabled in AndroidDevice options');
950
+ const deviceInfo = await this.getDevicePhysicalInfo();
951
+ await adapter.initialize(deviceInfo);
952
+ return adapter.getStatus();
953
+ }
854
954
  getScrcpyAdapter() {
855
955
  if (!this.scrcpyAdapter) this.scrcpyAdapter = new ScrcpyDeviceAdapter(this.deviceId, this.options?.scrcpyConfig);
856
956
  return this.scrcpyAdapter;
package/dist/lib/cli.js CHANGED
@@ -40,6 +40,8 @@ var __webpack_modules__ = {
40
40
  const KEYFRAME_POLL_INTERVAL_MS = 200;
41
41
  const MAX_SCAN_BYTES = 1000;
42
42
  const CONNECTION_WAIT_MS = 1000;
43
+ const MAX_SERVER_OUTPUT_LINES = 100;
44
+ const SERVER_OUTPUT_DRAIN_TIMEOUT_MS = 500;
43
45
  const BUSY_LOOP_WINDOW_MS = 1000;
44
46
  const BUSY_LOOP_MAX_READS = 500;
45
47
  const BUSY_LOOP_COOLDOWN_MS = 50;
@@ -80,6 +82,8 @@ var __webpack_modules__ = {
80
82
  if (this.scrcpyClient && this.videoStream) return void this.resetIdleTimer();
81
83
  throw new Error('Scrcpy connection failed: another connection attempt did not complete in time');
82
84
  }
85
+ const serverOutput = [];
86
+ let serverOutputTask = null;
83
87
  try {
84
88
  this.isConnecting = true;
85
89
  debugScrcpy('Starting scrcpy connection...');
@@ -98,6 +102,7 @@ var __webpack_modules__ = {
98
102
  videoCodecOptions: 'i-frame-interval=0,bitrate-mode=2'
99
103
  });
100
104
  this.scrcpyClient = await AdbScrcpyClient.start(this.adb, DefaultServerPath, scrcpyOptions);
105
+ serverOutputTask = this.collectServerOutput(this.scrcpyClient.output, serverOutput);
101
106
  const videoStreamPromise = this.scrcpyClient.videoStream;
102
107
  if (!videoStreamPromise) throw new Error('Scrcpy client did not provide video stream');
103
108
  this.videoStream = await videoStreamPromise;
@@ -114,11 +119,50 @@ var __webpack_modules__ = {
114
119
  } catch (error) {
115
120
  debugScrcpy(`Failed to connect scrcpy: ${error}`);
116
121
  await this.disconnect();
117
- throw error;
122
+ if (serverOutputTask) await Promise.race([
123
+ serverOutputTask,
124
+ new Promise((resolve)=>setTimeout(resolve, SERVER_OUTPUT_DRAIN_TIMEOUT_MS))
125
+ ]);
126
+ throw this.createConnectionError(error, serverOutput);
118
127
  } finally{
119
128
  this.isConnecting = false;
120
129
  }
121
130
  }
131
+ async collectServerOutput(output, lines) {
132
+ const reader = output.getReader();
133
+ try {
134
+ while(true){
135
+ const { done, value } = await reader.read();
136
+ if (done) break;
137
+ lines.push(value);
138
+ if (lines.length > MAX_SERVER_OUTPUT_LINES) lines.splice(0, lines.length - MAX_SERVER_OUTPUT_LINES);
139
+ }
140
+ } catch (error) {
141
+ debugScrcpy(`Failed to read scrcpy server output: ${error}`);
142
+ } finally{
143
+ reader.releaseLock();
144
+ }
145
+ }
146
+ createConnectionError(error, serverOutput) {
147
+ const errorOutput = this.getErrorOutput(error);
148
+ const output = [
149
+ ...new Set([
150
+ ...errorOutput,
151
+ ...serverOutput
152
+ ])
153
+ ].filter((line)=>line.trim().length > 0);
154
+ const message = error instanceof Error ? error.message : String(error);
155
+ const outputDetails = output.length > 0 ? `\nScrcpy server output:\n${output.join('\n')}` : '';
156
+ return new Error(`Failed to connect scrcpy: ${message}${outputDetails}`, {
157
+ cause: error
158
+ });
159
+ }
160
+ getErrorOutput(error) {
161
+ if ('object' != typeof error || null === error || !('output' in error)) return [];
162
+ const output = error.output;
163
+ if (!Array.isArray(output)) return [];
164
+ return output.filter((line)=>'string' == typeof line);
165
+ }
122
166
  resolveServerBinPath() {
123
167
  const androidPkgJson = (0, node_module__rspack_import_1.createRequire)(__rslib_import_meta_url__).resolve('@midscene/android/package.json');
124
168
  return node_path__rspack_import_2_default().join(node_path__rspack_import_2_default().dirname(androidPkgJson), 'bin', 'scrcpy-server');
@@ -145,6 +189,7 @@ var __webpack_modules__ = {
145
189
  let windowStart = Date.now();
146
190
  let lastBusyWarn = 0;
147
191
  let totalReads = 0;
192
+ let endReason = 'stream closed';
148
193
  try {
149
194
  while(true){
150
195
  const { done, value } = await reader.read();
@@ -168,10 +213,11 @@ var __webpack_modules__ = {
168
213
  this.processFrame(value);
169
214
  }
170
215
  } catch (error) {
216
+ endReason = 'stream error';
171
217
  debugScrcpy(`Frame consumer error (total reads: ${totalReads}): ${error}`);
172
- await this.disconnect();
173
218
  }
174
- debugScrcpy(`Frame consumer loop ended normally (total reads: ${totalReads})`);
219
+ if (this.streamReader === reader) await this.disconnect();
220
+ debugScrcpy(`Frame consumer loop ended (${endReason}, total reads: ${totalReads})`);
175
221
  }
176
222
  processFrame(packet) {
177
223
  if ('configuration' === packet.type) {
@@ -405,8 +451,10 @@ var __webpack_modules__ = {
405
451
  this.keyframeResolvers = [];
406
452
  this.keyframeListeners.clear();
407
453
  if (reader) try {
408
- reader.cancel();
409
- } catch {}
454
+ await reader.cancel();
455
+ } catch (error) {
456
+ debugScrcpy(`Error cancelling scrcpy stream reader: ${error}`);
457
+ }
410
458
  if (client) try {
411
459
  await client.close();
412
460
  } catch (error) {
@@ -657,27 +705,52 @@ ${stdout ? truncateAdbShellStream(stdout, 'stdout') : EMPTY_ADB_SHELL_STDOUT}`;
657
705
  return obj;
658
706
  }
659
707
  const debugAdapter = (0, logger_.getDebug)('android:scrcpy-adapter');
708
+ const SCRCPY_RETRY_COOLDOWN_MS = 5000;
660
709
  class ScrcpyDeviceAdapter {
661
710
  isEnabled() {
662
- if (this.initFailed) return false;
711
+ if (!this.isConfigured()) return false;
712
+ return null === this.retryAfter || Date.now() >= this.retryAfter;
713
+ }
714
+ getStatus() {
715
+ return {
716
+ enabled: this.isConfigured(),
717
+ connected: this.manager?.isConnected() ?? false,
718
+ lastError: this.lastError,
719
+ retryAfter: this.retryAfter
720
+ };
721
+ }
722
+ isConfigured() {
663
723
  return this.scrcpyConfig?.enabled ?? scrcpy_manager.o.enabled;
664
724
  }
665
725
  async initialize(deviceInfo) {
666
726
  try {
667
727
  const manager = await this.ensureManager(deviceInfo);
668
728
  await manager.ensureConnected();
729
+ this.clearFailure();
669
730
  } catch (error) {
670
- this.initFailed = true;
731
+ this.recordFailure(error);
671
732
  throw error;
672
733
  }
673
734
  }
735
+ recordFailure(error) {
736
+ this.lastError = error instanceof Error ? error.message : String(error);
737
+ this.retryAfter = Date.now() + SCRCPY_RETRY_COOLDOWN_MS;
738
+ }
739
+ clearFailure() {
740
+ this.lastError = null;
741
+ this.retryAfter = null;
742
+ }
743
+ ensureRetryReady() {
744
+ if (null === this.retryAfter || Date.now() >= this.retryAfter) return;
745
+ throw new Error(`scrcpy retry is cooling down until ${new Date(this.retryAfter).toISOString()}. Last error: ${this.lastError}`);
746
+ }
674
747
  resolveConfig(deviceInfo) {
675
748
  if (this.resolvedConfig) return this.resolvedConfig;
676
749
  const config = this.scrcpyConfig;
677
750
  const maxSize = config?.maxSize ?? scrcpy_manager.o.maxSize;
678
751
  const videoBitRate = config?.videoBitRate ?? scrcpy_manager.o.videoBitRate;
679
752
  this.resolvedConfig = {
680
- enabled: this.isEnabled(),
753
+ enabled: this.isConfigured(),
681
754
  maxSize,
682
755
  idleTimeoutMs: config?.idleTimeoutMs ?? scrcpy_manager.o.idleTimeoutMs,
683
756
  videoBitRate
@@ -714,14 +787,28 @@ ${stdout ? truncateAdbShellStream(stdout, 'stdout') : EMPTY_ADB_SHELL_STDOUT}`;
714
787
  }
715
788
  }
716
789
  async screenshotBase64(deviceInfo) {
717
- const manager = await this.ensureManager(deviceInfo);
718
- const screenshotBuffer = await manager.getScreenshotJpeg();
719
- return (0, img_namespaceObject.createImgBase64ByFormat)('jpeg', screenshotBuffer.toString('base64'));
790
+ this.ensureRetryReady();
791
+ try {
792
+ const manager = await this.ensureManager(deviceInfo);
793
+ const screenshotBuffer = await manager.getScreenshotJpeg();
794
+ this.clearFailure();
795
+ return (0, img_namespaceObject.createImgBase64ByFormat)('jpeg', screenshotBuffer.toString('base64'));
796
+ } catch (error) {
797
+ this.recordFailure(error);
798
+ throw error;
799
+ }
720
800
  }
721
801
  async subscribeKeyframes(deviceInfo, listener) {
722
- const manager = await this.ensureManager(deviceInfo);
723
- await manager.ensureConnected();
724
- return manager.subscribeKeyframes(listener);
802
+ this.ensureRetryReady();
803
+ try {
804
+ const manager = await this.ensureManager(deviceInfo);
805
+ await manager.ensureConnected();
806
+ this.clearFailure();
807
+ return manager.subscribeKeyframes(listener);
808
+ } catch (error) {
809
+ this.recordFailure(error);
810
+ throw error;
811
+ }
725
812
  }
726
813
  getLatestRawKeyframe() {
727
814
  return this.manager?.getLatestRawKeyframe() ?? null;
@@ -758,18 +845,21 @@ ${stdout ? truncateAdbShellStream(stdout, 'stdout') : EMPTY_ADB_SHELL_STDOUT}`;
758
845
  this.manager = null;
759
846
  }
760
847
  this.resolvedConfig = null;
848
+ this.clearFailure();
761
849
  }
762
850
  constructor(deviceId, scrcpyConfig){
763
851
  _define_property(this, "deviceId", void 0);
764
852
  _define_property(this, "scrcpyConfig", void 0);
765
853
  _define_property(this, "manager", void 0);
766
854
  _define_property(this, "resolvedConfig", void 0);
767
- _define_property(this, "initFailed", void 0);
855
+ _define_property(this, "lastError", void 0);
856
+ _define_property(this, "retryAfter", void 0);
768
857
  this.deviceId = deviceId;
769
858
  this.scrcpyConfig = scrcpyConfig;
770
859
  this.manager = null;
771
860
  this.resolvedConfig = null;
772
- this.initFailed = false;
861
+ this.lastError = null;
862
+ this.retryAfter = null;
773
863
  }
774
864
  }
775
865
  function device_define_property(obj, key, value) {
@@ -896,7 +986,7 @@ ${stdout ? truncateAdbShellStream(stdout, 'stdout') : EMPTY_ADB_SHELL_STDOUT}`;
896
986
  console.log(`[midscene] Using scrcpy for screenshots (device: ${this.deviceId})`);
897
987
  } catch (error) {
898
988
  const msg = error instanceof Error ? error.message : String(error);
899
- warnDevice(`[midscene] Scrcpy unavailable, using ADB fallback (device: ${this.deviceId}): ${msg}`);
989
+ warnDevice(`[midscene] Scrcpy unavailable, using ADB fallback (device: ${this.deviceId}): ${msg}. Call retryScrcpy() to retry immediately.`);
900
990
  }
901
991
  return adb;
902
992
  }
@@ -963,6 +1053,16 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
963
1053
  }
964
1054
  });
965
1055
  }
1056
+ getScrcpyStatus() {
1057
+ return this.getScrcpyAdapter().getStatus();
1058
+ }
1059
+ async retryScrcpy() {
1060
+ const adapter = this.getScrcpyAdapter();
1061
+ if (!adapter.getStatus().enabled) throw new Error('scrcpy is disabled in AndroidDevice options');
1062
+ const deviceInfo = await this.getDevicePhysicalInfo();
1063
+ await adapter.initialize(deviceInfo);
1064
+ return adapter.getStatus();
1065
+ }
966
1066
  getScrcpyAdapter() {
967
1067
  if (!this.scrcpyAdapter) this.scrcpyAdapter = new ScrcpyDeviceAdapter(this.deviceId, this.options?.scrcpyConfig);
968
1068
  return this.scrcpyAdapter;
@@ -2191,7 +2291,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
2191
2291
  const tools = new AndroidMidsceneTools();
2192
2292
  (0, cli_namespaceObject.runToolsCLI)(tools, 'midscene-android', {
2193
2293
  stripPrefix: 'android_',
2194
- version: "1.10.5",
2294
+ version: "1.10.6-beta-20260717061640.0",
2195
2295
  extraCommands: (0, core_namespaceObject.createReportCliCommands)()
2196
2296
  }).catch((e)=>{
2197
2297
  process.exit((0, cli_namespaceObject.reportCLIError)(e));
package/dist/lib/index.js CHANGED
@@ -40,6 +40,8 @@ var __webpack_modules__ = {
40
40
  const KEYFRAME_POLL_INTERVAL_MS = 200;
41
41
  const MAX_SCAN_BYTES = 1000;
42
42
  const CONNECTION_WAIT_MS = 1000;
43
+ const MAX_SERVER_OUTPUT_LINES = 100;
44
+ const SERVER_OUTPUT_DRAIN_TIMEOUT_MS = 500;
43
45
  const BUSY_LOOP_WINDOW_MS = 1000;
44
46
  const BUSY_LOOP_MAX_READS = 500;
45
47
  const BUSY_LOOP_COOLDOWN_MS = 50;
@@ -80,6 +82,8 @@ var __webpack_modules__ = {
80
82
  if (this.scrcpyClient && this.videoStream) return void this.resetIdleTimer();
81
83
  throw new Error('Scrcpy connection failed: another connection attempt did not complete in time');
82
84
  }
85
+ const serverOutput = [];
86
+ let serverOutputTask = null;
83
87
  try {
84
88
  this.isConnecting = true;
85
89
  debugScrcpy('Starting scrcpy connection...');
@@ -98,6 +102,7 @@ var __webpack_modules__ = {
98
102
  videoCodecOptions: 'i-frame-interval=0,bitrate-mode=2'
99
103
  });
100
104
  this.scrcpyClient = await AdbScrcpyClient.start(this.adb, DefaultServerPath, scrcpyOptions);
105
+ serverOutputTask = this.collectServerOutput(this.scrcpyClient.output, serverOutput);
101
106
  const videoStreamPromise = this.scrcpyClient.videoStream;
102
107
  if (!videoStreamPromise) throw new Error('Scrcpy client did not provide video stream');
103
108
  this.videoStream = await videoStreamPromise;
@@ -114,11 +119,50 @@ var __webpack_modules__ = {
114
119
  } catch (error) {
115
120
  debugScrcpy(`Failed to connect scrcpy: ${error}`);
116
121
  await this.disconnect();
117
- throw error;
122
+ if (serverOutputTask) await Promise.race([
123
+ serverOutputTask,
124
+ new Promise((resolve)=>setTimeout(resolve, SERVER_OUTPUT_DRAIN_TIMEOUT_MS))
125
+ ]);
126
+ throw this.createConnectionError(error, serverOutput);
118
127
  } finally{
119
128
  this.isConnecting = false;
120
129
  }
121
130
  }
131
+ async collectServerOutput(output, lines) {
132
+ const reader = output.getReader();
133
+ try {
134
+ while(true){
135
+ const { done, value } = await reader.read();
136
+ if (done) break;
137
+ lines.push(value);
138
+ if (lines.length > MAX_SERVER_OUTPUT_LINES) lines.splice(0, lines.length - MAX_SERVER_OUTPUT_LINES);
139
+ }
140
+ } catch (error) {
141
+ debugScrcpy(`Failed to read scrcpy server output: ${error}`);
142
+ } finally{
143
+ reader.releaseLock();
144
+ }
145
+ }
146
+ createConnectionError(error, serverOutput) {
147
+ const errorOutput = this.getErrorOutput(error);
148
+ const output = [
149
+ ...new Set([
150
+ ...errorOutput,
151
+ ...serverOutput
152
+ ])
153
+ ].filter((line)=>line.trim().length > 0);
154
+ const message = error instanceof Error ? error.message : String(error);
155
+ const outputDetails = output.length > 0 ? `\nScrcpy server output:\n${output.join('\n')}` : '';
156
+ return new Error(`Failed to connect scrcpy: ${message}${outputDetails}`, {
157
+ cause: error
158
+ });
159
+ }
160
+ getErrorOutput(error) {
161
+ if ('object' != typeof error || null === error || !('output' in error)) return [];
162
+ const output = error.output;
163
+ if (!Array.isArray(output)) return [];
164
+ return output.filter((line)=>'string' == typeof line);
165
+ }
122
166
  resolveServerBinPath() {
123
167
  const androidPkgJson = (0, node_module__rspack_import_1.createRequire)(__rslib_import_meta_url__).resolve('@midscene/android/package.json');
124
168
  return node_path__rspack_import_2_default().join(node_path__rspack_import_2_default().dirname(androidPkgJson), 'bin', 'scrcpy-server');
@@ -145,6 +189,7 @@ var __webpack_modules__ = {
145
189
  let windowStart = Date.now();
146
190
  let lastBusyWarn = 0;
147
191
  let totalReads = 0;
192
+ let endReason = 'stream closed';
148
193
  try {
149
194
  while(true){
150
195
  const { done, value } = await reader.read();
@@ -168,10 +213,11 @@ var __webpack_modules__ = {
168
213
  this.processFrame(value);
169
214
  }
170
215
  } catch (error) {
216
+ endReason = 'stream error';
171
217
  debugScrcpy(`Frame consumer error (total reads: ${totalReads}): ${error}`);
172
- await this.disconnect();
173
218
  }
174
- debugScrcpy(`Frame consumer loop ended normally (total reads: ${totalReads})`);
219
+ if (this.streamReader === reader) await this.disconnect();
220
+ debugScrcpy(`Frame consumer loop ended (${endReason}, total reads: ${totalReads})`);
175
221
  }
176
222
  processFrame(packet) {
177
223
  if ('configuration' === packet.type) {
@@ -405,8 +451,10 @@ var __webpack_modules__ = {
405
451
  this.keyframeResolvers = [];
406
452
  this.keyframeListeners.clear();
407
453
  if (reader) try {
408
- reader.cancel();
409
- } catch {}
454
+ await reader.cancel();
455
+ } catch (error) {
456
+ debugScrcpy(`Error cancelling scrcpy stream reader: ${error}`);
457
+ }
410
458
  if (client) try {
411
459
  await client.close();
412
460
  } catch (error) {
@@ -578,27 +626,52 @@ ${stdout ? truncateAdbShellStream(stdout, 'stdout') : EMPTY_ADB_SHELL_STDOUT}`;
578
626
  return obj;
579
627
  }
580
628
  const debugAdapter = (0, logger_.getDebug)('android:scrcpy-adapter');
629
+ const SCRCPY_RETRY_COOLDOWN_MS = 5000;
581
630
  class ScrcpyDeviceAdapter {
582
631
  isEnabled() {
583
- if (this.initFailed) return false;
632
+ if (!this.isConfigured()) return false;
633
+ return null === this.retryAfter || Date.now() >= this.retryAfter;
634
+ }
635
+ getStatus() {
636
+ return {
637
+ enabled: this.isConfigured(),
638
+ connected: this.manager?.isConnected() ?? false,
639
+ lastError: this.lastError,
640
+ retryAfter: this.retryAfter
641
+ };
642
+ }
643
+ isConfigured() {
584
644
  return this.scrcpyConfig?.enabled ?? scrcpy_manager.o.enabled;
585
645
  }
586
646
  async initialize(deviceInfo) {
587
647
  try {
588
648
  const manager = await this.ensureManager(deviceInfo);
589
649
  await manager.ensureConnected();
650
+ this.clearFailure();
590
651
  } catch (error) {
591
- this.initFailed = true;
652
+ this.recordFailure(error);
592
653
  throw error;
593
654
  }
594
655
  }
656
+ recordFailure(error) {
657
+ this.lastError = error instanceof Error ? error.message : String(error);
658
+ this.retryAfter = Date.now() + SCRCPY_RETRY_COOLDOWN_MS;
659
+ }
660
+ clearFailure() {
661
+ this.lastError = null;
662
+ this.retryAfter = null;
663
+ }
664
+ ensureRetryReady() {
665
+ if (null === this.retryAfter || Date.now() >= this.retryAfter) return;
666
+ throw new Error(`scrcpy retry is cooling down until ${new Date(this.retryAfter).toISOString()}. Last error: ${this.lastError}`);
667
+ }
595
668
  resolveConfig(deviceInfo) {
596
669
  if (this.resolvedConfig) return this.resolvedConfig;
597
670
  const config = this.scrcpyConfig;
598
671
  const maxSize = config?.maxSize ?? scrcpy_manager.o.maxSize;
599
672
  const videoBitRate = config?.videoBitRate ?? scrcpy_manager.o.videoBitRate;
600
673
  this.resolvedConfig = {
601
- enabled: this.isEnabled(),
674
+ enabled: this.isConfigured(),
602
675
  maxSize,
603
676
  idleTimeoutMs: config?.idleTimeoutMs ?? scrcpy_manager.o.idleTimeoutMs,
604
677
  videoBitRate
@@ -635,14 +708,28 @@ ${stdout ? truncateAdbShellStream(stdout, 'stdout') : EMPTY_ADB_SHELL_STDOUT}`;
635
708
  }
636
709
  }
637
710
  async screenshotBase64(deviceInfo) {
638
- const manager = await this.ensureManager(deviceInfo);
639
- const screenshotBuffer = await manager.getScreenshotJpeg();
640
- return (0, img_namespaceObject.createImgBase64ByFormat)('jpeg', screenshotBuffer.toString('base64'));
711
+ this.ensureRetryReady();
712
+ try {
713
+ const manager = await this.ensureManager(deviceInfo);
714
+ const screenshotBuffer = await manager.getScreenshotJpeg();
715
+ this.clearFailure();
716
+ return (0, img_namespaceObject.createImgBase64ByFormat)('jpeg', screenshotBuffer.toString('base64'));
717
+ } catch (error) {
718
+ this.recordFailure(error);
719
+ throw error;
720
+ }
641
721
  }
642
722
  async subscribeKeyframes(deviceInfo, listener) {
643
- const manager = await this.ensureManager(deviceInfo);
644
- await manager.ensureConnected();
645
- return manager.subscribeKeyframes(listener);
723
+ this.ensureRetryReady();
724
+ try {
725
+ const manager = await this.ensureManager(deviceInfo);
726
+ await manager.ensureConnected();
727
+ this.clearFailure();
728
+ return manager.subscribeKeyframes(listener);
729
+ } catch (error) {
730
+ this.recordFailure(error);
731
+ throw error;
732
+ }
646
733
  }
647
734
  getLatestRawKeyframe() {
648
735
  return this.manager?.getLatestRawKeyframe() ?? null;
@@ -679,18 +766,21 @@ ${stdout ? truncateAdbShellStream(stdout, 'stdout') : EMPTY_ADB_SHELL_STDOUT}`;
679
766
  this.manager = null;
680
767
  }
681
768
  this.resolvedConfig = null;
769
+ this.clearFailure();
682
770
  }
683
771
  constructor(deviceId, scrcpyConfig){
684
772
  _define_property(this, "deviceId", void 0);
685
773
  _define_property(this, "scrcpyConfig", void 0);
686
774
  _define_property(this, "manager", void 0);
687
775
  _define_property(this, "resolvedConfig", void 0);
688
- _define_property(this, "initFailed", void 0);
776
+ _define_property(this, "lastError", void 0);
777
+ _define_property(this, "retryAfter", void 0);
689
778
  this.deviceId = deviceId;
690
779
  this.scrcpyConfig = scrcpyConfig;
691
780
  this.manager = null;
692
781
  this.resolvedConfig = null;
693
- this.initFailed = false;
782
+ this.lastError = null;
783
+ this.retryAfter = null;
694
784
  }
695
785
  }
696
786
  function device_define_property(obj, key, value) {
@@ -817,7 +907,7 @@ ${stdout ? truncateAdbShellStream(stdout, 'stdout') : EMPTY_ADB_SHELL_STDOUT}`;
817
907
  console.log(`[midscene] Using scrcpy for screenshots (device: ${this.deviceId})`);
818
908
  } catch (error) {
819
909
  const msg = error instanceof Error ? error.message : String(error);
820
- warnDevice(`[midscene] Scrcpy unavailable, using ADB fallback (device: ${this.deviceId}): ${msg}`);
910
+ warnDevice(`[midscene] Scrcpy unavailable, using ADB fallback (device: ${this.deviceId}): ${msg}. Call retryScrcpy() to retry immediately.`);
821
911
  }
822
912
  return adb;
823
913
  }
@@ -884,6 +974,16 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
884
974
  }
885
975
  });
886
976
  }
977
+ getScrcpyStatus() {
978
+ return this.getScrcpyAdapter().getStatus();
979
+ }
980
+ async retryScrcpy() {
981
+ const adapter = this.getScrcpyAdapter();
982
+ if (!adapter.getStatus().enabled) throw new Error('scrcpy is disabled in AndroidDevice options');
983
+ const deviceInfo = await this.getDevicePhysicalInfo();
984
+ await adapter.initialize(deviceInfo);
985
+ return adapter.getStatus();
986
+ }
887
987
  getScrcpyAdapter() {
888
988
  if (!this.scrcpyAdapter) this.scrcpyAdapter = new ScrcpyDeviceAdapter(this.deviceId, this.options?.scrcpyConfig);
889
989
  return this.scrcpyAdapter;
@@ -113,6 +113,10 @@ export declare class AndroidDevice implements AbstractInterface {
113
113
  connect(): Promise<ADB>;
114
114
  getAdb(): Promise<ADB>;
115
115
  private createAdbProxy;
116
+ /** Current scrcpy configuration, connection, and recovery state. */
117
+ getScrcpyStatus(): ScrcpyStatus;
118
+ /** Retry scrcpy initialization without recreating the AndroidDevice. */
119
+ retryScrcpy(): Promise<ScrcpyStatus>;
116
120
  /**
117
121
  * Get or create the scrcpy adapter (lazy initialization)
118
122
  */
@@ -367,14 +371,19 @@ export declare class ScrcpyDeviceAdapter {
367
371
  private scrcpyConfig;
368
372
  private manager;
369
373
  private resolvedConfig;
370
- private initFailed;
374
+ private lastError;
375
+ private retryAfter;
371
376
  constructor(deviceId: string, scrcpyConfig: ScrcpyConfig | undefined);
372
377
  isEnabled(): boolean;
378
+ getStatus(): ScrcpyStatus;
379
+ private isConfigured;
373
380
  /**
374
- * Initialize scrcpy connection. Called once during device.connect().
375
- * If initialization fails, marks scrcpy as permanently disabled (no further retries).
381
+ * Initialize scrcpy connection. Called during device.connect() and explicit retries.
376
382
  */
377
383
  initialize(deviceInfo: DevicePhysicalInfo): Promise<void>;
384
+ private recordFailure;
385
+ private clearFailure;
386
+ private ensureRetryReady;
378
387
  /**
379
388
  * Resolve scrcpy config.
380
389
  * maxSize defaults to 0 (no scaling, full physical resolution) so the Agent layer
@@ -454,6 +463,9 @@ declare class ScrcpyScreenshotManager {
454
463
  * Ensure scrcpy connection is active
455
464
  */
456
465
  ensureConnected(): Promise<void>;
466
+ private collectServerOutput;
467
+ private createConnectionError;
468
+ private getErrorOutput;
457
469
  /**
458
470
  * Resolve path to scrcpy server binary
459
471
  */
@@ -562,6 +574,13 @@ declare interface ScrcpyScreenshotOptions {
562
574
  idleTimeoutMs?: number;
563
575
  }
564
576
 
577
+ export declare interface ScrcpyStatus {
578
+ enabled: boolean;
579
+ connected: boolean;
580
+ lastError: string | null;
581
+ retryAfter: number | null;
582
+ }
583
+
565
584
  declare type ScrollDirection = 'up' | 'down' | 'left' | 'right';
566
585
 
567
586
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@midscene/android",
3
- "version": "1.10.5",
3
+ "version": "1.10.6-beta-20260717061640.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/web-infra-dev/midscene.git",
@@ -41,8 +41,8 @@
41
41
  "@yume-chan/stream-extra": "2.1.0",
42
42
  "appium-adb": "12.12.1",
43
43
  "sharp": "^0.34.3",
44
- "@midscene/shared": "1.10.5",
45
- "@midscene/core": "1.10.5"
44
+ "@midscene/shared": "1.10.6-beta-20260717061640.0",
45
+ "@midscene/core": "1.10.6-beta-20260717061640.0"
46
46
  },
47
47
  "optionalDependencies": {
48
48
  "@ffmpeg-installer/ffmpeg": "^1.1.0"
@@ -56,7 +56,7 @@
56
56
  "undici": "^6.0.0",
57
57
  "vitest": "3.0.5",
58
58
  "zod": "^3.25.1",
59
- "@midscene/playground": "1.10.5"
59
+ "@midscene/playground": "1.10.6-beta-20260717061640.0"
60
60
  },
61
61
  "license": "MIT",
62
62
  "scripts": {