@midscene/android 1.5.3-beta-20260309062917.0 → 1.5.3

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 CHANGED
@@ -8,6 +8,7 @@ import { BaseMidsceneTools } from "@midscene/shared/mcp";
8
8
  import { Agent } from "@midscene/core/agent";
9
9
  import { mergeAndNormalizeAppNameMapping, normalizeForComparison, repeat } from "@midscene/shared/utils";
10
10
  import node_assert from "node:assert";
11
+ import { execFile } from "node:child_process";
11
12
  import { defineAction, defineActionClearInput, defineActionCursorMove, defineActionDoubleClick, defineActionDragAndDrop, defineActionKeyboardPress, defineActionScroll, defineActionSwipe, defineActionTap, normalizeMobileSwipeParam } from "@midscene/core/device";
12
13
  import { getTmpFile, sleep } from "@midscene/core/utils";
13
14
  import { MIDSCENE_ADB_PATH, MIDSCENE_ADB_REMOTE_HOST, MIDSCENE_ADB_REMOTE_PORT, MIDSCENE_ANDROID_IME_STRATEGY, globalConfigManager } from "@midscene/shared/env";
@@ -50,6 +51,10 @@ var __webpack_modules__ = {
50
51
  const KEYFRAME_POLL_INTERVAL_MS = 200;
51
52
  const MAX_SCAN_BYTES = 1000;
52
53
  const CONNECTION_WAIT_MS = 1000;
54
+ const BUSY_LOOP_WINDOW_MS = 1000;
55
+ const BUSY_LOOP_MAX_READS = 500;
56
+ const BUSY_LOOP_COOLDOWN_MS = 50;
57
+ const BUSY_LOOP_WARN_INTERVAL_MS = 5000;
53
58
  const DEFAULT_SCRCPY_CONFIG = {
54
59
  enabled: false,
55
60
  maxSize: DEFAULT_MAX_SIZE,
@@ -147,16 +152,37 @@ var __webpack_modules__ = {
147
152
  this.consumeFramesLoop(reader);
148
153
  }
149
154
  async consumeFramesLoop(reader) {
155
+ let readCount = 0;
156
+ let windowStart = Date.now();
157
+ let lastBusyWarn = 0;
158
+ let totalReads = 0;
150
159
  try {
151
160
  while(true){
152
161
  const { done, value } = await reader.read();
153
162
  if (done) break;
163
+ totalReads++;
164
+ readCount++;
165
+ const now = Date.now();
166
+ const elapsed = now - windowStart;
167
+ if (elapsed >= BUSY_LOOP_WINDOW_MS) {
168
+ const readsPerSec = readCount / elapsed * 1000;
169
+ if (readCount > BUSY_LOOP_MAX_READS) {
170
+ if (now - lastBusyWarn >= BUSY_LOOP_WARN_INTERVAL_MS) {
171
+ 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.`);
172
+ lastBusyWarn = now;
173
+ }
174
+ await new Promise((resolve)=>setTimeout(resolve, BUSY_LOOP_COOLDOWN_MS));
175
+ } else debugScrcpy(`[CPU-DIAG] Frame loop stats: ${readCount} reads in ${elapsed}ms (${readsPerSec.toFixed(1)} reads/sec), total: ${totalReads}`);
176
+ readCount = 0;
177
+ windowStart = Date.now();
178
+ }
154
179
  this.processFrame(value);
155
180
  }
156
181
  } catch (error) {
157
- debugScrcpy(`Frame consumer error: ${error}`);
182
+ debugScrcpy(`Frame consumer error (total reads: ${totalReads}): ${error}`);
158
183
  await this.disconnect();
159
184
  }
185
+ debugScrcpy(`Frame consumer loop ended normally (total reads: ${totalReads})`);
160
186
  }
161
187
  processFrame(packet) {
162
188
  if ('configuration' === packet.type) {
@@ -1171,14 +1197,23 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1171
1197
  const androidScreenshotPath = `/data/local/tmp/ms_${screenshotId}.png`;
1172
1198
  const useShellScreencap = 'number' == typeof this.options?.displayId;
1173
1199
  try {
1174
- if (useShellScreencap) throw new Error('Using shell screencap for displayId');
1175
- debugDevice('Taking screenshot via adb.takeScreenshot');
1176
- screenshotBuffer = await adb.takeScreenshot(null);
1177
- debugDevice('adb.takeScreenshot completed');
1178
- if (!screenshotBuffer) throw new Error('Failed to capture screenshot: screenshotBuffer is null');
1179
- if (!isValidImageBuffer(screenshotBuffer)) {
1180
- debugDevice('Invalid image buffer detected: not a valid image format');
1181
- throw new Error('Screenshot buffer has invalid format: could not find valid image signature');
1200
+ if (!useShellScreencap && this.takeScreenshotFailCount < AndroidDevice.TAKE_SCREENSHOT_FAIL_THRESHOLD) {
1201
+ debugDevice('Taking screenshot via adb.takeScreenshot');
1202
+ screenshotBuffer = await adb.takeScreenshot(null);
1203
+ debugDevice('adb.takeScreenshot completed');
1204
+ if (!screenshotBuffer) {
1205
+ this.takeScreenshotFailCount++;
1206
+ throw new Error('Failed to capture screenshot: screenshotBuffer is null');
1207
+ }
1208
+ if (!isValidImageBuffer(screenshotBuffer)) {
1209
+ debugDevice('Invalid image buffer detected: not a valid image format');
1210
+ this.takeScreenshotFailCount++;
1211
+ throw new Error('Screenshot buffer has invalid format: could not find valid image signature');
1212
+ }
1213
+ this.takeScreenshotFailCount = 0;
1214
+ } else {
1215
+ if (this.takeScreenshotFailCount >= AndroidDevice.TAKE_SCREENSHOT_FAIL_THRESHOLD) debugDevice('Skipping takeScreenshot (failed %d consecutive times), using shell screencap directly', this.takeScreenshotFailCount);
1216
+ throw new Error('Using shell screencap directly');
1182
1217
  }
1183
1218
  const validScreenshotBufferSize = this.options?.minScreenshotBufferSize ?? 10240;
1184
1219
  if (validScreenshotBufferSize > 0 && screenshotBuffer.length < validScreenshotBufferSize) {
@@ -1210,9 +1245,18 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1210
1245
  if (!isValidImageBuffer(screenshotBuffer)) throw new Error('Fallback screenshot buffer has invalid PNG format');
1211
1246
  debugDevice(`Fallback screenshot validated successfully: ${screenshotBuffer.length} bytes`);
1212
1247
  } finally{
1213
- Promise.resolve().then(()=>adb.shell(`rm ${androidScreenshotPath}`)).catch((error)=>{
1214
- debugDevice(`Failed to delete remote screenshot: ${error}`);
1248
+ const adbPath = adb.executable?.path ?? 'adb';
1249
+ const child = execFile(adbPath, [
1250
+ '-s',
1251
+ this.deviceId,
1252
+ 'shell',
1253
+ `rm ${androidScreenshotPath}`
1254
+ ], {
1255
+ timeout: 3000
1256
+ }, (err)=>{
1257
+ if (err) debugDevice('Failed to delete remote screenshot: %s', err.message);
1215
1258
  });
1259
+ child.unref();
1216
1260
  }
1217
1261
  }
1218
1262
  if (!screenshotBuffer) throw new Error('Failed to capture screenshot: all methods failed');
@@ -1656,6 +1700,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1656
1700
  device_define_property(this, "scrcpyAdapter", null);
1657
1701
  device_define_property(this, "appNameMapping", {});
1658
1702
  device_define_property(this, "cachedAdjustScale", null);
1703
+ device_define_property(this, "takeScreenshotFailCount", 0);
1659
1704
  device_define_property(this, "interfaceType", 'android');
1660
1705
  device_define_property(this, "uri", void 0);
1661
1706
  device_define_property(this, "options", void 0);
@@ -1665,6 +1710,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1665
1710
  this.customActions = options?.customActions;
1666
1711
  }
1667
1712
  }
1713
+ device_define_property(AndroidDevice, "TAKE_SCREENSHOT_FAIL_THRESHOLD", 3);
1668
1714
  const runAdbShellParamSchema = z.object({
1669
1715
  command: z.string().describe('ADB shell command to execute')
1670
1716
  });
package/dist/es/index.mjs CHANGED
@@ -3,6 +3,7 @@ import * as __rspack_external_node_fs_5ea92f0c from "node:fs";
3
3
  import * as __rspack_external_node_module_ab9f2194 from "node:module";
4
4
  import * as __rspack_external_node_path_c5b9b54f from "node:path";
5
5
  import node_assert from "node:assert";
6
+ import { execFile } from "node:child_process";
6
7
  import { getMidsceneLocationSchema, z } from "@midscene/core";
7
8
  import { defineAction, defineActionClearInput, defineActionCursorMove, defineActionDoubleClick, defineActionDragAndDrop, defineActionKeyboardPress, defineActionScroll, defineActionSwipe, defineActionTap, normalizeMobileSwipeParam } from "@midscene/core/device";
8
9
  import { getTmpFile, sleep } from "@midscene/core/utils";
@@ -48,6 +49,10 @@ var __webpack_modules__ = {
48
49
  const KEYFRAME_POLL_INTERVAL_MS = 200;
49
50
  const MAX_SCAN_BYTES = 1000;
50
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;
51
56
  const DEFAULT_SCRCPY_CONFIG = {
52
57
  enabled: false,
53
58
  maxSize: DEFAULT_MAX_SIZE,
@@ -145,16 +150,37 @@ var __webpack_modules__ = {
145
150
  this.consumeFramesLoop(reader);
146
151
  }
147
152
  async consumeFramesLoop(reader) {
153
+ let readCount = 0;
154
+ let windowStart = Date.now();
155
+ let lastBusyWarn = 0;
156
+ let totalReads = 0;
148
157
  try {
149
158
  while(true){
150
159
  const { done, value } = await reader.read();
151
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
+ }
152
177
  this.processFrame(value);
153
178
  }
154
179
  } catch (error) {
155
- debugScrcpy(`Frame consumer error: ${error}`);
180
+ debugScrcpy(`Frame consumer error (total reads: ${totalReads}): ${error}`);
156
181
  await this.disconnect();
157
182
  }
183
+ debugScrcpy(`Frame consumer loop ended normally (total reads: ${totalReads})`);
158
184
  }
159
185
  processFrame(packet) {
160
186
  if ('configuration' === packet.type) {
@@ -1073,14 +1099,23 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1073
1099
  const androidScreenshotPath = `/data/local/tmp/ms_${screenshotId}.png`;
1074
1100
  const useShellScreencap = 'number' == typeof this.options?.displayId;
1075
1101
  try {
1076
- if (useShellScreencap) throw new Error('Using shell screencap for displayId');
1077
- debugDevice('Taking screenshot via adb.takeScreenshot');
1078
- screenshotBuffer = await adb.takeScreenshot(null);
1079
- debugDevice('adb.takeScreenshot completed');
1080
- if (!screenshotBuffer) throw new Error('Failed to capture screenshot: screenshotBuffer is null');
1081
- if (!isValidImageBuffer(screenshotBuffer)) {
1082
- debugDevice('Invalid image buffer detected: not a valid image format');
1083
- throw new Error('Screenshot buffer has invalid format: could not find valid image signature');
1102
+ if (!useShellScreencap && this.takeScreenshotFailCount < AndroidDevice.TAKE_SCREENSHOT_FAIL_THRESHOLD) {
1103
+ debugDevice('Taking screenshot via adb.takeScreenshot');
1104
+ screenshotBuffer = await adb.takeScreenshot(null);
1105
+ debugDevice('adb.takeScreenshot completed');
1106
+ if (!screenshotBuffer) {
1107
+ this.takeScreenshotFailCount++;
1108
+ throw new Error('Failed to capture screenshot: screenshotBuffer is null');
1109
+ }
1110
+ if (!isValidImageBuffer(screenshotBuffer)) {
1111
+ debugDevice('Invalid image buffer detected: not a valid image format');
1112
+ this.takeScreenshotFailCount++;
1113
+ throw new Error('Screenshot buffer has invalid format: could not find valid image signature');
1114
+ }
1115
+ this.takeScreenshotFailCount = 0;
1116
+ } else {
1117
+ if (this.takeScreenshotFailCount >= AndroidDevice.TAKE_SCREENSHOT_FAIL_THRESHOLD) debugDevice('Skipping takeScreenshot (failed %d consecutive times), using shell screencap directly', this.takeScreenshotFailCount);
1118
+ throw new Error('Using shell screencap directly');
1084
1119
  }
1085
1120
  const validScreenshotBufferSize = this.options?.minScreenshotBufferSize ?? 10240;
1086
1121
  if (validScreenshotBufferSize > 0 && screenshotBuffer.length < validScreenshotBufferSize) {
@@ -1112,9 +1147,18 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1112
1147
  if (!isValidImageBuffer(screenshotBuffer)) throw new Error('Fallback screenshot buffer has invalid PNG format');
1113
1148
  debugDevice(`Fallback screenshot validated successfully: ${screenshotBuffer.length} bytes`);
1114
1149
  } finally{
1115
- Promise.resolve().then(()=>adb.shell(`rm ${androidScreenshotPath}`)).catch((error)=>{
1116
- debugDevice(`Failed to delete remote screenshot: ${error}`);
1150
+ const adbPath = adb.executable?.path ?? 'adb';
1151
+ const child = execFile(adbPath, [
1152
+ '-s',
1153
+ this.deviceId,
1154
+ 'shell',
1155
+ `rm ${androidScreenshotPath}`
1156
+ ], {
1157
+ timeout: 3000
1158
+ }, (err)=>{
1159
+ if (err) debugDevice('Failed to delete remote screenshot: %s', err.message);
1117
1160
  });
1161
+ child.unref();
1118
1162
  }
1119
1163
  }
1120
1164
  if (!screenshotBuffer) throw new Error('Failed to capture screenshot: all methods failed');
@@ -1558,6 +1602,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1558
1602
  device_define_property(this, "scrcpyAdapter", null);
1559
1603
  device_define_property(this, "appNameMapping", {});
1560
1604
  device_define_property(this, "cachedAdjustScale", null);
1605
+ device_define_property(this, "takeScreenshotFailCount", 0);
1561
1606
  device_define_property(this, "interfaceType", 'android');
1562
1607
  device_define_property(this, "uri", void 0);
1563
1608
  device_define_property(this, "options", void 0);
@@ -1567,6 +1612,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1567
1612
  this.customActions = options?.customActions;
1568
1613
  }
1569
1614
  }
1615
+ device_define_property(AndroidDevice, "TAKE_SCREENSHOT_FAIL_THRESHOLD", 3);
1570
1616
  const runAdbShellParamSchema = z.object({
1571
1617
  command: z.string().describe('ADB shell command to execute')
1572
1618
  });
@@ -6,6 +6,7 @@ import { BaseMCPServer, BaseMidsceneTools, createMCPServerLauncher } from "@mids
6
6
  import { Agent } from "@midscene/core/agent";
7
7
  import { mergeAndNormalizeAppNameMapping, normalizeForComparison, repeat } from "@midscene/shared/utils";
8
8
  import node_assert from "node:assert";
9
+ import { execFile } from "node:child_process";
9
10
  import { getMidsceneLocationSchema, z } from "@midscene/core";
10
11
  import { defineAction, defineActionClearInput, defineActionCursorMove, defineActionDoubleClick, defineActionDragAndDrop, defineActionKeyboardPress, defineActionScroll, defineActionSwipe, defineActionTap, normalizeMobileSwipeParam } from "@midscene/core/device";
11
12
  import { getTmpFile, sleep } from "@midscene/core/utils";
@@ -49,6 +50,10 @@ var __webpack_modules__ = {
49
50
  const KEYFRAME_POLL_INTERVAL_MS = 200;
50
51
  const MAX_SCAN_BYTES = 1000;
51
52
  const CONNECTION_WAIT_MS = 1000;
53
+ const BUSY_LOOP_WINDOW_MS = 1000;
54
+ const BUSY_LOOP_MAX_READS = 500;
55
+ const BUSY_LOOP_COOLDOWN_MS = 50;
56
+ const BUSY_LOOP_WARN_INTERVAL_MS = 5000;
52
57
  const DEFAULT_SCRCPY_CONFIG = {
53
58
  enabled: false,
54
59
  maxSize: DEFAULT_MAX_SIZE,
@@ -146,16 +151,37 @@ var __webpack_modules__ = {
146
151
  this.consumeFramesLoop(reader);
147
152
  }
148
153
  async consumeFramesLoop(reader) {
154
+ let readCount = 0;
155
+ let windowStart = Date.now();
156
+ let lastBusyWarn = 0;
157
+ let totalReads = 0;
149
158
  try {
150
159
  while(true){
151
160
  const { done, value } = await reader.read();
152
161
  if (done) break;
162
+ totalReads++;
163
+ readCount++;
164
+ const now = Date.now();
165
+ const elapsed = now - windowStart;
166
+ if (elapsed >= BUSY_LOOP_WINDOW_MS) {
167
+ const readsPerSec = readCount / elapsed * 1000;
168
+ if (readCount > BUSY_LOOP_MAX_READS) {
169
+ if (now - lastBusyWarn >= BUSY_LOOP_WARN_INTERVAL_MS) {
170
+ 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.`);
171
+ lastBusyWarn = now;
172
+ }
173
+ await new Promise((resolve)=>setTimeout(resolve, BUSY_LOOP_COOLDOWN_MS));
174
+ } else debugScrcpy(`[CPU-DIAG] Frame loop stats: ${readCount} reads in ${elapsed}ms (${readsPerSec.toFixed(1)} reads/sec), total: ${totalReads}`);
175
+ readCount = 0;
176
+ windowStart = Date.now();
177
+ }
153
178
  this.processFrame(value);
154
179
  }
155
180
  } catch (error) {
156
- debugScrcpy(`Frame consumer error: ${error}`);
181
+ debugScrcpy(`Frame consumer error (total reads: ${totalReads}): ${error}`);
157
182
  await this.disconnect();
158
183
  }
184
+ debugScrcpy(`Frame consumer loop ended normally (total reads: ${totalReads})`);
159
185
  }
160
186
  processFrame(packet) {
161
187
  if ('configuration' === packet.type) {
@@ -1170,14 +1196,23 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1170
1196
  const androidScreenshotPath = `/data/local/tmp/ms_${screenshotId}.png`;
1171
1197
  const useShellScreencap = 'number' == typeof this.options?.displayId;
1172
1198
  try {
1173
- if (useShellScreencap) throw new Error('Using shell screencap for displayId');
1174
- debugDevice('Taking screenshot via adb.takeScreenshot');
1175
- screenshotBuffer = await adb.takeScreenshot(null);
1176
- debugDevice('adb.takeScreenshot completed');
1177
- if (!screenshotBuffer) throw new Error('Failed to capture screenshot: screenshotBuffer is null');
1178
- if (!isValidImageBuffer(screenshotBuffer)) {
1179
- debugDevice('Invalid image buffer detected: not a valid image format');
1180
- throw new Error('Screenshot buffer has invalid format: could not find valid image signature');
1199
+ if (!useShellScreencap && this.takeScreenshotFailCount < AndroidDevice.TAKE_SCREENSHOT_FAIL_THRESHOLD) {
1200
+ debugDevice('Taking screenshot via adb.takeScreenshot');
1201
+ screenshotBuffer = await adb.takeScreenshot(null);
1202
+ debugDevice('adb.takeScreenshot completed');
1203
+ if (!screenshotBuffer) {
1204
+ this.takeScreenshotFailCount++;
1205
+ throw new Error('Failed to capture screenshot: screenshotBuffer is null');
1206
+ }
1207
+ if (!isValidImageBuffer(screenshotBuffer)) {
1208
+ debugDevice('Invalid image buffer detected: not a valid image format');
1209
+ this.takeScreenshotFailCount++;
1210
+ throw new Error('Screenshot buffer has invalid format: could not find valid image signature');
1211
+ }
1212
+ this.takeScreenshotFailCount = 0;
1213
+ } else {
1214
+ if (this.takeScreenshotFailCount >= AndroidDevice.TAKE_SCREENSHOT_FAIL_THRESHOLD) debugDevice('Skipping takeScreenshot (failed %d consecutive times), using shell screencap directly', this.takeScreenshotFailCount);
1215
+ throw new Error('Using shell screencap directly');
1181
1216
  }
1182
1217
  const validScreenshotBufferSize = this.options?.minScreenshotBufferSize ?? 10240;
1183
1218
  if (validScreenshotBufferSize > 0 && screenshotBuffer.length < validScreenshotBufferSize) {
@@ -1209,9 +1244,18 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1209
1244
  if (!isValidImageBuffer(screenshotBuffer)) throw new Error('Fallback screenshot buffer has invalid PNG format');
1210
1245
  debugDevice(`Fallback screenshot validated successfully: ${screenshotBuffer.length} bytes`);
1211
1246
  } finally{
1212
- Promise.resolve().then(()=>adb.shell(`rm ${androidScreenshotPath}`)).catch((error)=>{
1213
- debugDevice(`Failed to delete remote screenshot: ${error}`);
1247
+ const adbPath = adb.executable?.path ?? 'adb';
1248
+ const child = execFile(adbPath, [
1249
+ '-s',
1250
+ this.deviceId,
1251
+ 'shell',
1252
+ `rm ${androidScreenshotPath}`
1253
+ ], {
1254
+ timeout: 3000
1255
+ }, (err)=>{
1256
+ if (err) debugDevice('Failed to delete remote screenshot: %s', err.message);
1214
1257
  });
1258
+ child.unref();
1215
1259
  }
1216
1260
  }
1217
1261
  if (!screenshotBuffer) throw new Error('Failed to capture screenshot: all methods failed');
@@ -1655,6 +1699,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1655
1699
  device_define_property(this, "scrcpyAdapter", null);
1656
1700
  device_define_property(this, "appNameMapping", {});
1657
1701
  device_define_property(this, "cachedAdjustScale", null);
1702
+ device_define_property(this, "takeScreenshotFailCount", 0);
1658
1703
  device_define_property(this, "interfaceType", 'android');
1659
1704
  device_define_property(this, "uri", void 0);
1660
1705
  device_define_property(this, "options", void 0);
@@ -1664,6 +1709,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1664
1709
  this.customActions = options?.customActions;
1665
1710
  }
1666
1711
  }
1712
+ device_define_property(AndroidDevice, "TAKE_SCREENSHOT_FAIL_THRESHOLD", 3);
1667
1713
  const runAdbShellParamSchema = z.object({
1668
1714
  command: z.string().describe('ADB shell command to execute')
1669
1715
  });
package/dist/lib/cli.js CHANGED
@@ -40,6 +40,10 @@ 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 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;
43
47
  const DEFAULT_SCRCPY_CONFIG = {
44
48
  enabled: false,
45
49
  maxSize: DEFAULT_MAX_SIZE,
@@ -137,16 +141,37 @@ var __webpack_modules__ = {
137
141
  this.consumeFramesLoop(reader);
138
142
  }
139
143
  async consumeFramesLoop(reader) {
144
+ let readCount = 0;
145
+ let windowStart = Date.now();
146
+ let lastBusyWarn = 0;
147
+ let totalReads = 0;
140
148
  try {
141
149
  while(true){
142
150
  const { done, value } = await reader.read();
143
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
+ }
144
168
  this.processFrame(value);
145
169
  }
146
170
  } catch (error) {
147
- debugScrcpy(`Frame consumer error: ${error}`);
171
+ debugScrcpy(`Frame consumer error (total reads: ${totalReads}): ${error}`);
148
172
  await this.disconnect();
149
173
  }
174
+ debugScrcpy(`Frame consumer loop ended normally (total reads: ${totalReads})`);
150
175
  }
151
176
  processFrame(packet) {
152
177
  if ('configuration' === packet.type) {
@@ -525,6 +550,7 @@ var __webpack_exports__ = {};
525
550
  };
526
551
  const external_node_assert_namespaceObject = require("node:assert");
527
552
  var external_node_assert_default = /*#__PURE__*/ __webpack_require__.n(external_node_assert_namespaceObject);
553
+ const external_node_child_process_namespaceObject = require("node:child_process");
528
554
  var external_node_fs_ = __webpack_require__("node:fs");
529
555
  var external_node_fs_default = /*#__PURE__*/ __webpack_require__.n(external_node_fs_);
530
556
  var external_node_module_ = __webpack_require__("node:module");
@@ -1186,14 +1212,23 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1186
1212
  const androidScreenshotPath = `/data/local/tmp/ms_${screenshotId}.png`;
1187
1213
  const useShellScreencap = 'number' == typeof this.options?.displayId;
1188
1214
  try {
1189
- if (useShellScreencap) throw new Error('Using shell screencap for displayId');
1190
- debugDevice('Taking screenshot via adb.takeScreenshot');
1191
- screenshotBuffer = await adb.takeScreenshot(null);
1192
- debugDevice('adb.takeScreenshot completed');
1193
- if (!screenshotBuffer) throw new Error('Failed to capture screenshot: screenshotBuffer is null');
1194
- if (!(0, img_namespaceObject.isValidImageBuffer)(screenshotBuffer)) {
1195
- debugDevice('Invalid image buffer detected: not a valid image format');
1196
- throw new Error('Screenshot buffer has invalid format: could not find valid image signature');
1215
+ if (!useShellScreencap && this.takeScreenshotFailCount < AndroidDevice.TAKE_SCREENSHOT_FAIL_THRESHOLD) {
1216
+ debugDevice('Taking screenshot via adb.takeScreenshot');
1217
+ screenshotBuffer = await adb.takeScreenshot(null);
1218
+ debugDevice('adb.takeScreenshot completed');
1219
+ if (!screenshotBuffer) {
1220
+ this.takeScreenshotFailCount++;
1221
+ throw new Error('Failed to capture screenshot: screenshotBuffer is null');
1222
+ }
1223
+ if (!(0, img_namespaceObject.isValidImageBuffer)(screenshotBuffer)) {
1224
+ debugDevice('Invalid image buffer detected: not a valid image format');
1225
+ this.takeScreenshotFailCount++;
1226
+ throw new Error('Screenshot buffer has invalid format: could not find valid image signature');
1227
+ }
1228
+ this.takeScreenshotFailCount = 0;
1229
+ } else {
1230
+ if (this.takeScreenshotFailCount >= AndroidDevice.TAKE_SCREENSHOT_FAIL_THRESHOLD) debugDevice('Skipping takeScreenshot (failed %d consecutive times), using shell screencap directly', this.takeScreenshotFailCount);
1231
+ throw new Error('Using shell screencap directly');
1197
1232
  }
1198
1233
  const validScreenshotBufferSize = this.options?.minScreenshotBufferSize ?? 10240;
1199
1234
  if (validScreenshotBufferSize > 0 && screenshotBuffer.length < validScreenshotBufferSize) {
@@ -1225,9 +1260,18 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1225
1260
  if (!(0, img_namespaceObject.isValidImageBuffer)(screenshotBuffer)) throw new Error('Fallback screenshot buffer has invalid PNG format');
1226
1261
  debugDevice(`Fallback screenshot validated successfully: ${screenshotBuffer.length} bytes`);
1227
1262
  } finally{
1228
- Promise.resolve().then(()=>adb.shell(`rm ${androidScreenshotPath}`)).catch((error)=>{
1229
- debugDevice(`Failed to delete remote screenshot: ${error}`);
1263
+ const adbPath = adb.executable?.path ?? 'adb';
1264
+ const child = (0, external_node_child_process_namespaceObject.execFile)(adbPath, [
1265
+ '-s',
1266
+ this.deviceId,
1267
+ 'shell',
1268
+ `rm ${androidScreenshotPath}`
1269
+ ], {
1270
+ timeout: 3000
1271
+ }, (err)=>{
1272
+ if (err) debugDevice('Failed to delete remote screenshot: %s', err.message);
1230
1273
  });
1274
+ child.unref();
1231
1275
  }
1232
1276
  }
1233
1277
  if (!screenshotBuffer) throw new Error('Failed to capture screenshot: all methods failed');
@@ -1671,6 +1715,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1671
1715
  device_define_property(this, "scrcpyAdapter", null);
1672
1716
  device_define_property(this, "appNameMapping", {});
1673
1717
  device_define_property(this, "cachedAdjustScale", null);
1718
+ device_define_property(this, "takeScreenshotFailCount", 0);
1674
1719
  device_define_property(this, "interfaceType", 'android');
1675
1720
  device_define_property(this, "uri", void 0);
1676
1721
  device_define_property(this, "options", void 0);
@@ -1680,6 +1725,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1680
1725
  this.customActions = options?.customActions;
1681
1726
  }
1682
1727
  }
1728
+ device_define_property(AndroidDevice, "TAKE_SCREENSHOT_FAIL_THRESHOLD", 3);
1683
1729
  const runAdbShellParamSchema = core_namespaceObject.z.object({
1684
1730
  command: core_namespaceObject.z.string().describe('ADB shell command to execute')
1685
1731
  });
package/dist/lib/index.js CHANGED
@@ -40,6 +40,10 @@ 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 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;
43
47
  const DEFAULT_SCRCPY_CONFIG = {
44
48
  enabled: false,
45
49
  maxSize: DEFAULT_MAX_SIZE,
@@ -137,16 +141,37 @@ var __webpack_modules__ = {
137
141
  this.consumeFramesLoop(reader);
138
142
  }
139
143
  async consumeFramesLoop(reader) {
144
+ let readCount = 0;
145
+ let windowStart = Date.now();
146
+ let lastBusyWarn = 0;
147
+ let totalReads = 0;
140
148
  try {
141
149
  while(true){
142
150
  const { done, value } = await reader.read();
143
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
+ }
144
168
  this.processFrame(value);
145
169
  }
146
170
  } catch (error) {
147
- debugScrcpy(`Frame consumer error: ${error}`);
171
+ debugScrcpy(`Frame consumer error (total reads: ${totalReads}): ${error}`);
148
172
  await this.disconnect();
149
173
  }
174
+ debugScrcpy(`Frame consumer loop ended normally (total reads: ${totalReads})`);
150
175
  }
151
176
  processFrame(packet) {
152
177
  if ('configuration' === packet.type) {
@@ -442,6 +467,7 @@ var __webpack_exports__ = {};
442
467
  });
443
468
  const external_node_assert_namespaceObject = require("node:assert");
444
469
  var external_node_assert_default = /*#__PURE__*/ __webpack_require__.n(external_node_assert_namespaceObject);
470
+ const external_node_child_process_namespaceObject = require("node:child_process");
445
471
  var external_node_fs_ = __webpack_require__("node:fs");
446
472
  var external_node_fs_default = /*#__PURE__*/ __webpack_require__.n(external_node_fs_);
447
473
  var external_node_module_ = __webpack_require__("node:module");
@@ -1106,14 +1132,23 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1106
1132
  const androidScreenshotPath = `/data/local/tmp/ms_${screenshotId}.png`;
1107
1133
  const useShellScreencap = 'number' == typeof this.options?.displayId;
1108
1134
  try {
1109
- if (useShellScreencap) throw new Error('Using shell screencap for displayId');
1110
- debugDevice('Taking screenshot via adb.takeScreenshot');
1111
- screenshotBuffer = await adb.takeScreenshot(null);
1112
- debugDevice('adb.takeScreenshot completed');
1113
- if (!screenshotBuffer) throw new Error('Failed to capture screenshot: screenshotBuffer is null');
1114
- if (!(0, img_namespaceObject.isValidImageBuffer)(screenshotBuffer)) {
1115
- debugDevice('Invalid image buffer detected: not a valid image format');
1116
- throw new Error('Screenshot buffer has invalid format: could not find valid image signature');
1135
+ if (!useShellScreencap && this.takeScreenshotFailCount < AndroidDevice.TAKE_SCREENSHOT_FAIL_THRESHOLD) {
1136
+ debugDevice('Taking screenshot via adb.takeScreenshot');
1137
+ screenshotBuffer = await adb.takeScreenshot(null);
1138
+ debugDevice('adb.takeScreenshot completed');
1139
+ if (!screenshotBuffer) {
1140
+ this.takeScreenshotFailCount++;
1141
+ throw new Error('Failed to capture screenshot: screenshotBuffer is null');
1142
+ }
1143
+ if (!(0, img_namespaceObject.isValidImageBuffer)(screenshotBuffer)) {
1144
+ debugDevice('Invalid image buffer detected: not a valid image format');
1145
+ this.takeScreenshotFailCount++;
1146
+ throw new Error('Screenshot buffer has invalid format: could not find valid image signature');
1147
+ }
1148
+ this.takeScreenshotFailCount = 0;
1149
+ } else {
1150
+ if (this.takeScreenshotFailCount >= AndroidDevice.TAKE_SCREENSHOT_FAIL_THRESHOLD) debugDevice('Skipping takeScreenshot (failed %d consecutive times), using shell screencap directly', this.takeScreenshotFailCount);
1151
+ throw new Error('Using shell screencap directly');
1117
1152
  }
1118
1153
  const validScreenshotBufferSize = this.options?.minScreenshotBufferSize ?? 10240;
1119
1154
  if (validScreenshotBufferSize > 0 && screenshotBuffer.length < validScreenshotBufferSize) {
@@ -1145,9 +1180,18 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1145
1180
  if (!(0, img_namespaceObject.isValidImageBuffer)(screenshotBuffer)) throw new Error('Fallback screenshot buffer has invalid PNG format');
1146
1181
  debugDevice(`Fallback screenshot validated successfully: ${screenshotBuffer.length} bytes`);
1147
1182
  } finally{
1148
- Promise.resolve().then(()=>adb.shell(`rm ${androidScreenshotPath}`)).catch((error)=>{
1149
- debugDevice(`Failed to delete remote screenshot: ${error}`);
1183
+ const adbPath = adb.executable?.path ?? 'adb';
1184
+ const child = (0, external_node_child_process_namespaceObject.execFile)(adbPath, [
1185
+ '-s',
1186
+ this.deviceId,
1187
+ 'shell',
1188
+ `rm ${androidScreenshotPath}`
1189
+ ], {
1190
+ timeout: 3000
1191
+ }, (err)=>{
1192
+ if (err) debugDevice('Failed to delete remote screenshot: %s', err.message);
1150
1193
  });
1194
+ child.unref();
1151
1195
  }
1152
1196
  }
1153
1197
  if (!screenshotBuffer) throw new Error('Failed to capture screenshot: all methods failed');
@@ -1591,6 +1635,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1591
1635
  device_define_property(this, "scrcpyAdapter", null);
1592
1636
  device_define_property(this, "appNameMapping", {});
1593
1637
  device_define_property(this, "cachedAdjustScale", null);
1638
+ device_define_property(this, "takeScreenshotFailCount", 0);
1594
1639
  device_define_property(this, "interfaceType", 'android');
1595
1640
  device_define_property(this, "uri", void 0);
1596
1641
  device_define_property(this, "options", void 0);
@@ -1600,6 +1645,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1600
1645
  this.customActions = options?.customActions;
1601
1646
  }
1602
1647
  }
1648
+ device_define_property(AndroidDevice, "TAKE_SCREENSHOT_FAIL_THRESHOLD", 3);
1603
1649
  const runAdbShellParamSchema = core_namespaceObject.z.object({
1604
1650
  command: core_namespaceObject.z.string().describe('ADB shell command to execute')
1605
1651
  });
@@ -40,6 +40,10 @@ 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 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;
43
47
  const DEFAULT_SCRCPY_CONFIG = {
44
48
  enabled: false,
45
49
  maxSize: DEFAULT_MAX_SIZE,
@@ -137,16 +141,37 @@ var __webpack_modules__ = {
137
141
  this.consumeFramesLoop(reader);
138
142
  }
139
143
  async consumeFramesLoop(reader) {
144
+ let readCount = 0;
145
+ let windowStart = Date.now();
146
+ let lastBusyWarn = 0;
147
+ let totalReads = 0;
140
148
  try {
141
149
  while(true){
142
150
  const { done, value } = await reader.read();
143
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
+ }
144
168
  this.processFrame(value);
145
169
  }
146
170
  } catch (error) {
147
- debugScrcpy(`Frame consumer error: ${error}`);
171
+ debugScrcpy(`Frame consumer error (total reads: ${totalReads}): ${error}`);
148
172
  await this.disconnect();
149
173
  }
174
+ debugScrcpy(`Frame consumer loop ended normally (total reads: ${totalReads})`);
150
175
  }
151
176
  processFrame(packet) {
152
177
  if ('configuration' === packet.type) {
@@ -539,6 +564,7 @@ var __webpack_exports__ = {};
539
564
  };
540
565
  const external_node_assert_namespaceObject = require("node:assert");
541
566
  var external_node_assert_default = /*#__PURE__*/ __webpack_require__.n(external_node_assert_namespaceObject);
567
+ const external_node_child_process_namespaceObject = require("node:child_process");
542
568
  var external_node_fs_ = __webpack_require__("node:fs");
543
569
  var external_node_fs_default = /*#__PURE__*/ __webpack_require__.n(external_node_fs_);
544
570
  var external_node_module_ = __webpack_require__("node:module");
@@ -1201,14 +1227,23 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1201
1227
  const androidScreenshotPath = `/data/local/tmp/ms_${screenshotId}.png`;
1202
1228
  const useShellScreencap = 'number' == typeof this.options?.displayId;
1203
1229
  try {
1204
- if (useShellScreencap) throw new Error('Using shell screencap for displayId');
1205
- debugDevice('Taking screenshot via adb.takeScreenshot');
1206
- screenshotBuffer = await adb.takeScreenshot(null);
1207
- debugDevice('adb.takeScreenshot completed');
1208
- if (!screenshotBuffer) throw new Error('Failed to capture screenshot: screenshotBuffer is null');
1209
- if (!(0, img_namespaceObject.isValidImageBuffer)(screenshotBuffer)) {
1210
- debugDevice('Invalid image buffer detected: not a valid image format');
1211
- throw new Error('Screenshot buffer has invalid format: could not find valid image signature');
1230
+ if (!useShellScreencap && this.takeScreenshotFailCount < AndroidDevice.TAKE_SCREENSHOT_FAIL_THRESHOLD) {
1231
+ debugDevice('Taking screenshot via adb.takeScreenshot');
1232
+ screenshotBuffer = await adb.takeScreenshot(null);
1233
+ debugDevice('adb.takeScreenshot completed');
1234
+ if (!screenshotBuffer) {
1235
+ this.takeScreenshotFailCount++;
1236
+ throw new Error('Failed to capture screenshot: screenshotBuffer is null');
1237
+ }
1238
+ if (!(0, img_namespaceObject.isValidImageBuffer)(screenshotBuffer)) {
1239
+ debugDevice('Invalid image buffer detected: not a valid image format');
1240
+ this.takeScreenshotFailCount++;
1241
+ throw new Error('Screenshot buffer has invalid format: could not find valid image signature');
1242
+ }
1243
+ this.takeScreenshotFailCount = 0;
1244
+ } else {
1245
+ if (this.takeScreenshotFailCount >= AndroidDevice.TAKE_SCREENSHOT_FAIL_THRESHOLD) debugDevice('Skipping takeScreenshot (failed %d consecutive times), using shell screencap directly', this.takeScreenshotFailCount);
1246
+ throw new Error('Using shell screencap directly');
1212
1247
  }
1213
1248
  const validScreenshotBufferSize = this.options?.minScreenshotBufferSize ?? 10240;
1214
1249
  if (validScreenshotBufferSize > 0 && screenshotBuffer.length < validScreenshotBufferSize) {
@@ -1240,9 +1275,18 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1240
1275
  if (!(0, img_namespaceObject.isValidImageBuffer)(screenshotBuffer)) throw new Error('Fallback screenshot buffer has invalid PNG format');
1241
1276
  debugDevice(`Fallback screenshot validated successfully: ${screenshotBuffer.length} bytes`);
1242
1277
  } finally{
1243
- Promise.resolve().then(()=>adb.shell(`rm ${androidScreenshotPath}`)).catch((error)=>{
1244
- debugDevice(`Failed to delete remote screenshot: ${error}`);
1278
+ const adbPath = adb.executable?.path ?? 'adb';
1279
+ const child = (0, external_node_child_process_namespaceObject.execFile)(adbPath, [
1280
+ '-s',
1281
+ this.deviceId,
1282
+ 'shell',
1283
+ `rm ${androidScreenshotPath}`
1284
+ ], {
1285
+ timeout: 3000
1286
+ }, (err)=>{
1287
+ if (err) debugDevice('Failed to delete remote screenshot: %s', err.message);
1245
1288
  });
1289
+ child.unref();
1246
1290
  }
1247
1291
  }
1248
1292
  if (!screenshotBuffer) throw new Error('Failed to capture screenshot: all methods failed');
@@ -1686,6 +1730,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1686
1730
  device_define_property(this, "scrcpyAdapter", null);
1687
1731
  device_define_property(this, "appNameMapping", {});
1688
1732
  device_define_property(this, "cachedAdjustScale", null);
1733
+ device_define_property(this, "takeScreenshotFailCount", 0);
1689
1734
  device_define_property(this, "interfaceType", 'android');
1690
1735
  device_define_property(this, "uri", void 0);
1691
1736
  device_define_property(this, "options", void 0);
@@ -1695,6 +1740,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1695
1740
  this.customActions = options?.customActions;
1696
1741
  }
1697
1742
  }
1743
+ device_define_property(AndroidDevice, "TAKE_SCREENSHOT_FAIL_THRESHOLD", 3);
1698
1744
  const runAdbShellParamSchema = core_namespaceObject.z.object({
1699
1745
  command: core_namespaceObject.z.string().describe('ADB shell command to execute')
1700
1746
  });
@@ -74,6 +74,8 @@ export declare class AndroidDevice implements AbstractInterface {
74
74
  private scrcpyAdapter;
75
75
  private appNameMapping;
76
76
  private cachedAdjustScale;
77
+ private takeScreenshotFailCount;
78
+ private static readonly TAKE_SCREENSHOT_FAIL_THRESHOLD;
77
79
  interfaceType: InterfaceType;
78
80
  uri: string | undefined;
79
81
  options?: AndroidDeviceOpt;
@@ -337,6 +339,8 @@ declare class ScrcpyScreenshotManager {
337
339
  private startFrameConsumer;
338
340
  /**
339
341
  * Main frame consumption loop
342
+ * Includes busy-loop detection: if reader.read() resolves too fast
343
+ * (e.g. broken stream returning immediately), we throttle to prevent 100% CPU.
340
344
  */
341
345
  private consumeFramesLoop;
342
346
  /**
@@ -75,6 +75,8 @@ declare class AndroidDevice implements AbstractInterface {
75
75
  private scrcpyAdapter;
76
76
  private appNameMapping;
77
77
  private cachedAdjustScale;
78
+ private takeScreenshotFailCount;
79
+ private static readonly TAKE_SCREENSHOT_FAIL_THRESHOLD;
78
80
  interfaceType: InterfaceType;
79
81
  uri: string | undefined;
80
82
  options?: AndroidDeviceOpt;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@midscene/android",
3
- "version": "1.5.3-beta-20260309062917.0",
3
+ "version": "1.5.3",
4
4
  "description": "Android automation library for Midscene",
5
5
  "keywords": [
6
6
  "Android UI automation",
@@ -41,8 +41,8 @@
41
41
  "@yume-chan/stream-extra": "^1.0.0",
42
42
  "appium-adb": "12.12.1",
43
43
  "sharp": "^0.34.3",
44
- "@midscene/core": "1.5.3-beta-20260309062917.0",
45
- "@midscene/shared": "1.5.3-beta-20260309062917.0"
44
+ "@midscene/core": "1.5.3",
45
+ "@midscene/shared": "1.5.3"
46
46
  },
47
47
  "optionalDependencies": {
48
48
  "@ffmpeg-installer/ffmpeg": "^1.1.0"
@@ -56,7 +56,7 @@
56
56
  "tsx": "^4.19.2",
57
57
  "vitest": "3.0.5",
58
58
  "zod": "3.24.3",
59
- "@midscene/playground": "1.5.3-beta-20260309062917.0"
59
+ "@midscene/playground": "1.5.3"
60
60
  },
61
61
  "license": "MIT",
62
62
  "scripts": {