@midscene/android 1.10.8 → 1.10.9-beta-20260731103801.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/lib/cli.js
CHANGED
|
@@ -87,20 +87,12 @@ var __webpack_modules__ = {
|
|
|
87
87
|
try {
|
|
88
88
|
this.isConnecting = true;
|
|
89
89
|
debugScrcpy('Starting scrcpy connection...');
|
|
90
|
-
const { AdbScrcpyClient
|
|
90
|
+
const { AdbScrcpyClient } = await import("@yume-chan/adb-scrcpy");
|
|
91
91
|
const { ReadableStream } = await import("@yume-chan/stream-extra");
|
|
92
92
|
const { DefaultServerPath } = await import("@yume-chan/scrcpy");
|
|
93
93
|
const serverBinPath = this.resolveServerBinPath();
|
|
94
94
|
await AdbScrcpyClient.pushServer(this.adb, ReadableStream.from((0, node_fs__rspack_import_0.createReadStream)(serverBinPath)));
|
|
95
|
-
const scrcpyOptions =
|
|
96
|
-
audio: false,
|
|
97
|
-
control: false,
|
|
98
|
-
maxSize: this.options.maxSize,
|
|
99
|
-
videoBitRate: this.options.videoBitRate,
|
|
100
|
-
maxFps: 10,
|
|
101
|
-
sendFrameMeta: true,
|
|
102
|
-
videoCodecOptions: 'i-frame-interval=0,bitrate-mode=2'
|
|
103
|
-
});
|
|
95
|
+
const scrcpyOptions = await this.createScrcpyOptions();
|
|
104
96
|
this.scrcpyClient = await AdbScrcpyClient.start(this.adb, DefaultServerPath, scrcpyOptions);
|
|
105
97
|
serverOutputTask = this.collectServerOutput(this.scrcpyClient.output, serverOutput);
|
|
106
98
|
const videoStreamPromise = this.scrcpyClient.videoStream;
|
|
@@ -128,6 +120,23 @@ var __webpack_modules__ = {
|
|
|
128
120
|
this.isConnecting = false;
|
|
129
121
|
}
|
|
130
122
|
}
|
|
123
|
+
async createScrcpyOptions() {
|
|
124
|
+
const [{ AdbScrcpyOptions3_3_3 }, { ScrcpyInstanceId }] = await Promise.all([
|
|
125
|
+
import("@yume-chan/adb-scrcpy"),
|
|
126
|
+
import("@yume-chan/scrcpy")
|
|
127
|
+
]);
|
|
128
|
+
return new AdbScrcpyOptions3_3_3({
|
|
129
|
+
audio: false,
|
|
130
|
+
control: false,
|
|
131
|
+
tunnelForward: true,
|
|
132
|
+
scid: ScrcpyInstanceId.random(),
|
|
133
|
+
maxSize: this.options.maxSize,
|
|
134
|
+
videoBitRate: this.options.videoBitRate,
|
|
135
|
+
maxFps: 10,
|
|
136
|
+
sendFrameMeta: true,
|
|
137
|
+
videoCodecOptions: 'i-frame-interval=0,bitrate-mode=2'
|
|
138
|
+
});
|
|
139
|
+
}
|
|
131
140
|
async collectServerOutput(output, lines) {
|
|
132
141
|
const reader = output.getReader();
|
|
133
142
|
try {
|
|
@@ -895,6 +904,230 @@ ${stdout ? truncateAdbShellStream(stdout, 'stdout') : EMPTY_ADB_SHELL_STDOUT}`;
|
|
|
895
904
|
this.retryAfter = null;
|
|
896
905
|
}
|
|
897
906
|
}
|
|
907
|
+
const TAG_RE = /<(\/?)([A-Za-z_][A-Za-z0-9_.\-:]*)([^>]*?)(\/?)>/g;
|
|
908
|
+
const ATTR_RE = /([A-Za-z_][A-Za-z0-9_.\-:]*)\s*=\s*("([^"]*)"|'([^']*)')/g;
|
|
909
|
+
const BOUNDS_RE = /^\[(-?\d+),(-?\d+)\]\[(-?\d+),(-?\d+)\]$/;
|
|
910
|
+
const ENTITY_TABLE = {
|
|
911
|
+
amp: '&',
|
|
912
|
+
lt: '<',
|
|
913
|
+
gt: '>',
|
|
914
|
+
quot: '"',
|
|
915
|
+
apos: "'"
|
|
916
|
+
};
|
|
917
|
+
function decodeEntities(value) {
|
|
918
|
+
if (!value.includes('&')) return value;
|
|
919
|
+
return value.replace(/&(#x?[0-9a-fA-F]+|[a-zA-Z]+);/g, (full, body)=>{
|
|
920
|
+
if (body.startsWith('#x') || body.startsWith('#X')) {
|
|
921
|
+
const code = Number.parseInt(body.slice(2), 16);
|
|
922
|
+
return Number.isFinite(code) ? String.fromCodePoint(code) : full;
|
|
923
|
+
}
|
|
924
|
+
if (body.startsWith('#')) {
|
|
925
|
+
const code = Number.parseInt(body.slice(1), 10);
|
|
926
|
+
return Number.isFinite(code) ? String.fromCodePoint(code) : full;
|
|
927
|
+
}
|
|
928
|
+
return ENTITY_TABLE[body] ?? full;
|
|
929
|
+
});
|
|
930
|
+
}
|
|
931
|
+
function parseAttrs(raw) {
|
|
932
|
+
const attrs = {};
|
|
933
|
+
ATTR_RE.lastIndex = 0;
|
|
934
|
+
let match;
|
|
935
|
+
while(match = ATTR_RE.exec(raw))attrs[match[1]] = decodeEntities(match[3] ?? match[4] ?? '');
|
|
936
|
+
return attrs;
|
|
937
|
+
}
|
|
938
|
+
function parseXml(input) {
|
|
939
|
+
if (0 === input.trim().length) throw new Error('parseAndroidUITreeXml: input is empty');
|
|
940
|
+
const cleaned = input.replace(/<\?xml[\s\S]*?\?>/g, '').replace(/<!DOCTYPE[\s\S]*?>/gi, '').replace(/<!--[\s\S]*?-->/g, '');
|
|
941
|
+
TAG_RE.lastIndex = 0;
|
|
942
|
+
const stack = [];
|
|
943
|
+
let root;
|
|
944
|
+
let match;
|
|
945
|
+
while(match = TAG_RE.exec(cleaned)){
|
|
946
|
+
const isClose = '/' === match[1];
|
|
947
|
+
const name = match[2];
|
|
948
|
+
if (isClose) {
|
|
949
|
+
const current = stack.pop();
|
|
950
|
+
if (!current || current.name !== name) throw new Error(`parseAndroidUITreeXml: unbalanced close tag </${name}>`);
|
|
951
|
+
continue;
|
|
952
|
+
}
|
|
953
|
+
const element = {
|
|
954
|
+
name,
|
|
955
|
+
attrs: parseAttrs(match[3] ?? ''),
|
|
956
|
+
children: []
|
|
957
|
+
};
|
|
958
|
+
const parent = stack[stack.length - 1];
|
|
959
|
+
if (parent) parent.children.push(element);
|
|
960
|
+
else if (root) throw new Error('parseAndroidUITreeXml: more than one top-level element');
|
|
961
|
+
else root = element;
|
|
962
|
+
if ('/' !== match[4]) stack.push(element);
|
|
963
|
+
}
|
|
964
|
+
if (stack.length > 0) throw new Error('parseAndroidUITreeXml: unclosed element');
|
|
965
|
+
if (!root) throw new Error('parseAndroidUITreeXml: no root element');
|
|
966
|
+
return root;
|
|
967
|
+
}
|
|
968
|
+
function parseBounds(raw, devicePixelRatio) {
|
|
969
|
+
const match = raw ? BOUNDS_RE.exec(raw) : void 0;
|
|
970
|
+
if (!match) return {
|
|
971
|
+
left: 0,
|
|
972
|
+
top: 0,
|
|
973
|
+
width: 0,
|
|
974
|
+
height: 0
|
|
975
|
+
};
|
|
976
|
+
const left = Number.parseInt(match[1], 10);
|
|
977
|
+
const top = Number.parseInt(match[2], 10);
|
|
978
|
+
const right = Number.parseInt(match[3], 10);
|
|
979
|
+
const bottom = Number.parseInt(match[4], 10);
|
|
980
|
+
const ratio = devicePixelRatio > 0 ? devicePixelRatio : 1;
|
|
981
|
+
return {
|
|
982
|
+
left: left / ratio,
|
|
983
|
+
top: top / ratio,
|
|
984
|
+
width: Math.max(0, right - left) / ratio,
|
|
985
|
+
height: Math.max(0, bottom - top) / ratio
|
|
986
|
+
};
|
|
987
|
+
}
|
|
988
|
+
function toUiNode(element, devicePixelRatio) {
|
|
989
|
+
const attrs = {};
|
|
990
|
+
for (const [name, value] of Object.entries(element.attrs))if ('bounds' !== name) attrs[name] = value;
|
|
991
|
+
return {
|
|
992
|
+
type: element.attrs.class || element.name,
|
|
993
|
+
attrs,
|
|
994
|
+
bounds: parseBounds(element.attrs.bounds, devicePixelRatio),
|
|
995
|
+
children: element.children.map((child)=>toUiNode(child, devicePixelRatio))
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
function uiautomatorXmlToUiNode(xml, devicePixelRatio) {
|
|
999
|
+
const root = parseXml(xml);
|
|
1000
|
+
if ('hierarchy' === root.name && 1 === root.children.length) return toUiNode(root.children[0], devicePixelRatio);
|
|
1001
|
+
return toUiNode(root, devicePixelRatio);
|
|
1002
|
+
}
|
|
1003
|
+
const ACCESSIBILITY_DUMP_ATTEMPTS = 3;
|
|
1004
|
+
const ACCESSIBILITY_DUMP_TIMEOUT_MS = 5000;
|
|
1005
|
+
const ACCESSIBILITY_DUMP_RETRY_DELAY_MS = 250;
|
|
1006
|
+
const UIAUTOMATOR_LAYOUT_PATH = '/sdcard/midscene_window_dump.xml';
|
|
1007
|
+
const debugUITree = (0, logger_.getDebug)('android:ui-tree');
|
|
1008
|
+
function errorMessage(error) {
|
|
1009
|
+
return error instanceof Error ? error.message : String(error);
|
|
1010
|
+
}
|
|
1011
|
+
function validateHierarchyXml(xml) {
|
|
1012
|
+
if ('string' != typeof xml || !/<hierarchy(?:\s|>)/.test(xml) || !xml.includes('</hierarchy>')) throw new Error('uiautomator did not produce valid hierarchy XML');
|
|
1013
|
+
}
|
|
1014
|
+
function focusedPackageForLogicalDisplay(windowDump, logicalDisplayId) {
|
|
1015
|
+
const displayMatches = [
|
|
1016
|
+
...windowDump.matchAll(/^\s*Display: mDisplayId=(\d+)\b.*$/gm)
|
|
1017
|
+
];
|
|
1018
|
+
const displayIndex = displayMatches.findIndex((match)=>Number(match[1]) === logicalDisplayId);
|
|
1019
|
+
if (-1 === displayIndex) return null;
|
|
1020
|
+
const start = displayMatches[displayIndex].index ?? 0;
|
|
1021
|
+
const end = displayMatches[displayIndex + 1]?.index ?? windowDump.length;
|
|
1022
|
+
const displayBlock = windowDump.slice(start, end);
|
|
1023
|
+
for (const field of [
|
|
1024
|
+
'mCurrentFocus',
|
|
1025
|
+
'mFocusedApp'
|
|
1026
|
+
]){
|
|
1027
|
+
const value = displayBlock.match(new RegExp(`^\\s*${field}=([^\\n]*)$`, 'm'))?.[1];
|
|
1028
|
+
const packageName = value?.match(/\bu\d+\s+([A-Za-z0-9_$.-]+)\//)?.[1];
|
|
1029
|
+
if (packageName) return packageName;
|
|
1030
|
+
}
|
|
1031
|
+
return null;
|
|
1032
|
+
}
|
|
1033
|
+
function firstTreePackage(root) {
|
|
1034
|
+
if (root.attrs.package) return root.attrs.package;
|
|
1035
|
+
for (const child of root.children){
|
|
1036
|
+
const packageName = firstTreePackage(child);
|
|
1037
|
+
if (packageName) return packageName;
|
|
1038
|
+
}
|
|
1039
|
+
return null;
|
|
1040
|
+
}
|
|
1041
|
+
function windowDisplayIdsForPackage(windowDump, packageName) {
|
|
1042
|
+
const windowMatches = [
|
|
1043
|
+
...windowDump.matchAll(/^\s*Window #\d+ Window\{.*$/gm)
|
|
1044
|
+
];
|
|
1045
|
+
const displayIds = new Set();
|
|
1046
|
+
for(let index = 0; index < windowMatches.length; index++){
|
|
1047
|
+
const start = windowMatches[index].index ?? 0;
|
|
1048
|
+
const end = windowMatches[index + 1]?.index ?? windowDump.length;
|
|
1049
|
+
const windowBlock = windowDump.slice(start, end);
|
|
1050
|
+
const ownerPackage = windowBlock.match(/^\s*mOwnerUid=.*\bpackage=([A-Za-z0-9_$.-]+)\b/m)?.[1];
|
|
1051
|
+
const headerPackage = windowMatches[index][0].match(/\bu\d+\s+([A-Za-z0-9_$.-]+)\//)?.[1];
|
|
1052
|
+
if (ownerPackage !== packageName && headerPackage !== packageName) continue;
|
|
1053
|
+
const displayId = windowBlock.match(/^\s*mDisplayId=(\d+)\b/m)?.[1];
|
|
1054
|
+
if (void 0 !== displayId) displayIds.add(Number(displayId));
|
|
1055
|
+
}
|
|
1056
|
+
return [
|
|
1057
|
+
...displayIds
|
|
1058
|
+
];
|
|
1059
|
+
}
|
|
1060
|
+
function uiTreeExtent(root) {
|
|
1061
|
+
let right = root.bounds.left + root.bounds.width;
|
|
1062
|
+
let bottom = root.bounds.top + root.bounds.height;
|
|
1063
|
+
for (const child of root.children){
|
|
1064
|
+
const childExtent = uiTreeExtent(child);
|
|
1065
|
+
right = Math.max(right, childExtent.right);
|
|
1066
|
+
bottom = Math.max(bottom, childExtent.bottom);
|
|
1067
|
+
}
|
|
1068
|
+
return {
|
|
1069
|
+
right,
|
|
1070
|
+
bottom
|
|
1071
|
+
};
|
|
1072
|
+
}
|
|
1073
|
+
async function dumpAccessibilityXml(adb) {
|
|
1074
|
+
await runAdbShellStdoutOrThrow(adb, `rm -f ${UIAUTOMATOR_LAYOUT_PATH}`, {
|
|
1075
|
+
timeout: ACCESSIBILITY_DUMP_TIMEOUT_MS
|
|
1076
|
+
});
|
|
1077
|
+
await runAdbShellStdoutOrThrow(adb, `uiautomator dump --compressed ${UIAUTOMATOR_LAYOUT_PATH}`, {
|
|
1078
|
+
timeout: ACCESSIBILITY_DUMP_TIMEOUT_MS
|
|
1079
|
+
});
|
|
1080
|
+
const xml = await runAdbShellStdoutOrThrow(adb, `cat ${UIAUTOMATOR_LAYOUT_PATH}`, {
|
|
1081
|
+
timeout: ACCESSIBILITY_DUMP_TIMEOUT_MS
|
|
1082
|
+
});
|
|
1083
|
+
validateHierarchyXml(xml);
|
|
1084
|
+
return xml;
|
|
1085
|
+
}
|
|
1086
|
+
async function validateUITreeDisplay(root, options) {
|
|
1087
|
+
if ('number' != typeof options.displayId) return;
|
|
1088
|
+
const { adb, displayId } = options;
|
|
1089
|
+
const displayWindowDump = await runAdbShellStdoutOrThrow(adb, 'dumpsys window displays', {
|
|
1090
|
+
timeout: ACCESSIBILITY_DUMP_TIMEOUT_MS
|
|
1091
|
+
});
|
|
1092
|
+
const expectedPackage = focusedPackageForLogicalDisplay(displayWindowDump, displayId);
|
|
1093
|
+
const treePackage = firstTreePackage(root);
|
|
1094
|
+
if (!expectedPackage) throw new Error(`Unable to verify Android UI tree display alignment for displayId=${displayId}: no focused package found on the target display`);
|
|
1095
|
+
if (!treePackage) throw new Error(`Unable to verify Android UI tree display alignment for displayId=${displayId}: accessibility hierarchy has no package`);
|
|
1096
|
+
const allWindowDump = await runAdbShellStdoutOrThrow(adb, 'dumpsys window windows', {
|
|
1097
|
+
timeout: ACCESSIBILITY_DUMP_TIMEOUT_MS
|
|
1098
|
+
});
|
|
1099
|
+
const treePackageDisplayIds = windowDisplayIdsForPackage(allWindowDump, treePackage);
|
|
1100
|
+
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}`);
|
|
1101
|
+
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(',')}`);
|
|
1102
|
+
const targetSize = await options.getDisplaySize();
|
|
1103
|
+
const extent = uiTreeExtent(root);
|
|
1104
|
+
const tolerance = 1;
|
|
1105
|
+
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}`);
|
|
1106
|
+
}
|
|
1107
|
+
async function captureAndroidUITree(options) {
|
|
1108
|
+
const failures = [];
|
|
1109
|
+
for(let attempt = 1; attempt <= ACCESSIBILITY_DUMP_ATTEMPTS; attempt++){
|
|
1110
|
+
const startedAt = Date.now();
|
|
1111
|
+
try {
|
|
1112
|
+
const xml = await dumpAccessibilityXml(options.adb);
|
|
1113
|
+
const capturedAt = Date.now();
|
|
1114
|
+
const root = uiautomatorXmlToUiNode(xml, options.devicePixelRatio);
|
|
1115
|
+
await validateUITreeDisplay(root, options);
|
|
1116
|
+
debugUITree('capture source=%s attempt=%d durationMs=%d bytes=%d', 'uiautomator', attempt, Date.now() - startedAt, xml.length);
|
|
1117
|
+
return {
|
|
1118
|
+
platform: 'android',
|
|
1119
|
+
capturedAt,
|
|
1120
|
+
root
|
|
1121
|
+
};
|
|
1122
|
+
} catch (error) {
|
|
1123
|
+
const failure = `uiautomator attempt ${attempt}/${ACCESSIBILITY_DUMP_ATTEMPTS}: ${errorMessage(error)}`;
|
|
1124
|
+
failures.push(failure);
|
|
1125
|
+
debugUITree('capture failed source=%s attempt=%d/%d durationMs=%d error=%s', 'uiautomator', attempt, ACCESSIBILITY_DUMP_ATTEMPTS, Date.now() - startedAt, errorMessage(error));
|
|
1126
|
+
if (attempt < ACCESSIBILITY_DUMP_ATTEMPTS) await (0, core_utils_namespaceObject.sleep)(ACCESSIBILITY_DUMP_RETRY_DELAY_MS);
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
throw new Error(`Unable to capture Android UI tree after ${failures.length} attempt(s): ${failures.join('; ')}`);
|
|
1130
|
+
}
|
|
898
1131
|
function device_define_property(obj, key, value) {
|
|
899
1132
|
if (key in obj) Object.defineProperty(obj, key, {
|
|
900
1133
|
value: value,
|
|
@@ -914,6 +1147,35 @@ ${stdout ? truncateAdbShellStream(stdout, 'stdout') : EMPTY_ADB_SHELL_STDOUT}`;
|
|
|
914
1147
|
const warnDevice = (0, logger_.getDebug)('android:device', {
|
|
915
1148
|
console: true
|
|
916
1149
|
});
|
|
1150
|
+
function displayViewportForLogicalDisplay(displayDump, logicalDisplayId) {
|
|
1151
|
+
for (const viewportMatch of displayDump.matchAll(/DisplayViewport\{([^}]*)\}/g)){
|
|
1152
|
+
const viewport = viewportMatch[1];
|
|
1153
|
+
const displayId = viewport.match(/\bdisplayId=(\d+)\b/)?.[1];
|
|
1154
|
+
if (Number(displayId) === logicalDisplayId) return viewportMatch[0];
|
|
1155
|
+
}
|
|
1156
|
+
return null;
|
|
1157
|
+
}
|
|
1158
|
+
function displayInfoLineForPhysicalDisplay(displayDump, physicalDisplayId) {
|
|
1159
|
+
const lineRegex = new RegExp(`^.*uniqueId ["']local:${physicalDisplayId}["'].*$`, 'm');
|
|
1160
|
+
return displayDump.match(lineRegex)?.[0] ?? null;
|
|
1161
|
+
}
|
|
1162
|
+
function physicalDisplayIdForLogicalDisplay(displayDump, logicalDisplayId) {
|
|
1163
|
+
const viewport = displayViewportForLogicalDisplay(displayDump, logicalDisplayId);
|
|
1164
|
+
if (viewport) {
|
|
1165
|
+
const physicalId = viewport.match(/\buniqueId=['"]local:(\d+)['"]/)?.[1];
|
|
1166
|
+
if (physicalId) return physicalId;
|
|
1167
|
+
}
|
|
1168
|
+
const lines = displayDump.split(/\r?\n/);
|
|
1169
|
+
for(let index = 0; index < lines.length; index++){
|
|
1170
|
+
const displayId = lines[index].match(/^\s*mDisplayId=(\d+)\b/)?.[1];
|
|
1171
|
+
if (Number(displayId) === logicalDisplayId) for(let cursor = index + 1; cursor < lines.length; cursor++){
|
|
1172
|
+
if (/^\s*mDisplayId=\d+\b/.test(lines[cursor])) break;
|
|
1173
|
+
const physicalId = lines[cursor].match(/mBaseDisplayInfo=.*\buniqueId "local:(\d+)"/)?.[1];
|
|
1174
|
+
if (physicalId) return physicalId;
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
return null;
|
|
1178
|
+
}
|
|
917
1179
|
function escapeForShell(text) {
|
|
918
1180
|
return text.replace(/'/g, "'\\''").replace(/\n/g, '\\n');
|
|
919
1181
|
}
|
|
@@ -1201,6 +1463,15 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1201
1463
|
children: []
|
|
1202
1464
|
};
|
|
1203
1465
|
}
|
|
1466
|
+
async getUITree() {
|
|
1467
|
+
await this.initializeDevicePixelRatio();
|
|
1468
|
+
return captureAndroidUITree({
|
|
1469
|
+
adb: await this.getAdb(),
|
|
1470
|
+
devicePixelRatio: this.devicePixelRatio,
|
|
1471
|
+
displayId: this.options?.displayId,
|
|
1472
|
+
getDisplaySize: ()=>this.size()
|
|
1473
|
+
});
|
|
1474
|
+
}
|
|
1204
1475
|
async getScreenSize() {
|
|
1205
1476
|
const shouldCache = !(this.options?.alwaysRefreshScreenInfo ?? false);
|
|
1206
1477
|
debugDevice(`getScreenSize: alwaysRefreshScreenInfo=${this.options?.alwaysRefreshScreenInfo}, shouldCache=${shouldCache}, hasCachedSize=${!!this.cachedScreenSize}`);
|
|
@@ -1209,13 +1480,10 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1209
1480
|
if ('number' == typeof this.options?.displayId) try {
|
|
1210
1481
|
const stdout = await adb.shell('dumpsys display');
|
|
1211
1482
|
if (this.options?.usePhysicalDisplayIdForDisplayLookup) {
|
|
1212
|
-
const physicalDisplayId = await this.
|
|
1483
|
+
const physicalDisplayId = await this.resolvePhysicalDisplayId(stdout);
|
|
1213
1484
|
if (physicalDisplayId) {
|
|
1214
|
-
const
|
|
1215
|
-
|
|
1216
|
-
const lineMatch = stdout.match(lineRegex);
|
|
1217
|
-
if (lineMatch) {
|
|
1218
|
-
const targetLine = lineMatch[0];
|
|
1485
|
+
const targetLine = displayInfoLineForPhysicalDisplay(stdout, physicalDisplayId);
|
|
1486
|
+
if (targetLine) {
|
|
1219
1487
|
const realMatch = targetLine.match(/real (\d+) x (\d+)/);
|
|
1220
1488
|
const rotationMatch = targetLine.match(/rotation (\d+)/);
|
|
1221
1489
|
if (realMatch && rotationMatch) {
|
|
@@ -1236,10 +1504,8 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1236
1504
|
}
|
|
1237
1505
|
}
|
|
1238
1506
|
} else {
|
|
1239
|
-
const
|
|
1240
|
-
|
|
1241
|
-
if (match) {
|
|
1242
|
-
const targetLine = match[0];
|
|
1507
|
+
const targetLine = displayViewportForLogicalDisplay(stdout, this.options.displayId);
|
|
1508
|
+
if (targetLine) {
|
|
1243
1509
|
const physicalFrameMatch = targetLine.match(/physicalFrame=Rect\(\d+, \d+ - (\d+), (\d+)\)/);
|
|
1244
1510
|
const orientationMatch = targetLine.match(/orientation=(\d+)/);
|
|
1245
1511
|
if (physicalFrameMatch && orientationMatch) {
|
|
@@ -1305,13 +1571,10 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1305
1571
|
if ('number' == typeof this.options?.displayId) try {
|
|
1306
1572
|
const stdout = await adb.shell('dumpsys display');
|
|
1307
1573
|
if (this.options?.usePhysicalDisplayIdForDisplayLookup) {
|
|
1308
|
-
const physicalDisplayId = await this.
|
|
1574
|
+
const physicalDisplayId = await this.resolvePhysicalDisplayId(stdout);
|
|
1309
1575
|
if (physicalDisplayId) {
|
|
1310
|
-
const
|
|
1311
|
-
|
|
1312
|
-
const lineMatch = stdout.match(lineRegex);
|
|
1313
|
-
if (lineMatch) {
|
|
1314
|
-
const targetLine = lineMatch[0];
|
|
1576
|
+
const targetLine = displayInfoLineForPhysicalDisplay(stdout, physicalDisplayId);
|
|
1577
|
+
if (targetLine) {
|
|
1315
1578
|
const densityMatch = targetLine.match(/density (\d+)/);
|
|
1316
1579
|
if (densityMatch) {
|
|
1317
1580
|
const density = Number(densityMatch[1]);
|
|
@@ -1449,7 +1712,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1449
1712
|
debugDevice('screenshotBase64 end (scrcpy mode)');
|
|
1450
1713
|
return result;
|
|
1451
1714
|
} catch (error) {
|
|
1452
|
-
|
|
1715
|
+
warnDevice(`Scrcpy screenshot failed, falling back to standard ADB method.\nError: ${error}`);
|
|
1453
1716
|
}
|
|
1454
1717
|
const adb = await this.getAdb();
|
|
1455
1718
|
let screenshotBuffer;
|
|
@@ -1936,6 +2199,9 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1936
2199
|
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.`);
|
|
1937
2200
|
}
|
|
1938
2201
|
async getPhysicalDisplayId() {
|
|
2202
|
+
return this.resolvePhysicalDisplayId();
|
|
2203
|
+
}
|
|
2204
|
+
async resolvePhysicalDisplayId(displayDump) {
|
|
1939
2205
|
if (void 0 !== this.cachedPhysicalDisplayId) return this.cachedPhysicalDisplayId;
|
|
1940
2206
|
if ('number' != typeof this.options?.displayId) {
|
|
1941
2207
|
this.cachedPhysicalDisplayId = null;
|
|
@@ -1943,6 +2209,13 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1943
2209
|
}
|
|
1944
2210
|
const adb = await this.getAdb();
|
|
1945
2211
|
try {
|
|
2212
|
+
const resolvedDisplayDump = displayDump ?? await adb.shell('dumpsys display');
|
|
2213
|
+
const logicalDisplayPhysicalId = physicalDisplayIdForLogicalDisplay(resolvedDisplayDump, this.options.displayId);
|
|
2214
|
+
if (logicalDisplayPhysicalId) {
|
|
2215
|
+
this.cachedPhysicalDisplayId = logicalDisplayPhysicalId;
|
|
2216
|
+
debugDevice(`Found and cached physical display ID: ${logicalDisplayPhysicalId} for logical display ID: ${this.options.displayId}`);
|
|
2217
|
+
return this.cachedPhysicalDisplayId;
|
|
2218
|
+
}
|
|
1946
2219
|
const stdout = await adb.shell(`dumpsys SurfaceFlinger --display-id ${this.options.displayId}`);
|
|
1947
2220
|
const regex = new RegExp(`Display (\\d+) \\(HWC display ${this.options.displayId}\\):`);
|
|
1948
2221
|
const displayMatch = stdout.match(regex);
|
|
@@ -2327,7 +2600,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
2327
2600
|
const tools = new AndroidMidsceneTools();
|
|
2328
2601
|
(0, cli_namespaceObject.runToolsCLI)(tools, 'midscene-android', {
|
|
2329
2602
|
stripPrefix: 'android_',
|
|
2330
|
-
version: "1.10.
|
|
2603
|
+
version: "1.10.9-beta-20260731103801.0",
|
|
2331
2604
|
extraCommands: (0, core_namespaceObject.createReportCliCommands)()
|
|
2332
2605
|
}).catch((e)=>{
|
|
2333
2606
|
process.exit((0, cli_namespaceObject.reportCLIError)(e));
|