@aiscene/android 1.3.6 → 1.6.0-cache
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/README.md +3 -7
- package/bin/.yadb-version +1 -0
- package/bin/midscene-android +2 -0
- package/bin/scrcpy-server +0 -0
- package/bin/scrcpy-server.version +1 -0
- package/bin/yadb +0 -0
- package/dist/es/cli.mjs +1872 -0
- package/dist/es/index.mjs +126 -69
- package/dist/es/mcp-server.mjs +71 -69
- package/dist/lib/cli.js +1892 -0
- package/dist/lib/index.js +131 -67
- package/dist/lib/mcp-server.js +71 -68
- package/dist/types/cli.d.ts +1 -0
- package/dist/types/index.d.ts +204 -0
- package/package.json +12 -13
package/dist/es/mcp-server.mjs
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import * as __rspack_external__ffmpeg_installer_ffmpeg_acfac5f1 from "@ffmpeg-installer/ffmpeg";
|
|
2
1
|
import * as __rspack_external__midscene_shared_logger_b1dc2426 from "@midscene/shared/logger";
|
|
3
2
|
import * as __rspack_external_node_fs_5ea92f0c from "node:fs";
|
|
4
3
|
import * as __rspack_external_node_module_ab9f2194 from "node:module";
|
|
@@ -34,24 +33,26 @@ var __webpack_modules__ = {
|
|
|
34
33
|
return obj;
|
|
35
34
|
}
|
|
36
35
|
const debugScrcpy = (0, _midscene_shared_logger__rspack_import_3.getDebug)('android:scrcpy');
|
|
36
|
+
const warnScrcpy = (0, _midscene_shared_logger__rspack_import_3.getDebug)('android:scrcpy', {
|
|
37
|
+
console: true
|
|
38
|
+
});
|
|
37
39
|
const NAL_TYPE_IDR = 5;
|
|
38
40
|
const NAL_TYPE_SPS = 7;
|
|
39
41
|
const NAL_TYPE_PPS = 8;
|
|
40
42
|
const NAL_TYPE_MASK = 0x1f;
|
|
41
|
-
const START_CODE_4_BYTE = Buffer.from([
|
|
42
|
-
0x00,
|
|
43
|
-
0x00,
|
|
44
|
-
0x00,
|
|
45
|
-
0x01
|
|
46
|
-
]);
|
|
47
43
|
const DEFAULT_MAX_SIZE = 0;
|
|
48
|
-
const DEFAULT_VIDEO_BIT_RATE =
|
|
44
|
+
const DEFAULT_VIDEO_BIT_RATE = 100000000;
|
|
45
|
+
const MAX_VIDEO_BIT_RATE = 100000000;
|
|
49
46
|
const DEFAULT_IDLE_TIMEOUT_MS = 30000;
|
|
50
47
|
const MAX_KEYFRAME_WAIT_MS = 5000;
|
|
51
48
|
const FRESH_FRAME_TIMEOUT_MS = 300;
|
|
52
49
|
const KEYFRAME_POLL_INTERVAL_MS = 200;
|
|
53
50
|
const MAX_SCAN_BYTES = 1000;
|
|
54
51
|
const CONNECTION_WAIT_MS = 1000;
|
|
52
|
+
const BUSY_LOOP_WINDOW_MS = 1000;
|
|
53
|
+
const BUSY_LOOP_MAX_READS = 500;
|
|
54
|
+
const BUSY_LOOP_COOLDOWN_MS = 50;
|
|
55
|
+
const BUSY_LOOP_WARN_INTERVAL_MS = 5000;
|
|
55
56
|
const DEFAULT_SCRCPY_CONFIG = {
|
|
56
57
|
enabled: false,
|
|
57
58
|
maxSize: DEFAULT_MAX_SIZE,
|
|
@@ -91,21 +92,21 @@ var __webpack_modules__ = {
|
|
|
91
92
|
try {
|
|
92
93
|
this.isConnecting = true;
|
|
93
94
|
debugScrcpy('Starting scrcpy connection...');
|
|
94
|
-
const { AdbScrcpyClient,
|
|
95
|
+
const { AdbScrcpyClient, AdbScrcpyOptions3_3_3 } = await import("@yume-chan/adb-scrcpy");
|
|
95
96
|
const { ReadableStream } = await import("@yume-chan/stream-extra");
|
|
96
|
-
const {
|
|
97
|
-
this.h264SearchConfigFn = h264SearchConfiguration;
|
|
97
|
+
const { DefaultServerPath } = await import("@yume-chan/scrcpy");
|
|
98
98
|
const serverBinPath = this.resolveServerBinPath();
|
|
99
99
|
await AdbScrcpyClient.pushServer(this.adb, ReadableStream.from((0, node_fs__rspack_import_0.createReadStream)(serverBinPath)));
|
|
100
|
-
const scrcpyOptions = new
|
|
100
|
+
const scrcpyOptions = new AdbScrcpyOptions3_3_3({
|
|
101
101
|
audio: false,
|
|
102
102
|
control: false,
|
|
103
103
|
maxSize: this.options.maxSize,
|
|
104
104
|
videoBitRate: this.options.videoBitRate,
|
|
105
|
-
|
|
106
|
-
|
|
105
|
+
maxFps: 10,
|
|
106
|
+
sendFrameMeta: true,
|
|
107
|
+
videoCodecOptions: 'i-frame-interval=0,bitrate-mode=2'
|
|
107
108
|
});
|
|
108
|
-
this.scrcpyClient = await AdbScrcpyClient.start(this.adb, DefaultServerPath,
|
|
109
|
+
this.scrcpyClient = await AdbScrcpyClient.start(this.adb, DefaultServerPath, scrcpyOptions);
|
|
109
110
|
const videoStreamPromise = this.scrcpyClient.videoStream;
|
|
110
111
|
if (!videoStreamPromise) throw new Error('Scrcpy client did not provide video stream');
|
|
111
112
|
this.videoStream = await videoStreamPromise;
|
|
@@ -133,7 +134,8 @@ var __webpack_modules__ = {
|
|
|
133
134
|
}
|
|
134
135
|
getFfmpegPath() {
|
|
135
136
|
try {
|
|
136
|
-
const
|
|
137
|
+
const dynamicRequire = (0, node_module__rspack_import_1.createRequire)(import.meta.url);
|
|
138
|
+
const ffmpegInstaller = dynamicRequire('@ffmpeg-installer/ffmpeg');
|
|
137
139
|
debugScrcpy(`Using ffmpeg from npm package: ${ffmpegInstaller.path}`);
|
|
138
140
|
return ffmpegInstaller.path;
|
|
139
141
|
} catch (error) {
|
|
@@ -148,22 +150,47 @@ var __webpack_modules__ = {
|
|
|
148
150
|
this.consumeFramesLoop(reader);
|
|
149
151
|
}
|
|
150
152
|
async consumeFramesLoop(reader) {
|
|
153
|
+
let readCount = 0;
|
|
154
|
+
let windowStart = Date.now();
|
|
155
|
+
let lastBusyWarn = 0;
|
|
156
|
+
let totalReads = 0;
|
|
151
157
|
try {
|
|
152
158
|
while(true){
|
|
153
159
|
const { done, value } = await reader.read();
|
|
154
160
|
if (done) break;
|
|
161
|
+
totalReads++;
|
|
162
|
+
readCount++;
|
|
163
|
+
const now = Date.now();
|
|
164
|
+
const elapsed = now - windowStart;
|
|
165
|
+
if (elapsed >= BUSY_LOOP_WINDOW_MS) {
|
|
166
|
+
const readsPerSec = readCount / elapsed * 1000;
|
|
167
|
+
if (readCount > BUSY_LOOP_MAX_READS) {
|
|
168
|
+
if (now - lastBusyWarn >= BUSY_LOOP_WARN_INTERVAL_MS) {
|
|
169
|
+
warnScrcpy(`[CPU-DIAG] Possible busy loop detected! ${readCount} reads in ${elapsed}ms (${readsPerSec.toFixed(0)} reads/sec). Total reads: ${totalReads}. Throttling with ${BUSY_LOOP_COOLDOWN_MS}ms delay.`);
|
|
170
|
+
lastBusyWarn = now;
|
|
171
|
+
}
|
|
172
|
+
await new Promise((resolve)=>setTimeout(resolve, BUSY_LOOP_COOLDOWN_MS));
|
|
173
|
+
} else debugScrcpy(`[CPU-DIAG] Frame loop stats: ${readCount} reads in ${elapsed}ms (${readsPerSec.toFixed(1)} reads/sec), total: ${totalReads}`);
|
|
174
|
+
readCount = 0;
|
|
175
|
+
windowStart = Date.now();
|
|
176
|
+
}
|
|
155
177
|
this.processFrame(value);
|
|
156
178
|
}
|
|
157
179
|
} catch (error) {
|
|
158
|
-
debugScrcpy(`Frame consumer error: ${error}`);
|
|
180
|
+
debugScrcpy(`Frame consumer error (total reads: ${totalReads}): ${error}`);
|
|
159
181
|
await this.disconnect();
|
|
160
182
|
}
|
|
183
|
+
debugScrcpy(`Frame consumer loop ended normally (total reads: ${totalReads})`);
|
|
161
184
|
}
|
|
162
185
|
processFrame(packet) {
|
|
186
|
+
if ('configuration' === packet.type) {
|
|
187
|
+
this.spsHeader = Buffer.from(packet.data);
|
|
188
|
+
debugScrcpy(`Received SPS/PPS configuration: ${this.spsHeader.length}B`);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
163
191
|
const frameBuffer = Buffer.from(packet.data);
|
|
164
|
-
const
|
|
165
|
-
if (
|
|
166
|
-
if (actualKeyFrame && this.spsHeader) {
|
|
192
|
+
const isKeyFrame = detectH264KeyFrame(frameBuffer);
|
|
193
|
+
if (isKeyFrame && this.spsHeader) {
|
|
167
194
|
this.lastRawKeyframe = frameBuffer;
|
|
168
195
|
if (this.keyframeResolvers.length > 0) {
|
|
169
196
|
const combined = Buffer.concat([
|
|
@@ -174,23 +201,7 @@ var __webpack_modules__ = {
|
|
|
174
201
|
}
|
|
175
202
|
}
|
|
176
203
|
}
|
|
177
|
-
|
|
178
|
-
if (!this.h264SearchConfigFn) return;
|
|
179
|
-
try {
|
|
180
|
-
const config = this.h264SearchConfigFn(new Uint8Array(frameBuffer));
|
|
181
|
-
if (!config.sequenceParameterSet || !config.pictureParameterSet) return;
|
|
182
|
-
this.spsHeader = Buffer.concat([
|
|
183
|
-
START_CODE_4_BYTE,
|
|
184
|
-
Buffer.from(config.sequenceParameterSet),
|
|
185
|
-
START_CODE_4_BYTE,
|
|
186
|
-
Buffer.from(config.pictureParameterSet)
|
|
187
|
-
]);
|
|
188
|
-
debugScrcpy(`Extracted SPS/PPS: SPS=${config.sequenceParameterSet.length}B, PPS=${config.pictureParameterSet.length}B, total=${this.spsHeader.length}B`);
|
|
189
|
-
} catch (error) {
|
|
190
|
-
debugScrcpy(`Failed to extract SPS/PPS from keyframe: ${error}`);
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
async getScreenshotPng() {
|
|
204
|
+
async getScreenshotJpeg() {
|
|
194
205
|
const perfStart = Date.now();
|
|
195
206
|
const t1 = Date.now();
|
|
196
207
|
await this.ensureConnected();
|
|
@@ -220,7 +231,7 @@ var __webpack_modules__ = {
|
|
|
220
231
|
this.resetIdleTimer();
|
|
221
232
|
debugScrcpy(`Decoding H.264 stream: ${keyframeBuffer.length} bytes (${frameSource})`);
|
|
222
233
|
const t4 = Date.now();
|
|
223
|
-
const result = await this.
|
|
234
|
+
const result = await this.decodeH264ToJpeg(keyframeBuffer);
|
|
224
235
|
const decodeTime = Date.now() - t4;
|
|
225
236
|
const totalTime = Date.now() - perfStart;
|
|
226
237
|
debugScrcpy(`Performance: total=${totalTime}ms (connect=${connectTime}ms, spsWait=${spsWaitTime}ms, frameWait=${frameWaitTime}ms[${frameSource}], decode=${decodeTime}ms)`);
|
|
@@ -283,7 +294,7 @@ var __webpack_modules__ = {
|
|
|
283
294
|
return false;
|
|
284
295
|
}
|
|
285
296
|
}
|
|
286
|
-
async
|
|
297
|
+
async decodeH264ToJpeg(h264Buffer) {
|
|
287
298
|
const { spawn } = await import("node:child_process");
|
|
288
299
|
return new Promise((resolve, reject)=>{
|
|
289
300
|
const ffmpegArgs = [
|
|
@@ -296,7 +307,9 @@ var __webpack_modules__ = {
|
|
|
296
307
|
'-f',
|
|
297
308
|
'image2pipe',
|
|
298
309
|
'-vcodec',
|
|
299
|
-
'
|
|
310
|
+
'mjpeg',
|
|
311
|
+
'-q:v',
|
|
312
|
+
'5',
|
|
300
313
|
'-loglevel',
|
|
301
314
|
'error',
|
|
302
315
|
'pipe:1'
|
|
@@ -319,13 +332,13 @@ var __webpack_modules__ = {
|
|
|
319
332
|
});
|
|
320
333
|
ffmpeg.on('close', (code)=>{
|
|
321
334
|
if (0 === code && chunks.length > 0) {
|
|
322
|
-
const
|
|
323
|
-
debugScrcpy(`FFmpeg decode successful,
|
|
324
|
-
resolve(
|
|
335
|
+
const jpegBuffer = Buffer.concat(chunks);
|
|
336
|
+
debugScrcpy(`FFmpeg decode successful, JPEG size: ${jpegBuffer.length} bytes`);
|
|
337
|
+
resolve(jpegBuffer);
|
|
325
338
|
} else {
|
|
326
339
|
const errorMsg = stderrOutput || `FFmpeg exited with code ${code}`;
|
|
327
340
|
debugScrcpy(`FFmpeg decode failed: ${errorMsg}`);
|
|
328
|
-
reject(new Error(`H.264 to
|
|
341
|
+
reject(new Error(`H.264 to JPEG decode failed: ${errorMsg}`));
|
|
329
342
|
}
|
|
330
343
|
});
|
|
331
344
|
ffmpeg.on('error', (error)=>{
|
|
@@ -357,7 +370,6 @@ var __webpack_modules__ = {
|
|
|
357
370
|
this.spsHeader = null;
|
|
358
371
|
this.lastRawKeyframe = null;
|
|
359
372
|
this.isInitialized = false;
|
|
360
|
-
this.h264SearchConfigFn = null;
|
|
361
373
|
this.keyframeResolvers = [];
|
|
362
374
|
if (reader) try {
|
|
363
375
|
reader.cancel();
|
|
@@ -385,20 +397,19 @@ var __webpack_modules__ = {
|
|
|
385
397
|
_define_property(this, "keyframeResolvers", []);
|
|
386
398
|
_define_property(this, "lastRawKeyframe", null);
|
|
387
399
|
_define_property(this, "videoResolution", null);
|
|
388
|
-
_define_property(this, "h264SearchConfigFn", null);
|
|
389
400
|
_define_property(this, "streamReader", null);
|
|
390
401
|
this.adb = adb;
|
|
402
|
+
const requestedBitRate = options.videoBitRate ?? DEFAULT_VIDEO_BIT_RATE;
|
|
403
|
+
const clampedBitRate = Math.min(requestedBitRate, MAX_VIDEO_BIT_RATE);
|
|
404
|
+
if (requestedBitRate > MAX_VIDEO_BIT_RATE) warnScrcpy(`videoBitRate ${requestedBitRate} exceeds maximum ${MAX_VIDEO_BIT_RATE}, clamped to ${clampedBitRate}`);
|
|
391
405
|
this.options = {
|
|
392
406
|
maxSize: options.maxSize ?? DEFAULT_MAX_SIZE,
|
|
393
|
-
videoBitRate:
|
|
407
|
+
videoBitRate: clampedBitRate,
|
|
394
408
|
idleTimeoutMs: options.idleTimeoutMs ?? DEFAULT_IDLE_TIMEOUT_MS
|
|
395
409
|
};
|
|
396
410
|
}
|
|
397
411
|
}
|
|
398
412
|
},
|
|
399
|
-
"@ffmpeg-installer/ffmpeg" (module) {
|
|
400
|
-
module.exports = __rspack_external__ffmpeg_installer_ffmpeg_acfac5f1;
|
|
401
|
-
},
|
|
402
413
|
"@midscene/shared/logger" (module) {
|
|
403
414
|
module.exports = __rspack_external__midscene_shared_logger_b1dc2426;
|
|
404
415
|
},
|
|
@@ -562,18 +573,13 @@ class ScrcpyDeviceAdapter {
|
|
|
562
573
|
resolveConfig(deviceInfo) {
|
|
563
574
|
if (this.resolvedConfig) return this.resolvedConfig;
|
|
564
575
|
const config = this.scrcpyConfig;
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
const physicalMax = Math.max(deviceInfo.physicalWidth, deviceInfo.physicalHeight);
|
|
568
|
-
const scale = this.screenshotResizeScale ?? 1 / deviceInfo.dpr;
|
|
569
|
-
maxSize = Math.round(physicalMax * scale);
|
|
570
|
-
debugAdapter(`Auto-calculated maxSize: ${maxSize} (physical=${physicalMax}, scale=${scale.toFixed(3)}, ${void 0 !== this.screenshotResizeScale ? 'from screenshotResizeScale' : 'from 1/dpr'})`);
|
|
571
|
-
}
|
|
576
|
+
const maxSize = config?.maxSize ?? scrcpy_manager.o.maxSize;
|
|
577
|
+
const videoBitRate = config?.videoBitRate ?? scrcpy_manager.o.videoBitRate;
|
|
572
578
|
this.resolvedConfig = {
|
|
573
579
|
enabled: this.isEnabled(),
|
|
574
580
|
maxSize,
|
|
575
581
|
idleTimeoutMs: config?.idleTimeoutMs ?? scrcpy_manager.o.idleTimeoutMs,
|
|
576
|
-
videoBitRate
|
|
582
|
+
videoBitRate
|
|
577
583
|
};
|
|
578
584
|
return this.resolvedConfig;
|
|
579
585
|
}
|
|
@@ -608,8 +614,8 @@ class ScrcpyDeviceAdapter {
|
|
|
608
614
|
}
|
|
609
615
|
async screenshotBase64(deviceInfo) {
|
|
610
616
|
const manager = await this.ensureManager(deviceInfo);
|
|
611
|
-
const screenshotBuffer = await manager.
|
|
612
|
-
return createImgBase64ByFormat('
|
|
617
|
+
const screenshotBuffer = await manager.getScreenshotJpeg();
|
|
618
|
+
return createImgBase64ByFormat('jpeg', screenshotBuffer.toString('base64'));
|
|
613
619
|
}
|
|
614
620
|
getResolution() {
|
|
615
621
|
return this.manager?.getResolution() ?? null;
|
|
@@ -620,8 +626,7 @@ class ScrcpyDeviceAdapter {
|
|
|
620
626
|
debugAdapter(`Using scrcpy resolution: ${resolution.width}x${resolution.height}`);
|
|
621
627
|
return {
|
|
622
628
|
width: resolution.width,
|
|
623
|
-
height: resolution.height
|
|
624
|
-
dpr: deviceInfo.dpr
|
|
629
|
+
height: resolution.height
|
|
625
630
|
};
|
|
626
631
|
}
|
|
627
632
|
getScalingRatio(physicalWidth) {
|
|
@@ -640,16 +645,14 @@ class ScrcpyDeviceAdapter {
|
|
|
640
645
|
}
|
|
641
646
|
this.resolvedConfig = null;
|
|
642
647
|
}
|
|
643
|
-
constructor(deviceId, scrcpyConfig
|
|
648
|
+
constructor(deviceId, scrcpyConfig){
|
|
644
649
|
_define_property(this, "deviceId", void 0);
|
|
645
650
|
_define_property(this, "scrcpyConfig", void 0);
|
|
646
|
-
_define_property(this, "screenshotResizeScale", void 0);
|
|
647
651
|
_define_property(this, "manager", void 0);
|
|
648
652
|
_define_property(this, "resolvedConfig", void 0);
|
|
649
653
|
_define_property(this, "initFailed", void 0);
|
|
650
654
|
this.deviceId = deviceId;
|
|
651
655
|
this.scrcpyConfig = scrcpyConfig;
|
|
652
|
-
this.screenshotResizeScale = screenshotResizeScale;
|
|
653
656
|
this.manager = null;
|
|
654
657
|
this.resolvedConfig = null;
|
|
655
658
|
this.initFailed = false;
|
|
@@ -876,7 +879,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
876
879
|
});
|
|
877
880
|
}
|
|
878
881
|
getScrcpyAdapter() {
|
|
879
|
-
if (!this.scrcpyAdapter) this.scrcpyAdapter = new ScrcpyDeviceAdapter(this.deviceId, this.options?.scrcpyConfig
|
|
882
|
+
if (!this.scrcpyAdapter) this.scrcpyAdapter = new ScrcpyDeviceAdapter(this.deviceId, this.options?.scrcpyConfig);
|
|
880
883
|
return this.scrcpyAdapter;
|
|
881
884
|
}
|
|
882
885
|
async getDevicePhysicalInfo() {
|
|
@@ -1122,8 +1125,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1122
1125
|
const logicalHeight = Math.round(height * scale);
|
|
1123
1126
|
return {
|
|
1124
1127
|
width: logicalWidth,
|
|
1125
|
-
height: logicalHeight
|
|
1126
|
-
dpr: this.devicePixelRatio
|
|
1128
|
+
height: logicalHeight
|
|
1127
1129
|
};
|
|
1128
1130
|
}
|
|
1129
1131
|
async cacheFeatureForPoint(center, options) {
|
|
@@ -1866,7 +1868,7 @@ class AndroidMCPServer extends BaseMCPServer {
|
|
|
1866
1868
|
constructor(toolsManager){
|
|
1867
1869
|
super({
|
|
1868
1870
|
name: '@midscene/android-mcp',
|
|
1869
|
-
version:
|
|
1871
|
+
version: "1.6.0-cache",
|
|
1870
1872
|
description: 'Control the Android device using natural language commands'
|
|
1871
1873
|
}, toolsManager);
|
|
1872
1874
|
}
|