@midscene/android 1.10.8 → 1.10.9-beta-20260730110858.0

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
@@ -99,20 +99,12 @@ var __webpack_modules__ = {
99
99
  try {
100
100
  this.isConnecting = true;
101
101
  debugScrcpy('Starting scrcpy connection...');
102
- const { AdbScrcpyClient, AdbScrcpyOptions3_3_3 } = await import("@yume-chan/adb-scrcpy");
102
+ const { AdbScrcpyClient } = await import("@yume-chan/adb-scrcpy");
103
103
  const { ReadableStream } = await import("@yume-chan/stream-extra");
104
104
  const { DefaultServerPath } = await import("@yume-chan/scrcpy");
105
105
  const serverBinPath = this.resolveServerBinPath();
106
106
  await AdbScrcpyClient.pushServer(this.adb, ReadableStream.from((0, node_fs__rspack_import_0.createReadStream)(serverBinPath)));
107
- const scrcpyOptions = new AdbScrcpyOptions3_3_3({
108
- audio: false,
109
- control: false,
110
- maxSize: this.options.maxSize,
111
- videoBitRate: this.options.videoBitRate,
112
- maxFps: 10,
113
- sendFrameMeta: true,
114
- videoCodecOptions: 'i-frame-interval=0,bitrate-mode=2'
115
- });
107
+ const scrcpyOptions = await this.createScrcpyOptions();
116
108
  this.scrcpyClient = await AdbScrcpyClient.start(this.adb, DefaultServerPath, scrcpyOptions);
117
109
  serverOutputTask = this.collectServerOutput(this.scrcpyClient.output, serverOutput);
118
110
  const videoStreamPromise = this.scrcpyClient.videoStream;
@@ -140,6 +132,23 @@ var __webpack_modules__ = {
140
132
  this.isConnecting = false;
141
133
  }
142
134
  }
135
+ async createScrcpyOptions() {
136
+ const [{ AdbScrcpyOptions3_3_3 }, { ScrcpyInstanceId }] = await Promise.all([
137
+ import("@yume-chan/adb-scrcpy"),
138
+ import("@yume-chan/scrcpy")
139
+ ]);
140
+ return new AdbScrcpyOptions3_3_3({
141
+ audio: false,
142
+ control: false,
143
+ tunnelForward: true,
144
+ scid: ScrcpyInstanceId.random(),
145
+ maxSize: this.options.maxSize,
146
+ videoBitRate: this.options.videoBitRate,
147
+ maxFps: 10,
148
+ sendFrameMeta: true,
149
+ videoCodecOptions: 'i-frame-interval=0,bitrate-mode=2'
150
+ });
151
+ }
143
152
  async collectServerOutput(output, lines) {
144
153
  const reader = output.getReader();
145
154
  try {
@@ -880,6 +889,230 @@ class ScrcpyDeviceAdapter {
880
889
  this.retryAfter = null;
881
890
  }
882
891
  }
892
+ const TAG_RE = /<(\/?)([A-Za-z_][A-Za-z0-9_.\-:]*)([^>]*?)(\/?)>/g;
893
+ const ATTR_RE = /([A-Za-z_][A-Za-z0-9_.\-:]*)\s*=\s*("([^"]*)"|'([^']*)')/g;
894
+ const BOUNDS_RE = /^\[(-?\d+),(-?\d+)\]\[(-?\d+),(-?\d+)\]$/;
895
+ const ENTITY_TABLE = {
896
+ amp: '&',
897
+ lt: '<',
898
+ gt: '>',
899
+ quot: '"',
900
+ apos: "'"
901
+ };
902
+ function decodeEntities(value) {
903
+ if (!value.includes('&')) return value;
904
+ return value.replace(/&(#x?[0-9a-fA-F]+|[a-zA-Z]+);/g, (full, body)=>{
905
+ if (body.startsWith('#x') || body.startsWith('#X')) {
906
+ const code = Number.parseInt(body.slice(2), 16);
907
+ return Number.isFinite(code) ? String.fromCodePoint(code) : full;
908
+ }
909
+ if (body.startsWith('#')) {
910
+ const code = Number.parseInt(body.slice(1), 10);
911
+ return Number.isFinite(code) ? String.fromCodePoint(code) : full;
912
+ }
913
+ return ENTITY_TABLE[body] ?? full;
914
+ });
915
+ }
916
+ function parseAttrs(raw) {
917
+ const attrs = {};
918
+ ATTR_RE.lastIndex = 0;
919
+ let match;
920
+ while(match = ATTR_RE.exec(raw))attrs[match[1]] = decodeEntities(match[3] ?? match[4] ?? '');
921
+ return attrs;
922
+ }
923
+ function parseXml(input) {
924
+ if (0 === input.trim().length) throw new Error('parseAndroidUITreeXml: input is empty');
925
+ const cleaned = input.replace(/<\?xml[\s\S]*?\?>/g, '').replace(/<!DOCTYPE[\s\S]*?>/gi, '').replace(/<!--[\s\S]*?-->/g, '');
926
+ TAG_RE.lastIndex = 0;
927
+ const stack = [];
928
+ let root;
929
+ let match;
930
+ while(match = TAG_RE.exec(cleaned)){
931
+ const isClose = '/' === match[1];
932
+ const name = match[2];
933
+ if (isClose) {
934
+ const current = stack.pop();
935
+ if (!current || current.name !== name) throw new Error(`parseAndroidUITreeXml: unbalanced close tag </${name}>`);
936
+ continue;
937
+ }
938
+ const element = {
939
+ name,
940
+ attrs: parseAttrs(match[3] ?? ''),
941
+ children: []
942
+ };
943
+ const parent = stack[stack.length - 1];
944
+ if (parent) parent.children.push(element);
945
+ else if (root) throw new Error('parseAndroidUITreeXml: more than one top-level element');
946
+ else root = element;
947
+ if ('/' !== match[4]) stack.push(element);
948
+ }
949
+ if (stack.length > 0) throw new Error('parseAndroidUITreeXml: unclosed element');
950
+ if (!root) throw new Error('parseAndroidUITreeXml: no root element');
951
+ return root;
952
+ }
953
+ function parseBounds(raw, devicePixelRatio) {
954
+ const match = raw ? BOUNDS_RE.exec(raw) : void 0;
955
+ if (!match) return {
956
+ left: 0,
957
+ top: 0,
958
+ width: 0,
959
+ height: 0
960
+ };
961
+ const left = Number.parseInt(match[1], 10);
962
+ const top = Number.parseInt(match[2], 10);
963
+ const right = Number.parseInt(match[3], 10);
964
+ const bottom = Number.parseInt(match[4], 10);
965
+ const ratio = devicePixelRatio > 0 ? devicePixelRatio : 1;
966
+ return {
967
+ left: left / ratio,
968
+ top: top / ratio,
969
+ width: Math.max(0, right - left) / ratio,
970
+ height: Math.max(0, bottom - top) / ratio
971
+ };
972
+ }
973
+ function toUiNode(element, devicePixelRatio) {
974
+ const attrs = {};
975
+ for (const [name, value] of Object.entries(element.attrs))if ('bounds' !== name) attrs[name] = value;
976
+ return {
977
+ type: element.attrs.class || element.name,
978
+ attrs,
979
+ bounds: parseBounds(element.attrs.bounds, devicePixelRatio),
980
+ children: element.children.map((child)=>toUiNode(child, devicePixelRatio))
981
+ };
982
+ }
983
+ function uiautomatorXmlToUiNode(xml, devicePixelRatio) {
984
+ const root = parseXml(xml);
985
+ if ('hierarchy' === root.name && 1 === root.children.length) return toUiNode(root.children[0], devicePixelRatio);
986
+ return toUiNode(root, devicePixelRatio);
987
+ }
988
+ const ACCESSIBILITY_DUMP_ATTEMPTS = 3;
989
+ const ACCESSIBILITY_DUMP_TIMEOUT_MS = 5000;
990
+ const ACCESSIBILITY_DUMP_RETRY_DELAY_MS = 250;
991
+ const UIAUTOMATOR_LAYOUT_PATH = '/sdcard/midscene_window_dump.xml';
992
+ const debugUITree = (0, logger_.getDebug)('android:ui-tree');
993
+ function errorMessage(error) {
994
+ return error instanceof Error ? error.message : String(error);
995
+ }
996
+ function validateHierarchyXml(xml) {
997
+ if ('string' != typeof xml || !/<hierarchy(?:\s|>)/.test(xml) || !xml.includes('</hierarchy>')) throw new Error('uiautomator did not produce valid hierarchy XML');
998
+ }
999
+ function focusedPackageForLogicalDisplay(windowDump, logicalDisplayId) {
1000
+ const displayMatches = [
1001
+ ...windowDump.matchAll(/^\s*Display: mDisplayId=(\d+)\b.*$/gm)
1002
+ ];
1003
+ const displayIndex = displayMatches.findIndex((match)=>Number(match[1]) === logicalDisplayId);
1004
+ if (-1 === displayIndex) return null;
1005
+ const start = displayMatches[displayIndex].index ?? 0;
1006
+ const end = displayMatches[displayIndex + 1]?.index ?? windowDump.length;
1007
+ const displayBlock = windowDump.slice(start, end);
1008
+ for (const field of [
1009
+ 'mCurrentFocus',
1010
+ 'mFocusedApp'
1011
+ ]){
1012
+ const value = displayBlock.match(new RegExp(`^\\s*${field}=([^\\n]*)$`, 'm'))?.[1];
1013
+ const packageName = value?.match(/\bu\d+\s+([A-Za-z0-9_$.-]+)\//)?.[1];
1014
+ if (packageName) return packageName;
1015
+ }
1016
+ return null;
1017
+ }
1018
+ function firstTreePackage(root) {
1019
+ if (root.attrs.package) return root.attrs.package;
1020
+ for (const child of root.children){
1021
+ const packageName = firstTreePackage(child);
1022
+ if (packageName) return packageName;
1023
+ }
1024
+ return null;
1025
+ }
1026
+ function windowDisplayIdsForPackage(windowDump, packageName) {
1027
+ const windowMatches = [
1028
+ ...windowDump.matchAll(/^\s*Window #\d+ Window\{.*$/gm)
1029
+ ];
1030
+ const displayIds = new Set();
1031
+ for(let index = 0; index < windowMatches.length; index++){
1032
+ const start = windowMatches[index].index ?? 0;
1033
+ const end = windowMatches[index + 1]?.index ?? windowDump.length;
1034
+ const windowBlock = windowDump.slice(start, end);
1035
+ const ownerPackage = windowBlock.match(/^\s*mOwnerUid=.*\bpackage=([A-Za-z0-9_$.-]+)\b/m)?.[1];
1036
+ const headerPackage = windowMatches[index][0].match(/\bu\d+\s+([A-Za-z0-9_$.-]+)\//)?.[1];
1037
+ if (ownerPackage !== packageName && headerPackage !== packageName) continue;
1038
+ const displayId = windowBlock.match(/^\s*mDisplayId=(\d+)\b/m)?.[1];
1039
+ if (void 0 !== displayId) displayIds.add(Number(displayId));
1040
+ }
1041
+ return [
1042
+ ...displayIds
1043
+ ];
1044
+ }
1045
+ function uiTreeExtent(root) {
1046
+ let right = root.bounds.left + root.bounds.width;
1047
+ let bottom = root.bounds.top + root.bounds.height;
1048
+ for (const child of root.children){
1049
+ const childExtent = uiTreeExtent(child);
1050
+ right = Math.max(right, childExtent.right);
1051
+ bottom = Math.max(bottom, childExtent.bottom);
1052
+ }
1053
+ return {
1054
+ right,
1055
+ bottom
1056
+ };
1057
+ }
1058
+ async function dumpAccessibilityXml(adb) {
1059
+ await runAdbShellStdoutOrThrow(adb, `rm -f ${UIAUTOMATOR_LAYOUT_PATH}`, {
1060
+ timeout: ACCESSIBILITY_DUMP_TIMEOUT_MS
1061
+ });
1062
+ await runAdbShellStdoutOrThrow(adb, `uiautomator dump --compressed ${UIAUTOMATOR_LAYOUT_PATH}`, {
1063
+ timeout: ACCESSIBILITY_DUMP_TIMEOUT_MS
1064
+ });
1065
+ const xml = await runAdbShellStdoutOrThrow(adb, `cat ${UIAUTOMATOR_LAYOUT_PATH}`, {
1066
+ timeout: ACCESSIBILITY_DUMP_TIMEOUT_MS
1067
+ });
1068
+ validateHierarchyXml(xml);
1069
+ return xml;
1070
+ }
1071
+ async function validateUITreeDisplay(root, options) {
1072
+ if ('number' != typeof options.displayId) return;
1073
+ const { adb, displayId } = options;
1074
+ const displayWindowDump = await runAdbShellStdoutOrThrow(adb, 'dumpsys window displays', {
1075
+ timeout: ACCESSIBILITY_DUMP_TIMEOUT_MS
1076
+ });
1077
+ const expectedPackage = focusedPackageForLogicalDisplay(displayWindowDump, displayId);
1078
+ const treePackage = firstTreePackage(root);
1079
+ if (!expectedPackage) throw new Error(`Unable to verify Android UI tree display alignment for displayId=${displayId}: no focused package found on the target display`);
1080
+ if (!treePackage) throw new Error(`Unable to verify Android UI tree display alignment for displayId=${displayId}: accessibility hierarchy has no package`);
1081
+ const allWindowDump = await runAdbShellStdoutOrThrow(adb, 'dumpsys window windows', {
1082
+ timeout: ACCESSIBILITY_DUMP_TIMEOUT_MS
1083
+ });
1084
+ const treePackageDisplayIds = windowDisplayIdsForPackage(allWindowDump, treePackage);
1085
+ if (0 === treePackageDisplayIds.length) throw new Error(`Unable to verify Android UI tree display alignment for displayId=${displayId}: no window display ownership found for accessibility package ${treePackage}`);
1086
+ if (!treePackageDisplayIds.includes(displayId)) throw new Error(`Android UI tree display mismatch for displayId=${displayId}: expected focused package ${expectedPackage}, but accessibility hierarchy belongs to ${treePackage} on displayId=${treePackageDisplayIds.join(',')}`);
1087
+ const targetSize = await options.getDisplaySize();
1088
+ const extent = uiTreeExtent(root);
1089
+ const tolerance = 1;
1090
+ if (extent.right > targetSize.width + tolerance || extent.bottom > targetSize.height + tolerance) throw new Error(`Android UI tree bounds exceed displayId=${displayId}: tree ${Math.round(extent.right)}x${Math.round(extent.bottom)} is outside target ${targetSize.width}x${targetSize.height}`);
1091
+ }
1092
+ async function captureAndroidUITree(options) {
1093
+ const failures = [];
1094
+ for(let attempt = 1; attempt <= ACCESSIBILITY_DUMP_ATTEMPTS; attempt++){
1095
+ const startedAt = Date.now();
1096
+ try {
1097
+ const xml = await dumpAccessibilityXml(options.adb);
1098
+ const capturedAt = Date.now();
1099
+ const root = uiautomatorXmlToUiNode(xml, options.devicePixelRatio);
1100
+ await validateUITreeDisplay(root, options);
1101
+ debugUITree('capture source=%s attempt=%d durationMs=%d bytes=%d', 'uiautomator', attempt, Date.now() - startedAt, xml.length);
1102
+ return {
1103
+ platform: 'android',
1104
+ capturedAt,
1105
+ root
1106
+ };
1107
+ } catch (error) {
1108
+ const failure = `uiautomator attempt ${attempt}/${ACCESSIBILITY_DUMP_ATTEMPTS}: ${errorMessage(error)}`;
1109
+ failures.push(failure);
1110
+ debugUITree('capture failed source=%s attempt=%d/%d durationMs=%d error=%s', 'uiautomator', attempt, ACCESSIBILITY_DUMP_ATTEMPTS, Date.now() - startedAt, errorMessage(error));
1111
+ if (attempt < ACCESSIBILITY_DUMP_ATTEMPTS) await sleep(ACCESSIBILITY_DUMP_RETRY_DELAY_MS);
1112
+ }
1113
+ }
1114
+ throw new Error(`Unable to capture Android UI tree after ${failures.length} attempt(s): ${failures.join('; ')}`);
1115
+ }
883
1116
  function device_define_property(obj, key, value) {
884
1117
  if (key in obj) Object.defineProperty(obj, key, {
885
1118
  value: value,
@@ -899,6 +1132,35 @@ const debugDevice = (0, logger_.getDebug)('android:device');
899
1132
  const warnDevice = (0, logger_.getDebug)('android:device', {
900
1133
  console: true
901
1134
  });
1135
+ function displayViewportForLogicalDisplay(displayDump, logicalDisplayId) {
1136
+ for (const viewportMatch of displayDump.matchAll(/DisplayViewport\{([^}]*)\}/g)){
1137
+ const viewport = viewportMatch[1];
1138
+ const displayId = viewport.match(/\bdisplayId=(\d+)\b/)?.[1];
1139
+ if (Number(displayId) === logicalDisplayId) return viewportMatch[0];
1140
+ }
1141
+ return null;
1142
+ }
1143
+ function displayInfoLineForPhysicalDisplay(displayDump, physicalDisplayId) {
1144
+ const lineRegex = new RegExp(`^.*uniqueId ["']local:${physicalDisplayId}["'].*$`, 'm');
1145
+ return displayDump.match(lineRegex)?.[0] ?? null;
1146
+ }
1147
+ function physicalDisplayIdForLogicalDisplay(displayDump, logicalDisplayId) {
1148
+ const viewport = displayViewportForLogicalDisplay(displayDump, logicalDisplayId);
1149
+ if (viewport) {
1150
+ const physicalId = viewport.match(/\buniqueId=['"]local:(\d+)['"]/)?.[1];
1151
+ if (physicalId) return physicalId;
1152
+ }
1153
+ const lines = displayDump.split(/\r?\n/);
1154
+ for(let index = 0; index < lines.length; index++){
1155
+ const displayId = lines[index].match(/^\s*mDisplayId=(\d+)\b/)?.[1];
1156
+ if (Number(displayId) === logicalDisplayId) for(let cursor = index + 1; cursor < lines.length; cursor++){
1157
+ if (/^\s*mDisplayId=\d+\b/.test(lines[cursor])) break;
1158
+ const physicalId = lines[cursor].match(/mBaseDisplayInfo=.*\buniqueId "local:(\d+)"/)?.[1];
1159
+ if (physicalId) return physicalId;
1160
+ }
1161
+ }
1162
+ return null;
1163
+ }
902
1164
  function escapeForShell(text) {
903
1165
  return text.replace(/'/g, "'\\''").replace(/\n/g, '\\n');
904
1166
  }
@@ -1186,6 +1448,15 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1186
1448
  children: []
1187
1449
  };
1188
1450
  }
1451
+ async getUITree() {
1452
+ await this.initializeDevicePixelRatio();
1453
+ return captureAndroidUITree({
1454
+ adb: await this.getAdb(),
1455
+ devicePixelRatio: this.devicePixelRatio,
1456
+ displayId: this.options?.displayId,
1457
+ getDisplaySize: ()=>this.size()
1458
+ });
1459
+ }
1189
1460
  async getScreenSize() {
1190
1461
  const shouldCache = !(this.options?.alwaysRefreshScreenInfo ?? false);
1191
1462
  debugDevice(`getScreenSize: alwaysRefreshScreenInfo=${this.options?.alwaysRefreshScreenInfo}, shouldCache=${shouldCache}, hasCachedSize=${!!this.cachedScreenSize}`);
@@ -1194,13 +1465,10 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1194
1465
  if ('number' == typeof this.options?.displayId) try {
1195
1466
  const stdout = await adb.shell('dumpsys display');
1196
1467
  if (this.options?.usePhysicalDisplayIdForDisplayLookup) {
1197
- const physicalDisplayId = await this.getPhysicalDisplayId();
1468
+ const physicalDisplayId = await this.resolvePhysicalDisplayId(stdout);
1198
1469
  if (physicalDisplayId) {
1199
- const lineRegex = new RegExp(`^.*uniqueId \"local:${physicalDisplayId}\".*$
1200
- `, 'm');
1201
- const lineMatch = stdout.match(lineRegex);
1202
- if (lineMatch) {
1203
- const targetLine = lineMatch[0];
1470
+ const targetLine = displayInfoLineForPhysicalDisplay(stdout, physicalDisplayId);
1471
+ if (targetLine) {
1204
1472
  const realMatch = targetLine.match(/real (\d+) x (\d+)/);
1205
1473
  const rotationMatch = targetLine.match(/rotation (\d+)/);
1206
1474
  if (realMatch && rotationMatch) {
@@ -1221,10 +1489,8 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1221
1489
  }
1222
1490
  }
1223
1491
  } else {
1224
- const viewportRegex = new RegExp(`DisplayViewport{[^}]*displayId=${this.options.displayId}[^}]*}`, 'g');
1225
- const match = stdout.match(viewportRegex);
1226
- if (match) {
1227
- const targetLine = match[0];
1492
+ const targetLine = displayViewportForLogicalDisplay(stdout, this.options.displayId);
1493
+ if (targetLine) {
1228
1494
  const physicalFrameMatch = targetLine.match(/physicalFrame=Rect\(\d+, \d+ - (\d+), (\d+)\)/);
1229
1495
  const orientationMatch = targetLine.match(/orientation=(\d+)/);
1230
1496
  if (physicalFrameMatch && orientationMatch) {
@@ -1290,13 +1556,10 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1290
1556
  if ('number' == typeof this.options?.displayId) try {
1291
1557
  const stdout = await adb.shell('dumpsys display');
1292
1558
  if (this.options?.usePhysicalDisplayIdForDisplayLookup) {
1293
- const physicalDisplayId = await this.getPhysicalDisplayId();
1559
+ const physicalDisplayId = await this.resolvePhysicalDisplayId(stdout);
1294
1560
  if (physicalDisplayId) {
1295
- const lineRegex = new RegExp(`^.*uniqueId \"local:${physicalDisplayId}\".*$
1296
- `, 'm');
1297
- const lineMatch = stdout.match(lineRegex);
1298
- if (lineMatch) {
1299
- const targetLine = lineMatch[0];
1561
+ const targetLine = displayInfoLineForPhysicalDisplay(stdout, physicalDisplayId);
1562
+ if (targetLine) {
1300
1563
  const densityMatch = targetLine.match(/density (\d+)/);
1301
1564
  if (densityMatch) {
1302
1565
  const density = Number(densityMatch[1]);
@@ -1434,7 +1697,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1434
1697
  debugDevice('screenshotBase64 end (scrcpy mode)');
1435
1698
  return result;
1436
1699
  } catch (error) {
1437
- debugDevice(`Scrcpy screenshot failed, falling back to standard ADB method.\nError: ${error}`);
1700
+ warnDevice(`Scrcpy screenshot failed, falling back to standard ADB method.\nError: ${error}`);
1438
1701
  }
1439
1702
  const adb = await this.getAdb();
1440
1703
  let screenshotBuffer;
@@ -1921,6 +2184,9 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1921
2184
  if ('number' == typeof this.options?.displayId && 0 !== this.options.displayId) warnDevice(`yadb ${operation} cannot target display ${this.options.displayId}; it will act on the default display.`);
1922
2185
  }
1923
2186
  async getPhysicalDisplayId() {
2187
+ return this.resolvePhysicalDisplayId();
2188
+ }
2189
+ async resolvePhysicalDisplayId(displayDump) {
1924
2190
  if (void 0 !== this.cachedPhysicalDisplayId) return this.cachedPhysicalDisplayId;
1925
2191
  if ('number' != typeof this.options?.displayId) {
1926
2192
  this.cachedPhysicalDisplayId = null;
@@ -1928,6 +2194,13 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1928
2194
  }
1929
2195
  const adb = await this.getAdb();
1930
2196
  try {
2197
+ const resolvedDisplayDump = displayDump ?? await adb.shell('dumpsys display');
2198
+ const logicalDisplayPhysicalId = physicalDisplayIdForLogicalDisplay(resolvedDisplayDump, this.options.displayId);
2199
+ if (logicalDisplayPhysicalId) {
2200
+ this.cachedPhysicalDisplayId = logicalDisplayPhysicalId;
2201
+ debugDevice(`Found and cached physical display ID: ${logicalDisplayPhysicalId} for logical display ID: ${this.options.displayId}`);
2202
+ return this.cachedPhysicalDisplayId;
2203
+ }
1931
2204
  const stdout = await adb.shell(`dumpsys SurfaceFlinger --display-id ${this.options.displayId}`);
1932
2205
  const regex = new RegExp(`Display (\\d+) \\(HWC display ${this.options.displayId}\\):`);
1933
2206
  const displayMatch = stdout.match(regex);
@@ -2312,7 +2585,7 @@ class AndroidMidsceneTools extends BaseMidsceneTools {
2312
2585
  const tools = new AndroidMidsceneTools();
2313
2586
  runToolsCLI(tools, 'midscene-android', {
2314
2587
  stripPrefix: 'android_',
2315
- version: "1.10.8",
2588
+ version: "1.10.9-beta-20260730110858.0",
2316
2589
  extraCommands: createReportCliCommands()
2317
2590
  }).catch((e)=>{
2318
2591
  process.exit(reportCLIError(e));