@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/dist/lib/index.js CHANGED
@@ -24,24 +24,26 @@ var __webpack_modules__ = {
24
24
  return obj;
25
25
  }
26
26
  const debugScrcpy = (0, _midscene_shared_logger__rspack_import_3.getDebug)('android:scrcpy');
27
+ const warnScrcpy = (0, _midscene_shared_logger__rspack_import_3.getDebug)('android:scrcpy', {
28
+ console: true
29
+ });
27
30
  const NAL_TYPE_IDR = 5;
28
31
  const NAL_TYPE_SPS = 7;
29
32
  const NAL_TYPE_PPS = 8;
30
33
  const NAL_TYPE_MASK = 0x1f;
31
- const START_CODE_4_BYTE = Buffer.from([
32
- 0x00,
33
- 0x00,
34
- 0x00,
35
- 0x01
36
- ]);
37
34
  const DEFAULT_MAX_SIZE = 0;
38
- const DEFAULT_VIDEO_BIT_RATE = 2000000;
35
+ const DEFAULT_VIDEO_BIT_RATE = 100000000;
36
+ const MAX_VIDEO_BIT_RATE = 100000000;
39
37
  const DEFAULT_IDLE_TIMEOUT_MS = 30000;
40
38
  const MAX_KEYFRAME_WAIT_MS = 5000;
41
39
  const FRESH_FRAME_TIMEOUT_MS = 300;
42
40
  const KEYFRAME_POLL_INTERVAL_MS = 200;
43
41
  const MAX_SCAN_BYTES = 1000;
44
42
  const CONNECTION_WAIT_MS = 1000;
