@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 +118 -18
- package/dist/es/index.mjs +117 -17
- package/dist/lib/cli.js +118 -18
- package/dist/lib/index.js +117 -17
- package/dist/types/index.d.ts +22 -3
- package/package.json +4 -4
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
703
|
-
|
|
704
|
-
|
|
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
|
-
|
|
708
|
-
|
|
709
|
-
|
|
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, "
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
606
|
-
|
|
607
|
-
|
|
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
|
-
|
|
611
|
-
|
|
612
|
-
|
|
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, "
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
718
|
-
|
|
719
|
-
|
|
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
|
-
|
|
723
|
-
|
|
724
|
-
|
|
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, "
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
639
|
-
|
|
640
|
-
|
|
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
|
-
|
|
644
|
-
|
|
645
|
-
|
|
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, "
|
|
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.
|
|
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;
|
package/dist/types/index.d.ts
CHANGED
|
@@ -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
|
|
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
|
|
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.
|
|
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.
|
|
45
|
-
"@midscene/core": "1.10.
|
|
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.
|
|
59
|
+
"@midscene/playground": "1.10.6-beta-20260717061640.0"
|
|
60
60
|
},
|
|
61
61
|
"license": "MIT",
|
|
62
62
|
"scripts": {
|