@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 +301 -28
- package/dist/es/index.mjs +300 -27
- package/dist/lib/cli.js +301 -28
- package/dist/lib/index.js +300 -27
- package/dist/types/index.d.ts +5 -2
- package/package.json +4 -4
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
|
|
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 =
|
|
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.
|
|
1468
|
+
const physicalDisplayId = await this.resolvePhysicalDisplayId(stdout);
|
|
1198
1469
|
if (physicalDisplayId) {
|
|
1199
|
-
const
|
|
1200
|
-
|
|
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
|
|
1225
|
-
|
|
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.
|
|
1559
|
+
const physicalDisplayId = await this.resolvePhysicalDisplayId(stdout);
|
|
1294
1560
|
if (physicalDisplayId) {
|
|
1295
|
-
const
|
|
1296
|
-
|
|
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
|
-
|
|
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.
|
|
2588
|
+
version: "1.10.9-beta-20260730110858.0",
|
|
2316
2589
|
extraCommands: createReportCliCommands()
|
|
2317
2590
|
}).catch((e)=>{
|
|
2318
2591
|
process.exit(reportCLIError(e));
|