43
+ const BUSY_LOOP_WINDOW_MS = 1000;
44
+ const BUSY_LOOP_MAX_READS = 500;
45
+ const BUSY_LOOP_COOLDOWN_MS = 50;
46
+ const BUSY_LOOP_WARN_INTERVAL_MS = 5000;
45
47
  const DEFAULT_SCRCPY_CONFIG = {
46
48
  enabled: false,
47
49
  maxSize: DEFAULT_MAX_SIZE,
@@ -81,21 +83,21 @@ var __webpack_modules__ = {
81
83
  try {
82
84
  this.isConnecting = true;
83
85
  debugScrcpy('Starting scrcpy connection...');
84
- const { AdbScrcpyClient, AdbScrcpyOptions2_1 } = await import("@yume-chan/adb-scrcpy");
86
+ const { AdbScrcpyClient, AdbScrcpyOptions3_3_3 } = await import("@yume-chan/adb-scrcpy");
85
87
  const { ReadableStream } = await import("@yume-chan/stream-extra");
86
- const { ScrcpyOptions3_1, DefaultServerPath, h264SearchConfiguration } = await import("@yume-chan/scrcpy");
87
- this.h264SearchConfigFn = h264SearchConfiguration;
88
+ const { DefaultServerPath } = await import("@yume-chan/scrcpy");
88
89
  const serverBinPath = this.resolveServerBinPath();
89
90
  await AdbScrcpyClient.pushServer(this.adb, ReadableStream.from((0, node_fs__rspack_import_0.createReadStream)(serverBinPath)));
90
- const scrcpyOptions = new ScrcpyOptions3_1({
91
+ const scrcpyOptions = new AdbScrcpyOptions3_3_3({
91
92
  audio: false,
92
93
  control: false,
93
94
  maxSize: this.options.maxSize,
94
95
  videoBitRate: this.options.videoBitRate,
95
- sendFrameMeta: false,
96
- videoCodecOptions: 'i-frame-interval=0'
96
+ maxFps: 10,
97
+ sendFrameMeta: true,
98
+ videoCodecOptions: 'i-frame-interval=0,bitrate-mode=2'
97
99
  });
98
- this.scrcpyClient = await AdbScrcpyClient.start(this.adb, DefaultServerPath, new AdbScrcpyOptions2_1(scrcpyOptions));
100
+ this.scrcpyClient = await AdbScrcpyClient.start(this.adb, DefaultServerPath, scrcpyOptions);
99
101
  const videoStreamPromise = this.scrcpyClient.videoStream;
100
102
  if (!videoStreamPromise) throw new Error('Scrcpy client did not provide video stream');
101
103
  this.videoStream = await videoStreamPromise;
@@ -123,7 +125,8 @@ var __webpack_modules__ = {
123
125
  }
124
126
  getFfmpegPath() {
125
127
  try {
126
- const ffmpegInstaller = __webpack_require__("@ffmpeg-installer/ffmpeg");
128
+ const dynamicRequire = (0, node_module__rspack_import_1.createRequire)(__rslib_import_meta_url__);
129
+ const ffmpegInstaller = dynamicRequire('@ffmpeg-installer/ffmpeg');
127
130
  debugScrcpy(`Using ffmpeg from npm package: ${ffmpegInstaller.path}`);
128
131
  return ffmpegInstaller.path;
129
132
  } catch (error) {
@@ -138,22 +141,47 @@ var __webpack_modules__ = {
138
141
  this.consumeFramesLoop(reader);
139
142
  }
140
143
  async consumeFramesLoop(reader) {
144
+ let readCount = 0;
145
+ let windowStart = Date.now();
146
+ let lastBusyWarn = 0;
147
+ let totalReads = 0;
141
148
  try {
142
149
  while(true){
143
150
  const { done, value } = await reader.read();
144
151
  if (done) break;
152
+ totalReads++;
153
+ readCount++;
154
+ const now = Date.now();
155
+ const elapsed = now - windowStart;
156
+ if (elapsed >= BUSY_LOOP_WINDOW_MS) {
157
+ const readsPerSec = readCount / elapsed * 1000;
158
+ if (readCount > BUSY_LOOP_MAX_READS) {
159
+ if (now - lastBusyWarn >= BUSY_LOOP_WARN_INTERVAL_MS) {
160
+ 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.`);
161
+ lastBusyWarn = now;
162
+ }
163
+ await new Promise((resolve)=>setTimeout(resolve, BUSY_LOOP_COOLDOWN_MS));
164
+ } else debugScrcpy(`[CPU-DIAG] Frame loop stats: ${readCount} reads in ${elapsed}ms (${readsPerSec.toFixed(1)} reads/sec), total: ${totalReads}`);
165
+ readCount = 0;
166
+ windowStart = Date.now();
167
+ }
145
168
  this.processFrame(value);
146
169
  }
147
170
  } catch (error) {
148
- debugScrcpy(`Frame consumer error: ${error}`);
171
+ debugScrcpy(`Frame consumer error (total reads: ${totalReads}): ${error}`);
149
172
  await this.disconnect();
150
173
  }
174
+ debugScrcpy(`Frame consumer loop ended normally (total reads: ${totalReads})`);
151
175
  }
152
176
  processFrame(packet) {
177
+ if ('configuration' === packet.type) {
178
+ this.spsHeader = Buffer.from(packet.data);
179
+ debugScrcpy(`Received SPS/PPS configuration: ${this.spsHeader.length}B`);
180
+ return;
181
+ }
153
182
  const frameBuffer = Buffer.from(packet.data);
154
- const actualKeyFrame = detectH264KeyFrame(frameBuffer);
155
- if (actualKeyFrame && !this.spsHeader) this.extractSpsHeader(frameBuffer);
156
- if (actualKeyFrame && this.spsHeader) {
183
+ const isKeyFrame = detectH264KeyFrame(frameBuffer);
184
+ if (isKeyFrame && this.spsHeader) {
157
185
  this.lastRawKeyframe = frameBuffer;
158
186
  if (this.keyframeResolvers.length > 0) {
159
187
  const combined = Buffer.concat([
@@ -164,23 +192,7 @@ var __webpack_modules__ = {
164
192
  }
165
193
  }
166
194
  }
167
- extractSpsHeader(frameBuffer) {
168
- if (!this.h264SearchConfigFn) return;
169
- try {
170
- const config = this.h264SearchConfigFn(new Uint8Array(frameBuffer));
171
- if (!config.sequenceParameterSet || !config.pictureParameterSet) return;
172
- this.spsHeader = Buffer.concat([
173
- START_CODE_4_BYTE,
174
- Buffer.from(config.sequenceParameterSet),
175
- START_CODE_4_BYTE,
176
- Buffer.from(config.pictureParameterSet)
177
- ]);
178
- debugScrcpy(`Extracted SPS/PPS: SPS=${config.sequenceParameterSet.length}B, PPS=${config.pictureParameterSet.length}B, total=${this.spsHeader.length}B`);
179
- } catch (error) {
180
- debugScrcpy(`Failed to extract SPS/PPS from keyframe: ${error}`);
181
- }
182
- }
183
- async getScreenshotPng() {
195
+ async getScreenshotJpeg() {
184
196
  const perfStart = Date.now();
185
197
  const t1 = Date.now();
186
198
  await this.ensureConnected();
@@ -210,7 +222,7 @@ var __webpack_modules__ = {
210
222
  this.resetIdleTimer();
211
223
  debugScrcpy(`Decoding H.264 stream: ${keyframeBuffer.length} bytes (${frameSource})`);
212
224
  const t4 = Date.now();
213
- const result = await this.decodeH264ToPng(keyframeBuffer);
225
+ const result = await this.decodeH264ToJpeg(keyframeBuffer);
214
226
  const decodeTime = Date.now() - t4;
215
227
  const totalTime = Date.now() - perfStart;
216
228
  debugScrcpy(`Performance: total=${totalTime}ms (connect=${connectTime}ms, spsWait=${spsWaitTime}ms, frameWait=${frameWaitTime}ms[${frameSource}], decode=${decodeTime}ms)`);
@@ -273,7 +285,7 @@ var __webpack_modules__ = {
273
285
  return false;
274
286
  }
275
287
  }
276
- async decodeH264ToPng(h264Buffer) {
288
+ async decodeH264ToJpeg(h264Buffer) {
277
289
  const { spawn } = await import("node:child_process");
278
290
  return new Promise((resolve, reject)=>{
279
291
  const ffmpegArgs = [
@@ -286,7 +298,9 @@ var __webpack_modules__ = {
286
298
  '-f',
287
299
  'image2pipe',
288
300
  '-vcodec',
289
- 'png',
301
+ 'mjpeg',
302
+ '-q:v',
303
+ '5',
290
304
  '-loglevel',
291
305
  'error',
292
306
  'pipe:1'
@@ -309,13 +323,13 @@ var __webpack_modules__ = {
309
323
  });
310
324
  ffmpeg.on('close', (code)=>{
311
325
  if (0 === code && chunks.length > 0) {
312
- const pngBuffer = Buffer.concat(chunks);
313
- debugScrcpy(`FFmpeg decode successful, PNG size: ${pngBuffer.length} bytes`);
314
- resolve(pngBuffer);
326
+ const jpegBuffer = Buffer.concat(chunks);
327
+ debugScrcpy(`FFmpeg decode successful, JPEG size: ${jpegBuffer.length} bytes`);
328
+ resolve(jpegBuffer);
315
329
  } else {
316
330
  const errorMsg = stderrOutput || `FFmpeg exited with code ${code}`;
317
331
  debugScrcpy(`FFmpeg decode failed: ${errorMsg}`);
318
- reject(new Error(`H.264 to PNG decode failed: ${errorMsg}`));
332
+ reject(new Error(`H.264 to JPEG decode failed: ${errorMsg}`));
319
333
  }
320
334
  });
321
335
  ffmpeg.on('error', (error)=>{
@@ -347,7 +361,6 @@ var __webpack_modules__ = {
347
361
  this.spsHeader = null;
348
362
  this.lastRawKeyframe = null;
349
363
  this.isInitialized = false;
350
- this.h264SearchConfigFn = null;
351
364
  this.keyframeResolvers = [];
352
365
  if (reader) try {
353
366
  reader.cancel();
@@ -375,20 +388,19 @@ var __webpack_modules__ = {
375
388
  _define_property(this, "keyframeResolvers", []);
376
389
  _define_property(this, "lastRawKeyframe", null);
377
390
  _define_property(this, "videoResolution", null);
378
- _define_property(this, "h264SearchConfigFn", null);
379
391
  _define_property(this, "streamReader", null);
380
392
  this.adb = adb;
393
+ const requestedBitRate = options.videoBitRate ?? DEFAULT_VIDEO_BIT_RATE;
394
+ const clampedBitRate = Math.min(requestedBitRate, MAX_VIDEO_BIT_RATE);
395
+ if (requestedBitRate > MAX_VIDEO_BIT_RATE) warnScrcpy(`videoBitRate ${requestedBitRate} exceeds maximum ${MAX_VIDEO_BIT_RATE}, clamped to ${clampedBitRate}`);
381
396
  this.options = {
382
397
  maxSize: options.maxSize ?? DEFAULT_MAX_SIZE,
383
- videoBitRate: options.videoBitRate ?? DEFAULT_VIDEO_BIT_RATE,
398
+ videoBitRate: clampedBitRate,
384
399
  idleTimeoutMs: options.idleTimeoutMs ?? DEFAULT_IDLE_TIMEOUT_MS
385
400
  };
386
401
  }
387
402
  }
388
403
  },
389
- "@ffmpeg-installer/ffmpeg" (module) {
390
- module.exports = require("@ffmpeg-installer/ffmpeg");
391
- },
392
404
  "@midscene/shared/logger" (module) {
393
405
  module.exports = require("@midscene/shared/logger");
394
406
  },
@@ -446,10 +458,12 @@ var __webpack_exports__ = {};
446
458
  (()=>{
447
459
  __webpack_require__.r(__webpack_exports__);
448
460
  __webpack_require__.d(__webpack_exports__, {
461
+ AndroidMidsceneTools: ()=>AndroidMidsceneTools,
449
462
  getConnectedDevices: ()=>getConnectedDevices,
450
463
  overrideAIConfig: ()=>env_namespaceObject.overrideAIConfig,
451
464
  AndroidAgent: ()=>AndroidAgent,
452
465
  agentFromAdbDevice: ()=>agentFromAdbDevice,
466
+ ScrcpyDeviceAdapter: ()=>ScrcpyDeviceAdapter,
453
467
  AndroidDevice: ()=>AndroidDevice
454
468
  });
455
469
  const external_node_assert_namespaceObject = require("node:assert");
@@ -496,18 +510,13 @@ var __webpack_exports__ = {};
496
510
  resolveConfig(deviceInfo) {
497
511
  if (this.resolvedConfig) return this.resolvedConfig;
498
512
  const config = this.scrcpyConfig;
499
- let maxSize = config?.maxSize ?? scrcpy_manager.o.maxSize;
500
- if (config?.maxSize === void 0) {
501
- const physicalMax = Math.max(deviceInfo.physicalWidth, deviceInfo.physicalHeight);
502
- const scale = this.screenshotResizeScale ?? 1 / deviceInfo.dpr;
503
- maxSize = Math.round(physicalMax * scale);
504
- debugAdapter(`Auto-calculated maxSize: ${maxSize} (physical=${physicalMax}, scale=${scale.toFixed(3)}, ${void 0 !== this.screenshotResizeScale ? 'from screenshotResizeScale' : 'from 1/dpr'})`);
505
- }
513
+ const maxSize = config?.maxSize ?? scrcpy_manager.o.maxSize;
514
+ const videoBitRate = config?.videoBitRate ?? scrcpy_manager.o.videoBitRate;
506
515
  this.resolvedConfig = {
507
516
  enabled: this.isEnabled(),
508
517
  maxSize,
509
518
  idleTimeoutMs: config?.idleTimeoutMs ?? scrcpy_manager.o.idleTimeoutMs,
510
- videoBitRate: config?.videoBitRate ?? scrcpy_manager.o.videoBitRate
519
+ videoBitRate
511
520
  };
512
521
  return this.resolvedConfig;
513
522
  }
@@ -542,8 +551,8 @@ var __webpack_exports__ = {};
542
551
  }
543
552
  async screenshotBase64(deviceInfo) {
544
553
  const manager = await this.ensureManager(deviceInfo);
545
- const screenshotBuffer = await manager.getScreenshotPng();
546
- return (0, img_namespaceObject.createImgBase64ByFormat)('png', screenshotBuffer.toString('base64'));
554
+ const screenshotBuffer = await manager.getScreenshotJpeg();
555
+ return (0, img_namespaceObject.createImgBase64ByFormat)('jpeg', screenshotBuffer.toString('base64'));
547
556
  }
548
557
  getResolution() {
549
558
  return this.manager?.getResolution() ?? null;
@@ -554,8 +563,7 @@ var __webpack_exports__ = {};
554
563
  debugAdapter(`Using scrcpy resolution: ${resolution.width}x${resolution.height}`);
555
564
  return {
556
565
  width: resolution.width,
557
- height: resolution.height,
558
- dpr: deviceInfo.dpr
566
+ height: resolution.height
559
567
  };
560
568
  }
561
569
  getScalingRatio(physicalWidth) {
@@ -574,16 +582,14 @@ var __webpack_exports__ = {};
574
582
  }
575
583
  this.resolvedConfig = null;
576
584
  }
577
- constructor(deviceId, scrcpyConfig, screenshotResizeScale){
585
+ constructor(deviceId, scrcpyConfig){
578
586
  _define_property(this, "deviceId", void 0);
579
587
  _define_property(this, "scrcpyConfig", void 0);
580
- _define_property(this, "screenshotResizeScale", void 0);
581
588
  _define_property(this, "manager", void 0);
582
589
  _define_property(this, "resolvedConfig", void 0);
583
590
  _define_property(this, "initFailed", void 0);
584
591
  this.deviceId = deviceId;
585
592
  this.scrcpyConfig = scrcpyConfig;
586
- this.screenshotResizeScale = screenshotResizeScale;
587
593
  this.manager = null;
588
594
  this.resolvedConfig = null;
589
595
  this.initFailed = false;
@@ -810,7 +816,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
810
816
  });
811
817
  }
812
818
  getScrcpyAdapter() {
813
- if (!this.scrcpyAdapter) this.scrcpyAdapter = new ScrcpyDeviceAdapter(this.deviceId, this.options?.scrcpyConfig, this.options?.screenshotResizeScale);
819
+ if (!this.scrcpyAdapter) this.scrcpyAdapter = new ScrcpyDeviceAdapter(this.deviceId, this.options?.scrcpyConfig);
814
820
  return this.scrcpyAdapter;
815
821
  }
816
822
  async getDevicePhysicalInfo() {
@@ -1056,8 +1062,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1056
1062
  const logicalHeight = Math.round(height * scale);
1057
1063
  return {
1058
1064
  width: logicalWidth,
1059
- height: logicalHeight,
1060
- dpr: this.devicePixelRatio
1065
+ height: logicalHeight
1061
1066
  };
1062
1067
  }
1063
1068
  async cacheFeatureForPoint(center, options) {
@@ -1836,15 +1841,74 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1836
1841
  await device.connect();
1837
1842
  return new AndroidAgent(device, opts);
1838
1843
  }
1844
+ const mcp_namespaceObject = require("@midscene/shared/mcp");
1845
+ const debug = (0, logger_.getDebug)('mcp:android-tools');
1846
+ class AndroidMidsceneTools extends mcp_namespaceObject.BaseMidsceneTools {
1847
+ createTemporaryDevice() {
1848
+ return new AndroidDevice('temp-for-action-space', {});
1849
+ }
1850
+ async ensureAgent(deviceId) {
1851
+ if (this.agent && deviceId) {
1852
+ try {
1853
+ await this.agent.destroy?.();
1854
+ } catch (error) {
1855
+ debug('Failed to destroy agent during cleanup:', error);
1856
+ }
1857
+ this.agent = void 0;
1858
+ }
1859
+ if (this.agent) return this.agent;
1860
+ debug('Creating Android agent with deviceId:', deviceId || 'auto-detect');
1861
+ const agent = await agentFromAdbDevice(deviceId, {
1862
+ autoDismissKeyboard: false
1863
+ });
1864
+ this.agent = agent;
1865
+ return agent;
1866
+ }
1867
+ preparePlatformTools() {
1868
+ return [
1869
+ {
1870
+ name: 'android_connect',
1871
+ description: 'Connect to Android device via ADB. If deviceId not provided, uses the first available device.',
1872
+ schema: {
1873
+ deviceId: core_namespaceObject.z.string().optional().describe('Android device ID (from adb devices)')
1874
+ },
1875
+ handler: async ({ deviceId })=>{
1876
+ const agent = await this.ensureAgent(deviceId);
1877
+ const screenshot = await agent.page.screenshotBase64();
1878
+ return {
1879
+ content: [
1880
+ {
1881
+ type: 'text',
1882
+ text: `Connected to Android device${deviceId ? `: ${deviceId}` : ' (auto-detected)'}`
1883
+ },
1884
+ ...this.buildScreenshotContent(screenshot)
1885
+ ],
1886
+ isError: false
1887
+ };
1888
+ }
1889
+ },
1890
+ {
1891
+ name: 'android_disconnect',
1892
+ description: 'Disconnect from current Android device and release ADB resources',
1893
+ schema: {},
1894
+ handler: this.createDisconnectHandler('Android device')
1895
+ }
1896
+ ];
1897
+ }
1898
+ }
1839
1899
  })();
1840
1900
  exports.AndroidAgent = __webpack_exports__.AndroidAgent;
1841
1901
  exports.AndroidDevice = __webpack_exports__.AndroidDevice;
1902
+ exports.AndroidMidsceneTools = __webpack_exports__.AndroidMidsceneTools;
1903
+ exports.ScrcpyDeviceAdapter = __webpack_exports__.ScrcpyDeviceAdapter;
1842
1904
  exports.agentFromAdbDevice = __webpack_exports__.agentFromAdbDevice;
1843
1905
  exports.getConnectedDevices = __webpack_exports__.getConnectedDevices;
1844
1906
  exports.overrideAIConfig = __webpack_exports__.overrideAIConfig;
1845
1907
  for(var __rspack_i in __webpack_exports__)if (-1 === [
1846
1908
  "AndroidAgent",
1847
1909
  "AndroidDevice",
1910
+ "AndroidMidsceneTools",
1911
+ "ScrcpyDeviceAdapter",
1848
1912
  "agentFromAdbDevice",
1849
1913
  "getConnectedDevices",
1850
1914
  "overrideAIConfig"