@aiscene/android 1.3.5 → 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/dist/es/index.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";
@@ -12,6 +11,7 @@ import { createImgBase64ByFormat, isValidPNGImageBuffer } from "@midscene/shared
12
11
  import { mergeAndNormalizeAppNameMapping, normalizeForComparison, repeat } from "@midscene/shared/utils";
13
12
  import { ADB } from "appium-adb";
14
13
  import { Agent } from "@midscene/core/agent";
14
+ import { BaseMidsceneTools } from "@midscene/shared/mcp";
15
15
  var __webpack_modules__ = {
16
16
  "./src/scrcpy-manager.ts" (__unused_rspack_module, __webpack_exports__, __webpack_require__) {
17
17
  __webpack_require__.d(__webpack_exports__, {
@@ -33,24 +33,26 @@ var __webpack_modules__ = {
33
33
  return obj;
34
34
  }
35
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
+ });
36
39
  const NAL_TYPE_IDR = 5;
37
40
  const NAL_TYPE_SPS = 7;
38
41
  const NAL_TYPE_PPS = 8;
39
42
  const NAL_TYPE_MASK = 0x1f;
40
- const START_CODE_4_BYTE = Buffer.from([
41
- 0x00,
42
- 0x00,
43
- 0x00,
44
- 0x01
45
- ]);
46
43
  const DEFAULT_MAX_SIZE = 0;
47
- const DEFAULT_VIDEO_BIT_RATE = 2000000;
44
+ const DEFAULT_VIDEO_BIT_RATE = 100000000;
45
+ const MAX_VIDEO_BIT_RATE = 100000000;
48
46
  const DEFAULT_IDLE_TIMEOUT_MS = 30000;
49
47
  const MAX_KEYFRAME_WAIT_MS = 5000;
50
48
  const FRESH_FRAME_TIMEOUT_MS = 300;
51
49
  const KEYFRAME_POLL_INTERVAL_MS = 200;
52
50
  const MAX_SCAN_BYTES = 1000;
53
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;
54
56
  const DEFAULT_SCRCPY_CONFIG = {
55
57
  enabled: false,
56
58
  maxSize: DEFAULT_MAX_SIZE,
@@ -90,21 +92,21 @@ var __webpack_modules__ = {
90
92
  try {
91
93
  this.isConnecting = true;
92
94
  debugScrcpy('Starting scrcpy connection...');
93
- const { AdbScrcpyClient, AdbScrcpyOptions2_1 } = await import("@yume-chan/adb-scrcpy");
95
+ const { AdbScrcpyClient, AdbScrcpyOptions3_3_3 } = await import("@yume-chan/adb-scrcpy");
94
96
  const { ReadableStream } = await import("@yume-chan/stream-extra");
95
- const { ScrcpyOptions3_1, DefaultServerPath, h264SearchConfiguration } = await import("@yume-chan/scrcpy");
96
- this.h264SearchConfigFn = h264SearchConfiguration;
97
+ const { DefaultServerPath } = await import("@yume-chan/scrcpy");
97
98
  const serverBinPath = this.resolveServerBinPath();
98
99
  await AdbScrcpyClient.pushServer(this.adb, ReadableStream.from((0, node_fs__rspack_import_0.createReadStream)(serverBinPath)));
99
- const scrcpyOptions = new ScrcpyOptions3_1({
100
+ const scrcpyOptions = new AdbScrcpyOptions3_3_3({
100
101
  audio: false,
101
102
  control: false,
102
103
  maxSize: this.options.maxSize,
103
104
  videoBitRate: this.options.videoBitRate,
104
- sendFrameMeta: false,
105
- videoCodecOptions: 'i-frame-interval=0'
105
+ maxFps: 10,
106
+ sendFrameMeta: true,
107
+ videoCodecOptions: 'i-frame-interval=0,bitrate-mode=2'
106
108
  });
107
- this.scrcpyClient = await AdbScrcpyClient.start(this.adb, DefaultServerPath, new AdbScrcpyOptions2_1(scrcpyOptions));
109
+ this.scrcpyClient = await AdbScrcpyClient.start(this.adb, DefaultServerPath, scrcpyOptions);
108
110
  const videoStreamPromise = this.scrcpyClient.videoStream;
109
111
  if (!videoStreamPromise) throw new Error('Scrcpy client did not provide video stream');
110
112
  this.videoStream = await videoStreamPromise;
@@ -127,12 +129,13 @@ var __webpack_modules__ = {
127
129
  }
128
130
  }
129
131
  resolveServerBinPath() {
130
- const androidPkgJson = (0, node_module__rspack_import_1.createRequire)(import.meta.url).resolve('@midscene/android/package.json');
132
+ const androidPkgJson = (0, node_module__rspack_import_1.createRequire)(import.meta.url).resolve('@aiscene/android/package.json');
131
133
  return node_path__rspack_import_2["default"].join(node_path__rspack_import_2["default"].dirname(androidPkgJson), 'bin', 'scrcpy-server');
132
134
  }
133
135
  getFfmpegPath() {
134
136
  try {
135
- const ffmpegInstaller = __webpack_require__("@ffmpeg-installer/ffmpeg");
137
+ const dynamicRequire = (0, node_module__rspack_import_1.createRequire)(import.meta.url);
138
+ const ffmpegInstaller = dynamicRequire('@ffmpeg-installer/ffmpeg');
136
139
  debugScrcpy(`Using ffmpeg from npm package: ${ffmpegInstaller.path}`);
137
140
  return ffmpegInstaller.path;
138
141
  } catch (error) {
@@ -147,22 +150,47 @@ var __webpack_modules__ = {
147
150
  this.consumeFramesLoop(reader);
148
151
  }
149
152
  async consumeFramesLoop(reader) {
153
+ let readCount = 0;
154
+ let windowStart = Date.now();
155
+ let lastBusyWarn = 0;
156
+ let totalReads = 0;
150
157
  try {
151
158
  while(true){
152
159
  const { done, value } = await reader.read();
153
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
+ }
154
177
  this.processFrame(value);
155
178
  }
156
179
  } catch (error) {
157
- debugScrcpy(`Frame consumer error: ${error}`);
180
+ debugScrcpy(`Frame consumer error (total reads: ${totalReads}): ${error}`);
158
181
  await this.disconnect();
159
182
  }
183
+ debugScrcpy(`Frame consumer loop ended normally (total reads: ${totalReads})`);
160
184
  }
161
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
+ }
162
191
  const frameBuffer = Buffer.from(packet.data);
163
- const actualKeyFrame = detectH264KeyFrame(frameBuffer);
164
- if (actualKeyFrame && !this.spsHeader) this.extractSpsHeader(frameBuffer);
165
- if (actualKeyFrame && this.spsHeader) {
192
+ const isKeyFrame = detectH264KeyFrame(frameBuffer);
193
+ if (isKeyFrame && this.spsHeader) {
166
194
  this.lastRawKeyframe = frameBuffer;
167
195
  if (this.keyframeResolvers.length > 0) {
168
196
  const combined = Buffer.concat([
@@ -173,23 +201,7 @@ var __webpack_modules__ = {
173
201
  }
174
202
  }
175
203
  }
176
- extractSpsHeader(frameBuffer) {
177
- if (!this.h264SearchConfigFn) return;
178
- try {
179
- const config = this.h264SearchConfigFn(new Uint8Array(frameBuffer));
180
- if (!config.sequenceParameterSet || !config.pictureParameterSet) return;
181
- this.spsHeader = Buffer.concat([
182
- START_CODE_4_BYTE,
183
- Buffer.from(config.sequenceParameterSet),
184
- START_CODE_4_BYTE,
185
- Buffer.from(config.pictureParameterSet)
186
- ]);
187
- debugScrcpy(`Extracted SPS/PPS: SPS=${config.sequenceParameterSet.length}B, PPS=${config.pictureParameterSet.length}B, total=${this.spsHeader.length}B`);
188
- } catch (error) {
189
- debugScrcpy(`Failed to extract SPS/PPS from keyframe: ${error}`);
190
- }
191
- }
192
- async getScreenshotPng() {
204
+ async getScreenshotJpeg() {
193
205
  const perfStart = Date.now();
194
206
  const t1 = Date.now();
195
207
  await this.ensureConnected();
@@ -219,7 +231,7 @@ var __webpack_modules__ = {
219
231
  this.resetIdleTimer();
220
232
  debugScrcpy(`Decoding H.264 stream: ${keyframeBuffer.length} bytes (${frameSource})`);
221
233
  const t4 = Date.now();
222
- const result = await this.decodeH264ToPng(keyframeBuffer);
234
+ const result = await this.decodeH264ToJpeg(keyframeBuffer);
223
235
  const decodeTime = Date.now() - t4;
224
236
  const totalTime = Date.now() - perfStart;
225
237
  debugScrcpy(`Performance: total=${totalTime}ms (connect=${connectTime}ms, spsWait=${spsWaitTime}ms, frameWait=${frameWaitTime}ms[${frameSource}], decode=${decodeTime}ms)`);
@@ -282,7 +294,7 @@ var __webpack_modules__ = {
282
294
  return false;
283
295
  }
284
296
  }
285
- async decodeH264ToPng(h264Buffer) {
297
+ async decodeH264ToJpeg(h264Buffer) {
286
298
  const { spawn } = await import("node:child_process");
287
299
  return new Promise((resolve, reject)=>{
288
300
  const ffmpegArgs = [
@@ -295,7 +307,9 @@ var __webpack_modules__ = {
295
307
  '-f',
296
308
  'image2pipe',
297
309
  '-vcodec',
298
- 'png',
310
+ 'mjpeg',
311
+ '-q:v',
312
+ '5',
299
313
  '-loglevel',
300
314
  'error',
301
315
  'pipe:1'
@@ -318,13 +332,13 @@ var __webpack_modules__ = {
318
332
  });
319
333
  ffmpeg.on('close', (code)=>{
320
334
  if (0 === code && chunks.length > 0) {
321
- const pngBuffer = Buffer.concat(chunks);
322
- debugScrcpy(`FFmpeg decode successful, PNG size: ${pngBuffer.length} bytes`);
323
- resolve(pngBuffer);
335
+ const jpegBuffer = Buffer.concat(chunks);
336
+ debugScrcpy(`FFmpeg decode successful, JPEG size: ${jpegBuffer.length} bytes`);
337
+ resolve(jpegBuffer);
324
338
  } else {
325
339
  const errorMsg = stderrOutput || `FFmpeg exited with code ${code}`;
326
340
  debugScrcpy(`FFmpeg decode failed: ${errorMsg}`);
327
- reject(new Error(`H.264 to PNG decode failed: ${errorMsg}`));
341
+ reject(new Error(`H.264 to JPEG decode failed: ${errorMsg}`));
328
342
  }
329
343
  });
330
344
  ffmpeg.on('error', (error)=>{
@@ -356,7 +370,6 @@ var __webpack_modules__ = {
356
370
  this.spsHeader = null;
357
371
  this.lastRawKeyframe = null;
358
372
  this.isInitialized = false;
359
- this.h264SearchConfigFn = null;
360
373
  this.keyframeResolvers = [];
361
374
  if (reader) try {
362
375
  reader.cancel();
@@ -384,20 +397,19 @@ var __webpack_modules__ = {
384
397
  _define_property(this, "keyframeResolvers", []);
385
398
  _define_property(this, "lastRawKeyframe", null);
386
399
  _define_property(this, "videoResolution", null);
387
- _define_property(this, "h264SearchConfigFn", null);
388
400
  _define_property(this, "streamReader", null);
389
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}`);
390
405
  this.options = {
391
406
  maxSize: options.maxSize ?? DEFAULT_MAX_SIZE,
392
- videoBitRate: options.videoBitRate ?? DEFAULT_VIDEO_BIT_RATE,
407
+ videoBitRate: clampedBitRate,
393
408
  idleTimeoutMs: options.idleTimeoutMs ?? DEFAULT_IDLE_TIMEOUT_MS
394
409
  };
395
410
  }
396
411
  }
397
412
  },
398
- "@ffmpeg-installer/ffmpeg" (module) {
399
- module.exports = __rspack_external__ffmpeg_installer_ffmpeg_acfac5f1;
400
- },
401
413
  "@midscene/shared/logger" (module) {
402
414
  module.exports = __rspack_external__midscene_shared_logger_b1dc2426;
403
415
  },
@@ -465,18 +477,13 @@ class ScrcpyDeviceAdapter {
465
477
  resolveConfig(deviceInfo) {
466
478
  if (this.resolvedConfig) return this.resolvedConfig;
467
479
  const config = this.scrcpyConfig;
468
- let maxSize = config?.maxSize ?? scrcpy_manager.o.maxSize;
469
- if (config?.maxSize === void 0) {
470
- const physicalMax = Math.max(deviceInfo.physicalWidth, deviceInfo.physicalHeight);
471
- const scale = this.screenshotResizeScale ?? 1 / deviceInfo.dpr;
472
- maxSize = Math.round(physicalMax * scale);
473
- debugAdapter(`Auto-calculated maxSize: ${maxSize} (physical=${physicalMax}, scale=${scale.toFixed(3)}, ${void 0 !== this.screenshotResizeScale ? 'from screenshotResizeScale' : 'from 1/dpr'})`);
474
- }
480
+ const maxSize = config?.maxSize ?? scrcpy_manager.o.maxSize;
481
+ const videoBitRate = config?.videoBitRate ?? scrcpy_manager.o.videoBitRate;
475
482
  this.resolvedConfig = {
476
483
  enabled: this.isEnabled(),
477
484
  maxSize,
478
485
  idleTimeoutMs: config?.idleTimeoutMs ?? scrcpy_manager.o.idleTimeoutMs,
479
- videoBitRate: config?.videoBitRate ?? scrcpy_manager.o.videoBitRate
486
+ videoBitRate
480
487
  };
481
488
  return this.resolvedConfig;
482
489
  }
@@ -511,8 +518,8 @@ class ScrcpyDeviceAdapter {
511
518
  }
512
519
  async screenshotBase64(deviceInfo) {
513
520
  const manager = await this.ensureManager(deviceInfo);
514
- const screenshotBuffer = await manager.getScreenshotPng();
515
- return createImgBase64ByFormat('png', screenshotBuffer.toString('base64'));
521
+ const screenshotBuffer = await manager.getScreenshotJpeg();
522
+ return createImgBase64ByFormat('jpeg', screenshotBuffer.toString('base64'));
516
523
  }
517
524
  getResolution() {
518
525
  return this.manager?.getResolution() ?? null;
@@ -523,8 +530,7 @@ class ScrcpyDeviceAdapter {
523
530
  debugAdapter(`Using scrcpy resolution: ${resolution.width}x${resolution.height}`);
524
531
  return {
525
532
  width: resolution.width,
526
- height: resolution.height,
527
- dpr: deviceInfo.dpr
533
+ height: resolution.height
528
534
  };
529
535
  }
530
536
  getScalingRatio(physicalWidth) {
@@ -543,16 +549,14 @@ class ScrcpyDeviceAdapter {
543
549
  }
544
550
  this.resolvedConfig = null;
545
551
  }
546
- constructor(deviceId, scrcpyConfig, screenshotResizeScale){
552
+ constructor(deviceId, scrcpyConfig){
547
553
  _define_property(this, "deviceId", void 0);
548
554
  _define_property(this, "scrcpyConfig", void 0);
549
- _define_property(this, "screenshotResizeScale", void 0);
550
555
  _define_property(this, "manager", void 0);
551
556
  _define_property(this, "resolvedConfig", void 0);
552
557
  _define_property(this, "initFailed", void 0);
553
558
  this.deviceId = deviceId;
554
559
  this.scrcpyConfig = scrcpyConfig;
555
- this.screenshotResizeScale = screenshotResizeScale;
556
560
  this.manager = null;
557
561
  this.resolvedConfig = null;
558
562
  this.initFailed = false;
@@ -779,7 +783,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
779
783
  });
780
784
  }
781
785
  getScrcpyAdapter() {
782
- if (!this.scrcpyAdapter) this.scrcpyAdapter = new ScrcpyDeviceAdapter(this.deviceId, this.options?.scrcpyConfig, this.options?.screenshotResizeScale);
786
+ if (!this.scrcpyAdapter) this.scrcpyAdapter = new ScrcpyDeviceAdapter(this.deviceId, this.options?.scrcpyConfig);
783
787
  return this.scrcpyAdapter;
784
788
  }
785
789
  async getDevicePhysicalInfo() {
@@ -1025,8 +1029,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1025
1029
  const logicalHeight = Math.round(height * scale);
1026
1030
  return {
1027
1031
  width: logicalWidth,
1028
- height: logicalHeight,
1029
- dpr: this.devicePixelRatio
1032
+ height: logicalHeight
1030
1033
  };
1031
1034
  }
1032
1035
  async cacheFeatureForPoint(center, options) {
@@ -1312,7 +1315,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1312
1315
  async ensureYadb() {
1313
1316
  if (!this.yadbPushed) {
1314
1317
  const adb = await this.getAdb();
1315
- const androidPkgJson = (0, external_node_module_.createRequire)(import.meta.url).resolve('@midscene/android/package.json');
1318
+ const androidPkgJson = (0, external_node_module_.createRequire)(import.meta.url).resolve('@aiscene/android/package.json');
1316
1319
  const yadbBin = external_node_path_["default"].join(external_node_path_["default"].dirname(androidPkgJson), 'bin', 'yadb');
1317
1320
  await adb.push(yadbBin, '/data/local/tmp');
1318
1321
  this.yadbPushed = true;
@@ -1804,4 +1807,58 @@ async function agentFromAdbDevice(deviceId, opts) {
1804
1807
  await device.connect();
1805
1808
  return new AndroidAgent(device, opts);
1806
1809
  }
1807
- export { AndroidAgent, AndroidDevice, agentFromAdbDevice, getConnectedDevices, overrideAIConfig };
1810
+ const debug = (0, logger_.getDebug)('mcp:android-tools');
1811
+ class AndroidMidsceneTools extends BaseMidsceneTools {
1812
+ createTemporaryDevice() {
1813
+ return new AndroidDevice('temp-for-action-space', {});
1814
+ }
1815
+ async ensureAgent(deviceId) {
1816
+ if (this.agent && deviceId) {
1817
+ try {
1818
+ await this.agent.destroy?.();
1819
+ } catch (error) {
1820
+ debug('Failed to destroy agent during cleanup:', error);
1821
+ }
1822
+ this.agent = void 0;
1823
+ }
1824
+ if (this.agent) return this.agent;
1825
+ debug('Creating Android agent with deviceId:', deviceId || 'auto-detect');
1826
+ const agent = await agentFromAdbDevice(deviceId, {
1827
+ autoDismissKeyboard: false
1828
+ });
1829
+ this.agent = agent;
1830
+ return agent;
1831
+ }
1832
+ preparePlatformTools() {
1833
+ return [
1834
+ {
1835
+ name: 'android_connect',
1836
+ description: 'Connect to Android device via ADB. If deviceId not provided, uses the first available device.',
1837
+ schema: {
1838
+ deviceId: z.string().optional().describe('Android device ID (from adb devices)')
1839
+ },
1840
+ handler: async ({ deviceId })=>{
1841
+ const agent = await this.ensureAgent(deviceId);
1842
+ const screenshot = await agent.page.screenshotBase64();
1843
+ return {
1844
+ content: [
1845
+ {
1846
+ type: 'text',
1847
+ text: `Connected to Android device${deviceId ? `: ${deviceId}` : ' (auto-detected)'}`
1848
+ },
1849
+ ...this.buildScreenshotContent(screenshot)
1850
+ ],
1851
+ isError: false
1852
+ };
1853
+ }
1854
+ },
1855
+ {
1856
+ name: 'android_disconnect',
1857
+ description: 'Disconnect from current Android device and release ADB resources',
1858
+ schema: {},
1859
+ handler: this.createDisconnectHandler('Android device')
1860
+ }
1861
+ ];
1862
+ }
1863
+ }
1864
+ export { AndroidAgent, AndroidDevice, AndroidMidsceneTools, ScrcpyDeviceAdapter, agentFromAdbDevice, getConnectedDevices, overrideAIConfig };