@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.
@@ -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;
@@ -118,12 +120,13 @@ var __webpack_modules__ = {
118
120
  }
119
121
  }
120
122
  resolveServerBinPath() {
121
- const androidPkgJson = (0, node_module__rspack_import_1.createRequire)(__rslib_import_meta_url__).resolve('@midscene/android/package.json');
123
+ const androidPkgJson = (0, node_module__rspack_import_1.createRequire)(__rslib_import_meta_url__).resolve('@aiscene/android/package.json');
122
124
  return node_path__rspack_import_2_default().join(node_path__rspack_import_2_default().dirname(androidPkgJson), 'bin', 'scrcpy-server');
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
  },
@@ -592,18 +604,13 @@ var __webpack_exports__ = {};
592
604
  resolveConfig(deviceInfo) {
593
605
  if (this.resolvedConfig) return this.resolvedConfig;
594
606
  const config = this.scrcpyConfig;
595
- let maxSize = config?.maxSize ?? scrcpy_manager.o.maxSize;
596
- if (config?.maxSize === void 0) {
597
- const physicalMax = Math.max(deviceInfo.physicalWidth, deviceInfo.physicalHeight);
598
- const scale = this.screenshotResizeScale ?? 1 / deviceInfo.dpr;
599
- maxSize = Math.round(physicalMax * scale);
600
- debugAdapter(`Auto-calculated maxSize: ${maxSize} (physical=${physicalMax}, scale=${scale.toFixed(3)}, ${void 0 !== this.screenshotResizeScale ? 'from screenshotResizeScale' : 'from 1/dpr'})`);
601
- }
607
+ const maxSize = config?.maxSize ?? scrcpy_manager.o.maxSize;
608
+ const videoBitRate = config?.videoBitRate ?? scrcpy_manager.o.videoBitRate;
602
609
  this.resolvedConfig = {
603
610
  enabled: this.isEnabled(),
604
611
  maxSize,
605
612
  idleTimeoutMs: config?.idleTimeoutMs ?? scrcpy_manager.o.idleTimeoutMs,
606
- videoBitRate: config?.videoBitRate ?? scrcpy_manager.o.videoBitRate
613
+ videoBitRate
607
614
  };
608
615
  return this.resolvedConfig;
609
616
  }
@@ -638,8 +645,8 @@ var __webpack_exports__ = {};
638
645
  }
639
646
  async screenshotBase64(deviceInfo) {
640
647
  const manager = await this.ensureManager(deviceInfo);
641
- const screenshotBuffer = await manager.getScreenshotPng();
642
- return (0, img_namespaceObject.createImgBase64ByFormat)('png', screenshotBuffer.toString('base64'));
648
+ const screenshotBuffer = await manager.getScreenshotJpeg();
649
+ return (0, img_namespaceObject.createImgBase64ByFormat)('jpeg', screenshotBuffer.toString('base64'));
643
650
  }
644
651
  getResolution() {
645
652
  return this.manager?.getResolution() ?? null;
@@ -650,8 +657,7 @@ var __webpack_exports__ = {};
650
657
  debugAdapter(`Using scrcpy resolution: ${resolution.width}x${resolution.height}`);
651
658
  return {
652
659
  width: resolution.width,
653
- height: resolution.height,
654
- dpr: deviceInfo.dpr
660
+ height: resolution.height
655
661
  };
656
662
  }
657
663
  getScalingRatio(physicalWidth) {
@@ -670,16 +676,14 @@ var __webpack_exports__ = {};
670
676
  }
671
677
  this.resolvedConfig = null;
672
678
  }
673
- constructor(deviceId, scrcpyConfig, screenshotResizeScale){
679
+ constructor(deviceId, scrcpyConfig){
674
680
  _define_property(this, "deviceId", void 0);
675
681
  _define_property(this, "scrcpyConfig", void 0);
676
- _define_property(this, "screenshotResizeScale", void 0);
677
682
  _define_property(this, "manager", void 0);
678
683
  _define_property(this, "resolvedConfig", void 0);
679
684
  _define_property(this, "initFailed", void 0);
680
685
  this.deviceId = deviceId;
681
686
  this.scrcpyConfig = scrcpyConfig;
682
- this.screenshotResizeScale = screenshotResizeScale;
683
687
  this.manager = null;
684
688
  this.resolvedConfig = null;
685
689
  this.initFailed = false;
@@ -906,7 +910,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
906
910
  });
907
911
  }
908
912
  getScrcpyAdapter() {
909
- if (!this.scrcpyAdapter) this.scrcpyAdapter = new ScrcpyDeviceAdapter(this.deviceId, this.options?.scrcpyConfig, this.options?.screenshotResizeScale);
913
+ if (!this.scrcpyAdapter) this.scrcpyAdapter = new ScrcpyDeviceAdapter(this.deviceId, this.options?.scrcpyConfig);
910
914
  return this.scrcpyAdapter;
911
915
  }
