@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/es/index.mjs
CHANGED
|
@@ -98,20 +98,12 @@ var __webpack_modules__ = {
|
|
|
98
98
|
try {
|
|
99
99
|
this.isConnecting = true;
|
|
100
100
|
debugScrcpy('Starting scrcpy connection...');
|
|
101
|
-
const { AdbScrcpyClient
|
|
101
|
+
const { AdbScrcpyClient } = await import("@yume-chan/adb-scrcpy");
|
|
102
102
|
const { ReadableStream } = await import("@yume-chan/stream-extra");
|
|
103
103
|
const { DefaultServerPath } = await import("@yume-chan/scrcpy");
|
|
104
104
|
const serverBinPath = this.resolveServerBinPath();
|
|
105
105
|
await AdbScrcpyClient.pushServer(this.adb, ReadableStream.from((0, node_fs__rspack_import_0.createReadStream)(serverBinPath)));
|
|
106
|
-
const scrcpyOptions =
|
|
107
|
-
audio: false,
|
|
108
|
-
control: false,
|
|
109
|
-
maxSize: this.options.maxSize,
|
|
110
|
-
videoBitRate: this.options.videoBitRate,
|
|
111
|
-
maxFps: 10,
|
|
112
|
-
sendFrameMeta: true,
|
|
113
|
-
videoCodecOptions: 'i-frame-interval=0,bitrate-mode=2'
|
|
114
|
-
});
|
|
106
|
+
const scrcpyOptions = await this.createScrcpyOptions();
|
|
115
107
|
this.scrcpyClient = await AdbScrcpyClient.start(this.adb, DefaultServerPath, scrcpyOptions);
|
|
116
108
|
serverOutputTask = this.collectServerOutput(this.scrcpyClient.output, serverOutput);
|
|
117
109
|
const videoStreamPromise = this.scrcpyClient.videoStream;
|
|
@@ -139,6 +131,23 @@ var __webpack_modules__ = {
|
|
|
139
131
|
this.isConnecting = false;
|
|
140
132
|
}
|
|
141
133
|
}
|
|
134
|
+
async createScrcpyOptions() {
|
|
135
|
+
const [{ AdbScrcpyOptions3_3_3 }, { ScrcpyInstanceId }] = await Promise.all([
|
|
136
|
+
import("@yume-chan/adb-scrcpy"),
|
|
137
|
+
import("@yume-chan/scrcpy")
|
|
138
|
+
]);
|
|
139
|
+
return new AdbScrcpyOptions3_3_3({
|
|
140
|
+
audio: false,
|
|
141
|
+
control: false,
|
|
142
|
+
tunnelForward: true,
|
|
143
|
+
scid: ScrcpyInstanceId.random(),
|
|
144
|
+
maxSize: this.options.maxSize,
|
|
145
|
+
videoBitRate: this.options.videoBitRate,
|
|
146
|
+
maxFps: 10,
|
|
147
|
+
sendFrameMeta: true,
|
|
148
|
+
videoCodecOptions: 'i-frame-interval=0,bitrate-mode=2'
|
|
149
|
+
});
|
|
150
|
+
}
|
|
142
151
|
async collectServerOutput(output, lines) {
|
|
143
152
|
const reader = output.getReader();
|
|
144
153
|
try {
|
|
@@ -783,6 +792,230 @@ class ScrcpyDeviceAdapter {
|
|
|
783
792
|
this.retryAfter = null;
|
|
784
793
|
}
|
|
785
794
|
}
|
|
795
|
+
const TAG_RE = /<(\/?)([A-Za-z_][A-Za-z0-9_.\-:]*)([^>]*?)(\/?)>/g;
|
|
796
|
+
const ATTR_RE = /([A-Za-z_][A-Za-z0-9_.\-:]*)\s*=\s*("([^"]*)"|'([^']*)')/g;
|
|
797
|
+
const BOUNDS_RE = /^\[(-?\d+),(-?\d+)\]\[(-?\d+),(-?\d+)\]$/;
|
|
798
|
+
const ENTITY_TABLE = {
|
|
799
|
+
amp: '&',
|
|
800
|
+
lt: '<',
|
|
801
|
+
gt: '>',
|
|
802
|
+
quot: '"',
|
|
803
|
+
apos: "'"
|
|
804
|
+
};
|
|
805
|
+
function decodeEntities(value) {
|
|
806
|
+
if (!value.includes('&')) return value;
|
|
807
|
+
return value.replace(/&(#x?[0-9a-fA-F]+|[a-zA-Z]+);/g, (full, body)=>{
|
|
808
|
+
if (body.startsWith('#x') || body.startsWith('#X')) {
|
|
809
|
+
const code = Number.parseInt(body.slice(2), 16);
|
|
810
|
+
return Number.isFinite(code) ? String.fromCodePoint(code) : full;
|
|
811
|
+
}
|
|
812
|
+
if (body.startsWith('#')) {
|
|
813
|
+
const code = Number.parseInt(body.slice(1), 10);
|
|
814
|
+
return Number.isFinite(code) ? String.fromCodePoint(code) : full;
|
|
815
|
+
}
|
|
816
|
+
return ENTITY_TABLE[body] ?? full;
|
|
817
|
+
});
|
|
818
|
+
}
|
|
819
|
+
function parseAttrs(raw) {
|
|
820
|
+
const attrs = {};
|
|
821
|
+
ATTR_RE.lastIndex = 0;
|
|
822
|
+
let match;
|
|
823
|
+
while(match = ATTR_RE.exec(raw))attrs[match[1]] = decodeEntities(match[3] ?? match[4] ?? '');
|
|
824
|
+
return attrs;
|
|
825
|
+
}
|
|
826
|
+
function parseXml(input) {
|
|
827
|
+
if (0 === input.trim().length) throw new Error('parseAndroidUITreeXml: input is empty');
|
|
828
|
+
const cleaned = input.replace(/<\?xml[\s\S]*?\?>/g, '').replace(/<!DOCTYPE[\s\S]*?>/gi, '').replace(/<!--[\s\S]*?-->/g, '');
|
|
829
|
+
TAG_RE.lastIndex = 0;
|
|
830
|
+
const stack = [];
|
|
831
|
+
let root;
|
|
832
|
+
let match;
|
|
833
|
+
while(match = TAG_RE.exec(cleaned)){
|
|
834
|
+
const isClose = '/' === match[1];
|
|
835
|
+
const name = match[2];
|
|
836
|
+
if (isClose) {
|
|
837
|
+
const current = stack.pop();
|
|
838
|
+
if (!current || current.name !== name) throw new Error(`parseAndroidUITreeXml: unbalanced close tag </${name}>`);
|
|
839
|
+
continue;
|
|
840
|
+
}
|
|
841
|
+
const element = {
|
|
842
|
+
name,
|
|
843
|
+
attrs: parseAttrs(match[3] ?? ''),
|
|
844
|
+
children: []
|
|
845
|
+
};
|
|
846
|
+
const parent = stack[stack.length - 1];
|
|
847
|
+
if (parent) parent.children.push(element);
|
|
848
|
+
else if (root) throw new Error('parseAndroidUITreeXml: more than one top-level element');
|
|
849
|
+
else root = element;
|
|
850
|
+
if ('/' !== match[4]) stack.push(element);
|
|
851
|
+
}
|
|
852
|
+
if (stack.length > 0) throw new Error('parseAndroidUITreeXml: unclosed element');
|
|
853
|
+
if (!root) throw new Error('parseAndroidUITreeXml: no root element');
|
|
854
|
+
return root;
|
|
855
|
+
}
|
|
856
|
+
function parseBounds(raw, devicePixelRatio) {
|
|
857
|
+
const match = raw ? BOUNDS_RE.exec(raw) : void 0;
|
|
858
|
+
if (!match) return {
|
|
859
|
+
left: 0,
|
|
860
|
+
top: 0,
|
|
861
|
+
width: 0,
|
|
862
|
+
height: 0
|
|
863
|
+
};
|
|
864
|
+
const left = Number.parseInt(match[1], 10);
|
|
865
|
+
const top = Number.parseInt(match[2], 10);
|
|
866
|
+
const right = Number.parseInt(match[3], 10);
|
|
867
|
+
const bottom = Number.parseInt(match[4], 10);
|
|
868
|
+
const ratio = devicePixelRatio > 0 ? devicePixelRatio : 1;
|
|
869
|
+
return {
|
|
870
|
+
left: left / ratio,
|
|
871
|
+
top: top / ratio,
|
|
872
|
+
width: Math.max(0, right - left) / ratio,
|
|
873
|
+
height: Math.max(0, bottom - top) / ratio
|
|
874
|
+
};
|
|
875
|
+
}
|
|
876
|
+
function toUiNode(element, devicePixelRatio) {
|
|
877
|
+
const attrs = {};
|
|
878
|
+
for (const [name, value] of Object.entries(element.attrs))if ('bounds' !== name) attrs[name] = value;
|
|
879
|
+
return {
|
|
880
|
+
type: element.attrs.class || element.name,
|
|
881
|
+
attrs,
|
|
882
|
+
bounds: parseBounds(element.attrs.bounds, devicePixelRatio),
|
|
883
|
+
children: element.children.map((child)=>toUiNode(child, devicePixelRatio))
|
|
884
|
+
};
|
|
885
|
+
}
|
|
886
|
+
function uiautomatorXmlToUiNode(xml, devicePixelRatio) {
|
|
887
|
+
const root = parseXml(xml);
|
|
888
|
+
if ('hierarchy' === root.name && 1 === root.children.length) return toUiNode(root.children[0], devicePixelRatio);
|
|
889
|
+
return toUiNode(root, devicePixelRatio);
|
|
890
|
+
}
|
|
891
|
+
const ACCESSIBILITY_DUMP_ATTEMPTS = 3;
|
|
892
|
+
const ACCESSIBILITY_DUMP_TIMEOUT_MS = 5000;
|
|
893
|
+
const ACCESSIBILITY_DUMP_RETRY_DELAY_MS = 250;
|
|
894
|
+
const UIAUTOMATOR_LAYOUT_PATH = '/sdcard/midscene_window_dump.xml';
|
|
895
|
+
const debugUITree = (0, logger_.getDebug)('android:ui-tree');
|
|
896
|
+
function errorMessage(error) {
|
|
897
|
+
return error instanceof Error ? error.message : String(error);
|
|
898
|
+
}
|
|
899
|
+
function validateHierarchyXml(xml) {
|
|
900
|
+
if ('string' != typeof xml || !/<hierarchy(?:\s|>)/.test(xml) || !xml.includes('</hierarchy>')) throw new Error('uiautomator did not produce valid hierarchy XML');
|
|
901
|
+
}
|
|
902
|
+
function focusedPackageForLogicalDisplay(windowDump, logicalDisplayId) {
|
|
903
|
+
const displayMatches = [
|
|
904
|
+
...windowDump.matchAll(/^\s*Display: mDisplayId=(\d+)\b.*$/gm)
|
|
905
|
+
];
|
|
906
|
+
const displayIndex = displayMatches.findIndex((match)=>Number(match[1]) === logicalDisplayId);
|
|
907
|
+
if (-1 === displayIndex) return null;
|
|
908
|
+
const start = displayMatches[displayIndex].index ?? 0;
|
|
909
|
+
const end = displayMatches[displayIndex + 1]?.index ?? windowDump.length;
|
|
910
|
+
const displayBlock = windowDump.slice(start, end);
|
|
911
|
+
for (const field of [
|
|
912
|
+
'mCurrentFocus',
|
|
913
|
+
'mFocusedApp'
|
|
914
|
+
]){
|
|
915
|
+
const value = displayBlock.match(new RegExp(`^\\s*${field}=([^\\n]*)$`, 'm'))?.[1];
|
|
916
|
+
const packageName = value?.match(/\bu\d+\s+([A-Za-z0-9_$.-]+)\//)?.[1];
|
|
917
|
+
if (packageName) return packageName;
|
|
918
|
+
}
|
|
919
|
+
return null;
|
|
920
|
+
}
|
|
921
|
+
function firstTreePackage(root) {
|
|
922
|
+
if (root.attrs.package) return root.attrs.package;
|
|
923
|
+
for (const child of root.children){
|
|
924
|
+
const packageName = firstTreePackage(child);
|
|
925
|
+
if (packageName) return packageName;
|
|
926
|
+
}
|
|
927
|
+
return null;
|
|
928
|
+
}
|
|
929
|
+
function windowDisplayIdsForPackage(windowDump, packageName) {
|
|
930
|
+
const windowMatches = [
|
|
931
|
+
...windowDump.matchAll(/^\s*Window #\d+ Window\{.*$/gm)
|
|
932
|
+
];
|
|
933
|
+
const displayIds = new Set();
|
|
934
|
+
for(let index = 0; index < windowMatches.length; index++){
|
|
935
|
+
const start = windowMatches[index].index ?? 0;
|
|
936
|
+
const end = windowMatches[index + 1]?.index ?? windowDump.length;
|
|
937
|
+
const windowBlock = windowDump.slice(start, end);
|
|
938
|
+
const ownerPackage = windowBlock.match(/^\s*mOwnerUid=.*\bpackage=([A-Za-z0-9_$.-]+)\b/m)?.[1];
|
|
939
|
+
const headerPackage = windowMatches[index][0].match(/\bu\d+\s+([A-Za-z0-9_$.-]+)\//)?.[1];
|
|
940
|
+
if (ownerPackage !== packageName && headerPackage !== packageName) continue;
|
|
941
|
+
const displayId = windowBlock.match(/^\s*mDisplayId=(\d+)\b/m)?.[1];
|
|
942
|
+
if (void 0 !== displayId) displayIds.add(Number(displayId));
|
|
943
|
+
}
|
|
944
|
+
return [
|
|
945
|
+
...displayIds
|
|
946
|
+
];
|
|
947
|
+
}
|
|
948
|
+
function uiTreeExtent(root) {
|
|
949
|
+
let right = root.bounds.left + root.bounds.width;
|
|
950
|
+
let bottom = root.bounds.top + root.bounds.height;
|
|
951
|
+
for (const child of root.children){
|
|
952
|
+
const childExtent = uiTreeExtent(child);
|
|
953
|
+
right = Math.max(right, childExtent.right);
|
|
954
|
+
bottom = Math.max(bottom, childExtent.bottom);
|
|
955
|
+
}
|
|
956
|
+
return {
|
|
957
|
+
right,
|
|
958
|
+
bottom
|
|
959
|
+
};
|
|
960
|
+
}
|
|
961
|
+
async function dumpAccessibilityXml(adb) {
|
|
962
|
+
await runAdbShellStdoutOrThrow(adb, `rm -f ${UIAUTOMATOR_LAYOUT_PATH}`, {
|
|
963
|
+
timeout: ACCESSIBILITY_DUMP_TIMEOUT_MS
|
|
964
|
+
});
|
|
965
|
+
await runAdbShellStdoutOrThrow(adb, `uiautomator dump --compressed ${UIAUTOMATOR_LAYOUT_PATH}`, {
|
|
966
|
+
timeout: ACCESSIBILITY_DUMP_TIMEOUT_MS
|
|
967
|
+
});
|
|
968
|
+
const xml = await runAdbShellStdoutOrThrow(adb, `cat ${UIAUTOMATOR_LAYOUT_PATH}`, {
|
|
969
|
+
timeout: ACCESSIBILITY_DUMP_TIMEOUT_MS
|
|
970
|
+
});
|
|
971
|
+
validateHierarchyXml(xml);
|
|
972
|
+
return xml;
|
|
973
|
+
}
|
|
974
|
+
async function validateUITreeDisplay(root, options) {
|
|
975
|
+
if ('number' != typeof options.displayId) return;
|
|
976
|
+
const { adb, displayId } = options;
|
|
977
|
+
const displayWindowDump = await runAdbShellStdoutOrThrow(adb, 'dumpsys window displays', {
|
|
978
|
+
timeout: ACCESSIBILITY_DUMP_TIMEOUT_MS
|
|
979
|
+
});
|
|
980
|
+
const expectedPackage = focusedPackageForLogicalDisplay(displayWindowDump, displayId);
|
|
981
|
+
const treePackage = firstTreePackage(root);
|
|
982
|
+
if (!expectedPackage) throw new Error(`Unable to verify Android UI tree display alignment for displayId=${displayId}: no focused package found on the target display`);
|
|
983
|
+
if (!treePackage) throw new Error(`Unable to verify Android UI tree display alignment for displayId=${displayId}: accessibility hierarchy has no package`);
|
|
984
|
+
const allWindowDump = await runAdbShellStdoutOrThrow(adb, 'dumpsys window windows', {
|
|
985
|
+
timeout: ACCESSIBILITY_DUMP_TIMEOUT_MS
|
|
986
|
+
});
|
|
987
|
+
const treePackageDisplayIds = windowDisplayIdsForPackage(allWindowDump, treePackage);
|
|
988
|
+
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}`);
|
|
989
|
+
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(',')}`);
|
|
990
|
+
const targetSize = await options.getDisplaySize();
|
|
991
|
+
const extent = uiTreeExtent(root);
|
|
992
|
+
const tolerance = 1;
|
|
993
|
+
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}`);
|
|
994
|
+
}
|
|
995
|
+
async function captureAndroidUITree(options) {
|
|
996
|
+
const failures = [];
|
|
997
|
+
for(let attempt = 1; attempt <= ACCESSIBILITY_DUMP_ATTEMPTS; attempt++){
|
|
998
|
+
const startedAt = Date.now();
|
|
999
|
+
try {
|
|
1000
|
+
const xml = await dumpAccessibilityXml(options.adb);
|
|
1001
|
+
const capturedAt = Date.now();
|
|
1002
|
+
const root = uiautomatorXmlToUiNode(xml, options.devicePixelRatio);
|
|
1003
|
+
await validateUITreeDisplay(root, options);
|
|
1004
|
+
debugUITree('capture source=%s attempt=%d durationMs=%d bytes=%d', 'uiautomator', attempt, Date.now() - startedAt, xml.length);
|
|
1005
|
+
return {
|
|
1006
|
+
platform: 'android',
|
|
1007
|
+
capturedAt,
|
|
1008
|
+
root
|
|
1009
|
+
};
|
|
1010
|
+
} catch (error) {
|
|
1011
|
+
const failure = `uiautomator attempt ${attempt}/${ACCESSIBILITY_DUMP_ATTEMPTS}: ${errorMessage(error)}`;
|
|
1012
|
+
failures.push(failure);
|
|
1013
|
+
debugUITree('capture failed source=%s attempt=%d/%d durationMs=%d error=%s', 'uiautomator', attempt, ACCESSIBILITY_DUMP_ATTEMPTS, Date.now() - startedAt, errorMessage(error));
|
|
1014
|
+
if (attempt < ACCESSIBILITY_DUMP_ATTEMPTS) await sleep(ACCESSIBILITY_DUMP_RETRY_DELAY_MS);
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
throw new Error(`Unable to capture Android UI tree after ${failures.length} attempt(s): ${failures.join('; ')}`);
|
|
1018
|
+
}
|
|
786
1019
|
function device_define_property(obj, key, value) {
|
|
787
1020
|
if (key in obj) Object.defineProperty(obj, key, {
|
|
788
1021
|
value: value,
|
|
@@ -802,6 +1035,35 @@ const debugDevice = (0, logger_.getDebug)('android:device');
|
|
|
802
1035
|
const warnDevice = (0, logger_.getDebug)('android:device', {
|
|
803
1036
|
console: true
|
|
804
1037
|
});
|
|
1038
|
+
function displayViewportForLogicalDisplay(displayDump, logicalDisplayId) {
|
|
1039
|
+
for (const viewportMatch of displayDump.matchAll(/DisplayViewport\{([^}]*)\}/g)){
|
|
1040
|
+
const viewport = viewportMatch[1];
|
|
1041
|
+
const displayId = viewport.match(/\bdisplayId=(\d+)\b/)?.[1];
|
|
1042
|
+
if (Number(displayId) === logicalDisplayId) return viewportMatch[0];
|
|
1043
|
+
}
|
|
1044
|
+
return null;
|
|
1045
|
+
}
|
|
1046
|
+
function displayInfoLineForPhysicalDisplay(displayDump, physicalDisplayId) {
|
|
1047
|
+
const lineRegex = new RegExp(`^.*uniqueId ["']local:${physicalDisplayId}["'].*$`, 'm');
|
|
1048
|
+
return displayDump.match(lineRegex)?.[0] ?? null;
|
|
1049
|
+
}
|
|
1050
|
+
function physicalDisplayIdForLogicalDisplay(displayDump, logicalDisplayId) {
|
|
1051
|
+
const viewport = displayViewportForLogicalDisplay(displayDump, logicalDisplayId);
|
|
1052
|
+
if (viewport) {
|
|
1053
|
+
const physicalId = viewport.match(/\buniqueId=['"]local:(\d+)['"]/)?.[1];
|
|
1054
|
+
if (physicalId) return physicalId;
|
|
1055
|
+
}
|
|
1056
|
+
const lines = displayDump.split(/\r?\n/);
|
|
1057
|
+
for(let index = 0; index < lines.length; index++){
|
|
1058
|
+
const displayId = lines[index].match(/^\s*mDisplayId=(\d+)\b/)?.[1];
|
|
1059
|
+
if (Number(displayId) === logicalDisplayId) for(let cursor = index + 1; cursor < lines.length; cursor++){
|
|
1060
|
+
if (/^\s*mDisplayId=\d+\b/.test(lines[cursor])) break;
|
|
1061
|
+
const physicalId = lines[cursor].match(/mBaseDisplayInfo=.*\buniqueId "local:(\d+)"/)?.[1];
|
|
1062
|
+
if (physicalId) return physicalId;
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
return null;
|
|
1066
|
+
}
|
|
805
1067
|
function escapeForShell(text) {
|
|
806
1068
|
return text.replace(/'/g, "'\\''").replace(/\n/g, '\\n');
|
|
807
1069
|
}
|
|
@@ -1089,6 +1351,15 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1089
1351
|
children: []
|
|
1090
1352
|
};
|
|
1091
1353
|
}
|
|
1354
|
+
async getUITree() {
|
|
1355
|
+
await this.initializeDevicePixelRatio();
|
|
1356
|
+
return captureAndroidUITree({
|
|
1357
|
+
adb: await this.getAdb(),
|
|
1358
|
+
devicePixelRatio: this.devicePixelRatio,
|
|
1359
|
+
displayId: this.options?.displayId,
|
|
1360
|
+
getDisplaySize: ()=>this.size()
|
|
1361
|
+
});
|
|
1362
|
+
}
|
|
1092
1363
|
async getScreenSize() {
|
|
1093
1364
|
const shouldCache = !(this.options?.alwaysRefreshScreenInfo ?? false);
|
|
1094
1365
|
debugDevice(`getScreenSize: alwaysRefreshScreenInfo=${this.options?.alwaysRefreshScreenInfo}, shouldCache=${shouldCache}, hasCachedSize=${!!this.cachedScreenSize}`);
|
|
@@ -1097,13 +1368,10 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1097
1368
|
if ('number' == typeof this.options?.displayId) try {
|
|
1098
1369
|
const stdout = await adb.shell('dumpsys display');
|
|
1099
1370
|
if (this.options?.usePhysicalDisplayIdForDisplayLookup) {
|
|
1100
|
-
const physicalDisplayId = await this.
|
|
1371
|
+
const physicalDisplayId = await this.resolvePhysicalDisplayId(stdout);
|
|
1101
1372
|
if (physicalDisplayId) {
|
|
1102
|
-
const
|
|
1103
|
-
|
|
1104
|
-
const lineMatch = stdout.match(lineRegex);
|
|
1105
|
-
if (lineMatch) {
|
|
1106
|
-
const targetLine = lineMatch[0];
|
|
1373
|
+
const targetLine = displayInfoLineForPhysicalDisplay(stdout, physicalDisplayId);
|
|
1374
|
+
if (targetLine) {
|
|
1107
1375
|
const realMatch = targetLine.match(/real (\d+) x (\d+)/);
|
|
1108
1376
|
const rotationMatch = targetLine.match(/rotation (\d+)/);
|
|
1109
1377
|
if (realMatch && rotationMatch) {
|
|
@@ -1124,10 +1392,8 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1124
1392
|
}
|
|
1125
1393
|
}
|
|
1126
1394
|
} else {
|
|
1127
|
-
const
|
|
1128
|
-
|
|
1129
|
-
if (match) {
|
|
1130
|
-
const targetLine = match[0];
|
|
1395
|
+
const targetLine = displayViewportForLogicalDisplay(stdout, this.options.displayId);
|
|
1396
|
+
if (targetLine) {
|
|
1131
1397
|
const physicalFrameMatch = targetLine.match(/physicalFrame=Rect\(\d+, \d+ - (\d+), (\d+)\)/);
|
|
1132
1398
|
const orientationMatch = targetLine.match(/orientation=(\d+)/);
|
|
1133
1399
|
if (physicalFrameMatch && orientationMatch) {
|
|
@@ -1193,13 +1459,10 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1193
1459
|
if ('number' == typeof this.options?.displayId) try {
|
|
1194
1460
|
const stdout = await adb.shell('dumpsys display');
|
|
1195
1461
|
if (this.options?.usePhysicalDisplayIdForDisplayLookup) {
|
|
1196
|
-
const physicalDisplayId = await this.
|
|
1462
|
+
const physicalDisplayId = await this.resolvePhysicalDisplayId(stdout);
|
|
1197
1463
|
if (physicalDisplayId) {
|
|
1198
|
-
const
|
|
1199
|
-
|
|
1200
|
-
const lineMatch = stdout.match(lineRegex);
|
|
1201
|
-
if (lineMatch) {
|
|
1202
|
-
const targetLine = lineMatch[0];
|
|
1464
|
+
const targetLine = displayInfoLineForPhysicalDisplay(stdout, physicalDisplayId);
|
|
1465
|
+
if (targetLine) {
|
|
1203
1466
|
const densityMatch = targetLine.match(/density (\d+)/);
|
|
1204
1467
|
if (densityMatch) {
|
|
1205
1468
|
const density = Number(densityMatch[1]);
|
|
@@ -1337,7 +1600,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1337
1600
|
debugDevice('screenshotBase64 end (scrcpy mode)');
|
|
1338
1601
|
return result;
|
|
1339
1602
|
} catch (error) {
|
|
1340
|
-
|
|
1603
|
+
warnDevice(`Scrcpy screenshot failed, falling back to standard ADB method.\nError: ${error}`);
|
|
1341
1604
|
}
|
|
1342
1605
|
const adb = await this.getAdb();
|
|
1343
1606
|
let screenshotBuffer;
|
|
@@ -1824,6 +2087,9 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1824
2087
|
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.`);
|
|
1825
2088
|
}
|
|
1826
2089
|
async getPhysicalDisplayId() {
|
|
2090
|
+
return this.resolvePhysicalDisplayId();
|
|
2091
|
+
}
|
|
2092
|
+
async resolvePhysicalDisplayId(displayDump) {
|
|
1827
2093
|
if (void 0 !== this.cachedPhysicalDisplayId) return this.cachedPhysicalDisplayId;
|
|
1828
2094
|
if ('number' != typeof this.options?.displayId) {
|
|
1829
2095
|
this.cachedPhysicalDisplayId = null;
|
|
@@ -1831,6 +2097,13 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1831
2097
|
}
|
|
1832
2098
|
const adb = await this.getAdb();
|
|
1833
2099
|
try {
|
|
2100
|
+
const resolvedDisplayDump = displayDump ?? await adb.shell('dumpsys display');
|
|
2101
|
+
const logicalDisplayPhysicalId = physicalDisplayIdForLogicalDisplay(resolvedDisplayDump, this.options.displayId);
|
|
2102
|
+
if (logicalDisplayPhysicalId) {
|
|
2103
|
+
this.cachedPhysicalDisplayId = logicalDisplayPhysicalId;
|
|
2104
|
+
debugDevice(`Found and cached physical display ID: ${logicalDisplayPhysicalId} for logical display ID: ${this.options.displayId}`);
|
|
2105
|
+
return this.cachedPhysicalDisplayId;
|
|
2106
|
+
}
|
|
1834
2107
|
const stdout = await adb.shell(`dumpsys SurfaceFlinger --display-id ${this.options.displayId}`);
|
|
1835
2108
|
const regex = new RegExp(`Display (\\d+) \\(HWC display ${this.options.displayId}\\):`);
|
|
1836
2109
|
const displayMatch = stdout.match(regex);
|