912
916
  async getDevicePhysicalInfo() {
@@ -1152,8 +1156,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1152
1156
  const logicalHeight = Math.round(height * scale);
1153
1157
  return {
1154
1158
  width: logicalWidth,
1155
- height: logicalHeight,
1156
- dpr: this.devicePixelRatio
1159
+ height: logicalHeight
1157
1160
  };
1158
1161
  }
1159
1162
  async cacheFeatureForPoint(center, options) {
@@ -1439,7 +1442,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1439
1442
  async ensureYadb() {
1440
1443
  if (!this.yadbPushed) {
1441
1444
  const adb = await this.getAdb();
1442
- const androidPkgJson = (0, external_node_module_.createRequire)(__rslib_import_meta_url__).resolve('@midscene/android/package.json');
1445
+ const androidPkgJson = (0, external_node_module_.createRequire)(__rslib_import_meta_url__).resolve('@aiscene/android/package.json');
1443
1446
  const yadbBin = external_node_path_default().join(external_node_path_default().dirname(androidPkgJson), 'bin', 'yadb');
1444
1447
  await adb.push(yadbBin, '/data/local/tmp');
1445
1448
  this.yadbPushed = true;
@@ -1896,7 +1899,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1896
1899
  constructor(toolsManager){
1897
1900
  super({
1898
1901
  name: '@midscene/android-mcp',
1899
- version: __VERSION__,
1902
+ version: "1.6.0-cache",
1900
1903
  description: 'Control the Android device using natural language commands'
1901
1904
  }, toolsManager);
1902
1905
  }
@@ -0,0 +1 @@
1
+ export { }
@@ -2,10 +2,12 @@ import { AbstractInterface } from '@midscene/core/device';
2
2
  import type { ActionParam } from '@midscene/core';
3
3
  import type { ActionReturn } from '@midscene/core';
4
4
  import { ADB } from 'appium-adb';
5
+ import type { Adb } from '@yume-chan/adb';
5
6
  import { Agent } from '@midscene/core/agent';
6
7
  import { AgentOpt } from '@midscene/core/agent';
7
8
  import { AndroidDeviceInputOpt } from '@midscene/core/device';
8
9
  import { AndroidDeviceOpt } from '@midscene/core/device';
10
+ import { BaseMidsceneTools } from '@midscene/shared/mcp';
9
11
  import { Device } from 'appium-adb';
10
12
  import { DeviceAction } from '@midscene/core';
11
13
  import { ElementCacheFeature } from '@midscene/core';
@@ -16,6 +18,7 @@ import { overrideAIConfig } from '@midscene/shared/env';
16
18
  import { Point } from '@midscene/core';
17
19
  import { Rect } from '@midscene/core';
18
20
  import { Size } from '@midscene/core';
21
+ import { ToolDefinition } from '@midscene/shared/mcp';
19
22
 
20
23
  declare type ActionArgs<T extends DeviceAction> = [ActionParam<T>] extends [undefined] ? [] : [ActionParam<T>];
21
24
 
@@ -207,16 +210,217 @@ export declare class AndroidDevice implements AbstractInterface {
207
210
  hideKeyboard(options?: AndroidDeviceInputOpt, timeoutMs?: number): Promise<boolean>;
208
211
  }
209
212
 
213
+ /**
214
+ * Android-specific tools manager
215
+ * Extends BaseMidsceneTools to provide Android ADB device connection tools
216
+ */
217
+ export declare class AndroidMidsceneTools extends BaseMidsceneTools<AndroidAgent> {
218
+ protected createTemporaryDevice(): AndroidDevice;
219
+ protected ensureAgent(deviceId?: string): Promise<AndroidAgent>;
220
+ /**
221
+ * Provide Android-specific platform tools
222
+ */
223
+ protected preparePlatformTools(): ToolDefinition[];
224
+ }
225
+
210
226
  declare type DeviceActionAndroidBackButton = DeviceAction<undefined, void>;
211
227
 
212
228
  declare type DeviceActionAndroidHomeButton = DeviceAction<undefined, void>;
213
229
 
214
230
  declare type DeviceActionAndroidRecentAppsButton = DeviceAction<undefined, void>;
215
231
 
232
+ declare interface DevicePhysicalInfo {
233
+ physicalWidth: number;
234
+ physicalHeight: number;
235
+ dpr: number;
236
+ orientation: number;
237
+ isCurrentOrientation?: boolean;
238
+ }
239
+
216
240
  export declare function getConnectedDevices(): Promise<Device[]>;
217
241
 
218
242
  export { overrideAIConfig }
219
243
 
244
+ declare interface ResolvedScrcpyConfig {
245
+ enabled: boolean;
246
+ maxSize: number;
247
+ videoBitRate: number;
248
+ idleTimeoutMs: number;
249
+ }
250
+
251
+ declare interface ScrcpyConfig {
252
+ enabled?: boolean;
253
+ maxSize?: number;
254
+ videoBitRate?: number;
255
+ idleTimeoutMs?: number;
256
+ }
257
+
258
+ /**
259
+ * Adapter that encapsulates all scrcpy-related logic for AndroidDevice.
260
+ * Handles config normalization, manager lifecycle, screenshot, and resolution.
261
+ */
262
+ export declare class ScrcpyDeviceAdapter {
263
+ private deviceId;
264
+ private scrcpyConfig;
265
+ private manager;
266
+ private resolvedConfig;
267
+ private initFailed;
268
+ constructor(deviceId: string, scrcpyConfig: ScrcpyConfig | undefined);
269
+ isEnabled(): boolean;
270
+ /**
271
+ * Initialize scrcpy connection. Called once during device.connect().
272
+ * If initialization fails, marks scrcpy as permanently disabled (no further retries).
273
+ */
274
+ initialize(deviceInfo: DevicePhysicalInfo): Promise<void>;
275
+ /**
276
+ * Resolve scrcpy config.
277
+ * maxSize defaults to 0 (no scaling, full physical resolution) so the Agent layer
278
+ * receives the highest quality image for AI processing.
279
+ * videoBitRate is auto-scaled based on physical pixel count to ensure
280
+ * sufficient quality for all-I-frame H.264 encoding.
281
+ */
282
+ resolveConfig(deviceInfo: DevicePhysicalInfo): ResolvedScrcpyConfig;
283
+ /**
284
+ * Get or create the ScrcpyScreenshotManager.
285
+ * Uses dynamic import for @yume-chan packages (ESM-only, must use await import in CJS builds).
286
+ */
287
+ ensureManager(deviceInfo: DevicePhysicalInfo): Promise<ScrcpyScreenshotManager>;
288
+ /**
289
+ * Take a screenshot via scrcpy, returns base64 string.
290
+ * Throws on failure (caller should fallback to ADB).
291
+ */
292
+ screenshotBase64(deviceInfo: DevicePhysicalInfo): Promise<string>;
293
+ /**
294
+ * Get scrcpy's actual video resolution.
295
+ * Returns null if scrcpy is not connected yet.
296
+ */
297
+ getResolution(): {
298
+ width: number;
299
+ height: number;
300
+ } | null;
301
+ /**
302
+ * Compute size from scrcpy resolution.
303
+ * Returns null if scrcpy is not connected.
304
+ */
305
+ getSize(deviceInfo: DevicePhysicalInfo): Size | null;
306
+ /**
307
+ * Calculate the scaling ratio from physical to scrcpy resolution.
308
+ */
309
+ getScalingRatio(physicalWidth: number): number | null;
310
+ disconnect(): Promise<void>;
311
+ }
312
+
313
+ declare class ScrcpyScreenshotManager {
314
+ private adb;
315
+ private scrcpyClient;
316
+ private videoStream;
317
+ private spsHeader;
318
+ private idleTimer;
319
+ private isConnecting;
320
+ private isInitialized;
321
+ private options;
322
+ private ffmpegAvailable;
323
+ private keyframeResolvers;
324
+ private lastRawKeyframe;
325
+ private videoResolution;
326
+ private streamReader;
327
+ constructor(adb: Adb, options?: ScrcpyScreenshotOptions);
328
+ /**
329
+ * Validate environment prerequisites (ffmpeg, scrcpy-server, etc.)
330
+ * Must be called once after construction, before any screenshot operations.
331
+ * Throws if prerequisites are not met.
332
+ */
333
+ validateEnvironment(): Promise<void>;
334
+ /**
335
+ * Ensure scrcpy connection is active
336
+ */
337
+ ensureConnected(): Promise<void>;
338
+ /**
339
+ * Resolve path to scrcpy server binary
340
+ */
341
+ private resolveServerBinPath;
342
+ /**
343
+ * Get ffmpeg executable path
344
+ * Priority: @ffmpeg-installer/ffmpeg > system ffmpeg
345
+ */
346
+ private getFfmpegPath;
347
+ /**
348
+ * Consume video frames and keep latest frame
349
+ */
350
+ private startFrameConsumer;
351
+ /**
352
+ * Main frame consumption loop
353
+ * Includes busy-loop detection: if reader.read() resolves too fast
354
+ * (e.g. broken stream returning immediately), we throttle to prevent 100% CPU.
355
+ */
356
+ private consumeFramesLoop;
357
+ /**
358
+ * Process a single video packet from the scrcpy stream.
359
+ * With sendFrameMeta: true, the stream emits properly framed packets:
360
+ * - "configuration" packets contain SPS/PPS header data
361
+ * - "data" packets contain complete video frames with correct boundaries
362
+ * This avoids the frame-splitting issue that occurs with sendFrameMeta: false
363
+ * at high resolutions where raw chunks may not align with frame boundaries.
364
+ */
365
+ private processFrame;
366
+ /**
367
+ * Get screenshot as JPEG.
368
+ * Tries to get a fresh frame within a short timeout. If the screen is static
369
+ * (no new frames arrive), falls back to the latest cached keyframe.
370
+ */
371
+ getScreenshotJpeg(): Promise<Buffer>;
372
+ /**
373
+ * Get the actual video stream resolution
374
+ * Returns null if scrcpy is not connected yet
375
+ */
376
+ getResolution(): {
377
+ width: number;
378
+ height: number;
379
+ } | null;
380
+ /**
381
+ * Notify all pending keyframe waiters
382
+ */
383
+ private notifyKeyframeWaiters;
384
+ /**
385
+ * Wait for the next keyframe to arrive
386
+ */
387
+ private waitForNextKeyframe;
388
+ /**
389
+ * Ensure ffmpeg is available for PNG conversion
390
+ */
391
+ private ensureFfmpegAvailable;
392
+ /**
393
+ * Wait for first keyframe with SPS/PPS header
394
+ */
395
+ private waitForKeyframe;
396
+ /**
397
+ * Check if ffmpeg is available in the system
398
+ */
399
+ private checkFfmpegAvailable;
400
+ /**
401
+ * Decode H.264 data to JPEG using ffmpeg
402
+ */
403
+ private decodeH264ToJpeg;
404
+ /**
405
+ * Reset idle timeout timer
406
+ */
407
+ private resetIdleTimer;
408
+ /**
409
+ * Disconnect scrcpy
410
+ */
411
+ disconnect(): Promise<void>;
412
+ /**
413
+ * Check if scrcpy is initialized and connected
414
+ */
415
+ isConnected(): boolean;
416
+ }
417
+
418
+ declare interface ScrcpyScreenshotOptions {
419
+ maxSize?: number;
420
+ videoBitRate?: number;
421
+ idleTimeoutMs?: number;
422
+ }
423
+
220
424
  /**
221
425
  * Helper type to convert DeviceAction to wrapped method signature
222
426
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiscene/android",
3
- "version": "1.3.5",
3
+ "version": "1.6.0-cache",
4
4
  "description": "Android automation library for Midscene",
5
5
  "keywords": [
6
6
  "Android UI automation",
@@ -12,6 +12,9 @@
12
12
  "main": "./dist/lib/index.js",
13
13
  "module": "./dist/es/index.mjs",
14
14
  "types": "./dist/types/index.d.ts",
15
+ "bin": {
16
+ "midscene-android": "./bin/midscene-android"
17
+ },
15
18
  "files": ["bin", "dist", "README.md"],
16
19
  "exports": {
17
20
  ".": {
@@ -39,13 +42,13 @@
39
42
  "test:ai:cache": "MIDSCENE_CACHE=true AI_TEST_TYPE=android npm run test"
40
43
  },
41
44
  "dependencies": {
42
- "@midscene/core": "^1.3.4",
43
- "@midscene/shared": "^1.3.4",
44
- "@yume-chan/adb": "^1.1.0",
45
- "@yume-chan/adb-scrcpy": "^1.1.0",
46
- "@yume-chan/adb-server-node-tcp": "^1.1.0",
47
- "@yume-chan/scrcpy": "^1.1.0",
48
- "@yume-chan/stream-extra": "^1.0.0",
45
+ "@midscene/core": "workspace:*",
46
+ "@midscene/shared": "workspace:*",
47
+ "@yume-chan/adb": "2.5.1",
48
+ "@yume-chan/adb-scrcpy": "2.3.2",
49
+ "@yume-chan/adb-server-node-tcp": "2.5.2",
50
+ "@yume-chan/scrcpy": "2.3.0",
51
+ "@yume-chan/stream-extra": "2.1.0",
49
52
  "appium-adb": "12.12.1",
50
53
  "sharp": "^0.34.3"
51
54
  },
@@ -63,9 +66,5 @@
63
66
  "vitest": "3.0.5",
64
67
  "zod": "3.24.3"
65
68
  },
66
- "license": "MIT",
67
- "publishConfig": {
68
- "access": "public",
69
- "registry": "https://registry.npmjs.org"
70
- }
69
+ "license": "MIT"
71
70
  }