@midscene/android 0.14.2 → 0.14.3-beta-20250409031306.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/bin/playground +3 -0
- package/dist/es/index.js +4 -0
- package/dist/es/playground.d.ts +2 -0
- package/dist/es/playground.js +852 -0
- package/dist/lib/index.js +4 -0
- package/dist/lib/playground.d.ts +2 -0
- package/dist/lib/playground.js +872 -0
- package/dist/types/playground.d.ts +2 -0
- package/package.json +25 -4
- package/static/index.html +1 -0
- package/static/scripts/htmlElement.js +5 -0
- package/static/scripts/htmlElement.js.LICENSE.txt +12 -0
- package/static/scripts/htmlElementDebug.js +2 -0
- package/static/scripts/htmlElementDebug.js.LICENSE.txt +12 -0
- package/static/scripts/stop-water-flow.js +2 -0
- package/static/scripts/stop-water-flow.js.map +1 -0
- package/static/scripts/water-flow.js +54 -0
- package/static/scripts/water-flow.js.map +1 -0
- package/static/static/css/index.fa8491ca.css +2 -0
- package/static/static/css/index.fa8491ca.css.map +1 -0
- package/static/static/js/792.8ac93205.js +467 -0
- package/static/static/js/792.8ac93205.js.LICENSE.txt +1673 -0
- package/static/static/js/792.8ac93205.js.map +1 -0
- package/static/static/js/async/166.208adb78.js +2 -0
- package/static/static/js/async/166.208adb78.js.map +1 -0
- package/static/static/js/index.906d88a4.js +504 -0
- package/static/static/js/index.906d88a4.js.map +1 -0
- package/static/static/js/lib-polyfill.c3257577.js +2 -0
- package/static/static/js/lib-polyfill.c3257577.js.map +1 -0
- package/static/static/js/lib-react.bac2292c.js +3 -0
- package/static/static/js/lib-react.bac2292c.js.LICENSE.txt +39 -0
- package/static/static/js/lib-react.bac2292c.js.map +1 -0
- package/LICENSE +0 -21
|
@@ -0,0 +1,872 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __copyProps = (to, from, except, desc) => {
|
|
9
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
10
|
+
for (let key of __getOwnPropNames(from))
|
|
11
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
12
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
13
|
+
}
|
|
14
|
+
return to;
|
|
15
|
+
};
|
|
16
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
17
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
18
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
19
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
20
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
21
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
22
|
+
mod
|
|
23
|
+
));
|
|
24
|
+
|
|
25
|
+
// src/playground/bin.ts
|
|
26
|
+
var import_node_path2 = __toESM(require("path"));
|
|
27
|
+
var import_midscene_server = __toESM(require("@midscene/web/midscene-server"));
|
|
28
|
+
var import_open = __toESM(require("open"));
|
|
29
|
+
|
|
30
|
+
// src/page/index.ts
|
|
31
|
+
var import_node_assert = __toESM(require("assert"));
|
|
32
|
+
var import_node_fs = __toESM(require("fs"));
|
|
33
|
+
var import_node_path = __toESM(require("path"));
|
|
34
|
+
var import_utils = require("@midscene/core/utils");
|
|
35
|
+
var import_img = require("@midscene/shared/img");
|
|
36
|
+
var import_logger = require("@midscene/shared/logger");
|
|
37
|
+
var import_appium_adb = require("appium-adb");
|
|
38
|
+
var androidScreenshotPath = "/data/local/tmp/midscene_screenshot.png";
|
|
39
|
+
var debugPage = (0, import_logger.getDebug)("android:device");
|
|
40
|
+
var AndroidDevice = class {
|
|
41
|
+
constructor(deviceId) {
|
|
42
|
+
this.screenSize = null;
|
|
43
|
+
this.yadbPushed = false;
|
|
44
|
+
this.deviceRatio = 1;
|
|
45
|
+
this.adb = null;
|
|
46
|
+
this.connectingAdb = null;
|
|
47
|
+
this.pageType = "android";
|
|
48
|
+
(0, import_node_assert.default)(deviceId, "deviceId is required for AndroidDevice");
|
|
49
|
+
this.deviceId = deviceId;
|
|
50
|
+
}
|
|
51
|
+
async connect() {
|
|
52
|
+
return this.getAdb();
|
|
53
|
+
}
|
|
54
|
+
async getAdb() {
|
|
55
|
+
if (this.adb) {
|
|
56
|
+
return this.adb;
|
|
57
|
+
}
|
|
58
|
+
if (this.connectingAdb) {
|
|
59
|
+
return this.connectingAdb;
|
|
60
|
+
}
|
|
61
|
+
this.connectingAdb = (async () => {
|
|
62
|
+
let error = null;
|
|
63
|
+
debugPage(`Initializing ADB with device ID: ${this.deviceId}`);
|
|
64
|
+
try {
|
|
65
|
+
this.adb = await import_appium_adb.ADB.createADB({
|
|
66
|
+
udid: this.deviceId,
|
|
67
|
+
adbExecTimeout: 6e4
|
|
68
|
+
});
|
|
69
|
+
debugPage("ADB initialized successfully");
|
|
70
|
+
return this.adb;
|
|
71
|
+
} catch (e) {
|
|
72
|
+
debugPage(`Failed to initialize ADB: ${e}`);
|
|
73
|
+
error = new Error(`Unable to connect to device ${this.deviceId}: ${e}`);
|
|
74
|
+
} finally {
|
|
75
|
+
this.connectingAdb = null;
|
|
76
|
+
}
|
|
77
|
+
if (error) {
|
|
78
|
+
throw error;
|
|
79
|
+
}
|
|
80
|
+
throw new Error("ADB initialization failed unexpectedly");
|
|
81
|
+
})();
|
|
82
|
+
return this.connectingAdb;
|
|
83
|
+
}
|
|
84
|
+
async launch(uri) {
|
|
85
|
+
const adb = await this.getAdb();
|
|
86
|
+
try {
|
|
87
|
+
if (uri.startsWith("http://") || uri.startsWith("https://") || uri.includes("://")) {
|
|
88
|
+
await adb.startUri(uri);
|
|
89
|
+
} else if (uri.includes("/")) {
|
|
90
|
+
const [appPackage, appActivity] = uri.split("/");
|
|
91
|
+
await adb.startApp({
|
|
92
|
+
pkg: appPackage,
|
|
93
|
+
activity: appActivity
|
|
94
|
+
});
|
|
95
|
+
} else {
|
|
96
|
+
await adb.startApp({
|
|
97
|
+
pkg: uri
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
debugPage(`Successfully launched: ${uri}`);
|
|
101
|
+
} catch (error) {
|
|
102
|
+
debugPage(`Error launching ${uri}: ${error}`);
|
|
103
|
+
throw new Error(`Failed to launch ${uri}: ${error}`, { cause: error });
|
|
104
|
+
}
|
|
105
|
+
return this;
|
|
106
|
+
}
|
|
107
|
+
async execYadb(keyboardContent) {
|
|
108
|
+
await this.ensureYadb();
|
|
109
|
+
const adb = await this.getAdb();
|
|
110
|
+
await adb.shell(
|
|
111
|
+
`app_process -Djava.class.path=/data/local/tmp/yadb /data/local/tmp com.ysbing.yadb.Main -keyboard "${keyboardContent}"`
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
async getElementsInfo() {
|
|
115
|
+
return [];
|
|
116
|
+
}
|
|
117
|
+
async getElementsNodeTree() {
|
|
118
|
+
return {
|
|
119
|
+
node: null,
|
|
120
|
+
children: []
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
async size() {
|
|
124
|
+
if (this.screenSize) {
|
|
125
|
+
return this.screenSize;
|
|
126
|
+
}
|
|
127
|
+
const adb = await this.getAdb();
|
|
128
|
+
const screenSize = await adb.getScreenSize();
|
|
129
|
+
let width;
|
|
130
|
+
let height;
|
|
131
|
+
if (typeof screenSize === "string") {
|
|
132
|
+
const match = screenSize.match(/(\d+)x(\d+)/);
|
|
133
|
+
if (!match || match.length < 3) {
|
|
134
|
+
throw new Error(`Unable to parse screen size: ${screenSize}`);
|
|
135
|
+
}
|
|
136
|
+
width = Number.parseInt(match[1], 10);
|
|
137
|
+
height = Number.parseInt(match[2], 10);
|
|
138
|
+
} else if (typeof screenSize === "object" && screenSize !== null) {
|
|
139
|
+
const sizeObj = screenSize;
|
|
140
|
+
if ("width" in sizeObj && "height" in sizeObj) {
|
|
141
|
+
width = Number(sizeObj.width);
|
|
142
|
+
height = Number(sizeObj.height);
|
|
143
|
+
} else {
|
|
144
|
+
throw new Error(
|
|
145
|
+
`Invalid screen size object: ${JSON.stringify(screenSize)}`
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
} else {
|
|
149
|
+
throw new Error(`Invalid screen size format: ${screenSize}`);
|
|
150
|
+
}
|
|
151
|
+
const densityNum = await adb.getScreenDensity();
|
|
152
|
+
this.deviceRatio = Number(densityNum) / 160;
|
|
153
|
+
const { x: logicalWidth, y: logicalHeight } = this.reverseAdjustCoordinates(
|
|
154
|
+
width,
|
|
155
|
+
height
|
|
156
|
+
);
|
|
157
|
+
this.screenSize = {
|
|
158
|
+
width: logicalWidth,
|
|
159
|
+
height: logicalHeight
|
|
160
|
+
};
|
|
161
|
+
return this.screenSize;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Convert logical coordinates to physical coordinates, handling device ratio
|
|
165
|
+
* @param x Logical X coordinate
|
|
166
|
+
* @param y Logical Y coordinate
|
|
167
|
+
* @returns Physical coordinate point
|
|
168
|
+
*/
|
|
169
|
+
adjustCoordinates(x, y) {
|
|
170
|
+
const ratio = this.deviceRatio;
|
|
171
|
+
return {
|
|
172
|
+
x: Math.round(x * ratio),
|
|
173
|
+
y: Math.round(y * ratio)
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Convert physical coordinates to logical coordinates, handling device ratio
|
|
178
|
+
* @param x Physical X coordinate
|
|
179
|
+
* @param y Physical Y coordinate
|
|
180
|
+
* @returns Logical coordinate point
|
|
181
|
+
*/
|
|
182
|
+
reverseAdjustCoordinates(x, y) {
|
|
183
|
+
const ratio = this.deviceRatio;
|
|
184
|
+
return {
|
|
185
|
+
x: Math.round(x / ratio),
|
|
186
|
+
y: Math.round(y / ratio)
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
async screenshotBase64() {
|
|
190
|
+
debugPage("screenshotBase64 begin");
|
|
191
|
+
const { width, height } = await this.size();
|
|
192
|
+
const adb = await this.getAdb();
|
|
193
|
+
let screenshotBuffer;
|
|
194
|
+
try {
|
|
195
|
+
screenshotBuffer = await adb.takeScreenshot(null);
|
|
196
|
+
} catch (error) {
|
|
197
|
+
const screenshotPath = (0, import_utils.getTmpFile)("png");
|
|
198
|
+
try {
|
|
199
|
+
await adb.shell(`screencap -p ${androidScreenshotPath}`);
|
|
200
|
+
} catch (error2) {
|
|
201
|
+
await this.forceScreenshot(androidScreenshotPath);
|
|
202
|
+
}
|
|
203
|
+
await adb.pull(androidScreenshotPath, screenshotPath);
|
|
204
|
+
screenshotBuffer = await import_node_fs.default.promises.readFile(screenshotPath);
|
|
205
|
+
}
|
|
206
|
+
const resizedScreenshotBuffer = await (0, import_img.resizeImg)(screenshotBuffer, {
|
|
207
|
+
width,
|
|
208
|
+
height
|
|
209
|
+
});
|
|
210
|
+
const result = `data:image/jpeg;base64,${resizedScreenshotBuffer.toString("base64")}`;
|
|
211
|
+
debugPage("screenshotBase64 end");
|
|
212
|
+
return result;
|
|
213
|
+
}
|
|
214
|
+
get mouse() {
|
|
215
|
+
return {
|
|
216
|
+
click: (x, y) => this.mouseClick(x, y),
|
|
217
|
+
wheel: (deltaX, deltaY) => this.mouseWheel(deltaX, deltaY),
|
|
218
|
+
move: (x, y) => this.mouseMove(x, y),
|
|
219
|
+
drag: (from, to) => this.mouseDrag(from, to)
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
get keyboard() {
|
|
223
|
+
return {
|
|
224
|
+
type: (text) => this.keyboardType(text),
|
|
225
|
+
press: (action) => this.keyboardPressAction(action)
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
async clearInput(element) {
|
|
229
|
+
if (!element) {
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
await this.ensureYadb();
|
|
233
|
+
const adb = await this.getAdb();
|
|
234
|
+
await this.mouse.click(element.center[0], element.center[1]);
|
|
235
|
+
await adb.shell(
|
|
236
|
+
'app_process -Djava.class.path=/data/local/tmp/yadb /data/local/tmp com.ysbing.yadb.Main -keyboard "~CLEAR~"'
|
|
237
|
+
);
|
|
238
|
+
await this.mouse.click(element.center[0], element.center[1]);
|
|
239
|
+
}
|
|
240
|
+
async forceScreenshot(path3) {
|
|
241
|
+
await this.ensureYadb();
|
|
242
|
+
const adb = await this.getAdb();
|
|
243
|
+
await adb.shell(
|
|
244
|
+
`app_process -Djava.class.path=/data/local/tmp/yadb /data/local/tmp com.ysbing.yadb.Main -screenshot ${path3}`
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
async url() {
|
|
248
|
+
const adb = await this.getAdb();
|
|
249
|
+
const { appPackage, appActivity } = await adb.getFocusedPackageAndActivity();
|
|
250
|
+
return `${appPackage}/${appActivity}`;
|
|
251
|
+
}
|
|
252
|
+
async scrollUntilTop(startPoint) {
|
|
253
|
+
if (startPoint) {
|
|
254
|
+
const start = { x: startPoint.left, y: startPoint.top };
|
|
255
|
+
const end = { x: start.x, y: 0 };
|
|
256
|
+
await this.mouseDrag(start, end);
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
await this.mouseWheel(0, 9999999, 100);
|
|
260
|
+
}
|
|
261
|
+
async scrollUntilBottom(startPoint) {
|
|
262
|
+
if (startPoint) {
|
|
263
|
+
const { height } = await this.size();
|
|
264
|
+
const start = { x: startPoint.left, y: startPoint.top };
|
|
265
|
+
const end = { x: start.x, y: height };
|
|
266
|
+
await this.mouseDrag(start, end);
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
await this.mouseWheel(0, -9999999, 100);
|
|
270
|
+
}
|
|
271
|
+
async scrollUntilLeft(startPoint) {
|
|
272
|
+
if (startPoint) {
|
|
273
|
+
const start = { x: startPoint.left, y: startPoint.top };
|
|
274
|
+
const end = { x: 0, y: start.y };
|
|
275
|
+
await this.mouseDrag(start, end);
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
await this.mouseWheel(9999999, 0, 100);
|
|
279
|
+
}
|
|
280
|
+
async scrollUntilRight(startPoint) {
|
|
281
|
+
if (startPoint) {
|
|
282
|
+
const { width } = await this.size();
|
|
283
|
+
const start = { x: startPoint.left, y: startPoint.top };
|
|
284
|
+
const end = { x: width, y: start.y };
|
|
285
|
+
await this.mouseDrag(start, end);
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
await this.mouseWheel(-9999999, 0, 100);
|
|
289
|
+
}
|
|
290
|
+
async scrollUp(distance, startPoint) {
|
|
291
|
+
const { height } = await this.size();
|
|
292
|
+
const scrollDistance = distance || height;
|
|
293
|
+
if (startPoint) {
|
|
294
|
+
const start = { x: startPoint.left, y: startPoint.top };
|
|
295
|
+
const endY = Math.max(0, start.y - scrollDistance);
|
|
296
|
+
const end = { x: start.x, y: endY };
|
|
297
|
+
await this.mouseDrag(start, end);
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
await this.mouseWheel(0, scrollDistance, 1e3);
|
|
301
|
+
}
|
|
302
|
+
async scrollDown(distance, startPoint) {
|
|
303
|
+
const { height } = await this.size();
|
|
304
|
+
const scrollDistance = distance || height;
|
|
305
|
+
if (startPoint) {
|
|
306
|
+
const start = { x: startPoint.left, y: startPoint.top };
|
|
307
|
+
const endY = Math.min(height, start.y + scrollDistance);
|
|
308
|
+
const end = { x: start.x, y: endY };
|
|
309
|
+
await this.mouseDrag(start, end);
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
await this.mouseWheel(0, -scrollDistance, 1e3);
|
|
313
|
+
}
|
|
314
|
+
async scrollLeft(distance, startPoint) {
|
|
315
|
+
const { width } = await this.size();
|
|
316
|
+
const scrollDistance = distance || width;
|
|
317
|
+
if (startPoint) {
|
|
318
|
+
const start = { x: startPoint.left, y: startPoint.top };
|
|
319
|
+
const endX = Math.max(0, start.x - scrollDistance);
|
|
320
|
+
const end = { x: endX, y: start.y };
|
|
321
|
+
await this.mouseDrag(start, end);
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
await this.mouseWheel(scrollDistance, 0, 1e3);
|
|
325
|
+
}
|
|
326
|
+
async scrollRight(distance, startPoint) {
|
|
327
|
+
const { width } = await this.size();
|
|
328
|
+
const scrollDistance = distance || width;
|
|
329
|
+
if (startPoint) {
|
|
330
|
+
const start = { x: startPoint.left, y: startPoint.top };
|
|
331
|
+
const endX = Math.min(width, start.x + scrollDistance);
|
|
332
|
+
const end = { x: endX, y: start.y };
|
|
333
|
+
await this.mouseDrag(start, end);
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
await this.mouseWheel(-scrollDistance, 0, 1e3);
|
|
337
|
+
}
|
|
338
|
+
async ensureYadb() {
|
|
339
|
+
if (!this.yadbPushed) {
|
|
340
|
+
const adb = await this.getAdb();
|
|
341
|
+
const yadbBin = import_node_path.default.join(__dirname, "../../bin/yadb");
|
|
342
|
+
await adb.push(yadbBin, "/data/local/tmp");
|
|
343
|
+
this.yadbPushed = true;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
async keyboardType(text) {
|
|
347
|
+
if (!text)
|
|
348
|
+
return;
|
|
349
|
+
const adb = await this.getAdb();
|
|
350
|
+
const isChinese = /[\p{Script=Han}\p{sc=Hani}]/u.test(text);
|
|
351
|
+
if (!isChinese) {
|
|
352
|
+
await adb.inputText(text);
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
await this.execYadb(text);
|
|
356
|
+
}
|
|
357
|
+
async keyboardPress(key) {
|
|
358
|
+
const keyCodeMap = {
|
|
359
|
+
Enter: 66,
|
|
360
|
+
Backspace: 67,
|
|
361
|
+
Tab: 61,
|
|
362
|
+
ArrowUp: 19,
|
|
363
|
+
ArrowDown: 20,
|
|
364
|
+
ArrowLeft: 21,
|
|
365
|
+
ArrowRight: 22,
|
|
366
|
+
Escape: 111,
|
|
367
|
+
Home: 3,
|
|
368
|
+
End: 123
|
|
369
|
+
};
|
|
370
|
+
const adb = await this.getAdb();
|
|
371
|
+
const keyCode = keyCodeMap[key];
|
|
372
|
+
if (keyCode !== void 0) {
|
|
373
|
+
await adb.keyevent(keyCode);
|
|
374
|
+
} else {
|
|
375
|
+
if (key.length === 1) {
|
|
376
|
+
const asciiCode = key.toUpperCase().charCodeAt(0);
|
|
377
|
+
if (asciiCode >= 65 && asciiCode <= 90) {
|
|
378
|
+
await adb.keyevent(asciiCode - 36);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
async keyboardPressAction(action) {
|
|
384
|
+
if (Array.isArray(action)) {
|
|
385
|
+
for (const act of action) {
|
|
386
|
+
await this.keyboardPress(act.key);
|
|
387
|
+
}
|
|
388
|
+
} else {
|
|
389
|
+
await this.keyboardPress(action.key);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
async mouseClick(x, y) {
|
|
393
|
+
const adb = await this.getAdb();
|
|
394
|
+
const { x: adjustedX, y: adjustedY } = this.adjustCoordinates(x, y);
|
|
395
|
+
await adb.shell(`input tap ${adjustedX} ${adjustedY}`);
|
|
396
|
+
}
|
|
397
|
+
async mouseMove(x, y) {
|
|
398
|
+
return Promise.resolve();
|
|
399
|
+
}
|
|
400
|
+
async mouseDrag(from, to) {
|
|
401
|
+
const adb = await this.getAdb();
|
|
402
|
+
const { x: fromX, y: fromY } = this.adjustCoordinates(from.x, from.y);
|
|
403
|
+
const { x: toX, y: toY } = this.adjustCoordinates(to.x, to.y);
|
|
404
|
+
await adb.shell(`input swipe ${fromX} ${fromY} ${toX} ${toY} 300`);
|
|
405
|
+
}
|
|
406
|
+
async mouseWheel(deltaX, deltaY, duration = 1e3) {
|
|
407
|
+
const { width, height } = await this.size();
|
|
408
|
+
const n = 4;
|
|
409
|
+
const startX = deltaX < 0 ? (n - 1) * (width / n) : width / n;
|
|
410
|
+
const startY = deltaY < 0 ? (n - 1) * (height / n) : height / n;
|
|
411
|
+
const maxNegativeDeltaX = startX;
|
|
412
|
+
const maxPositiveDeltaX = (n - 1) * (width / n);
|
|
413
|
+
const maxNegativeDeltaY = startY;
|
|
414
|
+
const maxPositiveDeltaY = (n - 1) * (height / n);
|
|
415
|
+
deltaX = Math.max(-maxNegativeDeltaX, Math.min(deltaX, maxPositiveDeltaX));
|
|
416
|
+
deltaY = Math.max(-maxNegativeDeltaY, Math.min(deltaY, maxPositiveDeltaY));
|
|
417
|
+
const endX = startX + deltaX;
|
|
418
|
+
const endY = startY + deltaY;
|
|
419
|
+
const { x: adjustedStartX, y: adjustedStartY } = this.adjustCoordinates(
|
|
420
|
+
startX,
|
|
421
|
+
startY
|
|
422
|
+
);
|
|
423
|
+
const { x: adjustedEndX, y: adjustedEndY } = this.adjustCoordinates(
|
|
424
|
+
endX,
|
|
425
|
+
endY
|
|
426
|
+
);
|
|
427
|
+
const adb = await this.getAdb();
|
|
428
|
+
await adb.shell(
|
|
429
|
+
`input swipe ${adjustedStartX} ${adjustedStartY} ${adjustedEndX} ${adjustedEndY} ${duration}`
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
async destroy() {
|
|
433
|
+
try {
|
|
434
|
+
const adb = await this.getAdb();
|
|
435
|
+
await adb.shell(`rm -f ${androidScreenshotPath}`);
|
|
436
|
+
} catch (error) {
|
|
437
|
+
console.error("Error during cleanup:", error);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
};
|
|
441
|
+
|
|
442
|
+
// src/agent/index.ts
|
|
443
|
+
var import_agent = require("@midscene/web/agent");
|
|
444
|
+
|
|
445
|
+
// src/utils/index.ts
|
|
446
|
+
var import_appium_adb2 = require("appium-adb");
|
|
447
|
+
|
|
448
|
+
// src/agent/index.ts
|
|
449
|
+
var AndroidAgent = class extends import_agent.PageAgent {
|
|
450
|
+
async launch(uri) {
|
|
451
|
+
const device = this.page;
|
|
452
|
+
await device.launch(uri);
|
|
453
|
+
}
|
|
454
|
+
};
|
|
455
|
+
|
|
456
|
+
// src/playground/scrcpy-server.ts
|
|
457
|
+
var import_node_child_process = require("child_process");
|
|
458
|
+
var import_node_fs2 = require("fs");
|
|
459
|
+
var import_node_http = require("http");
|
|
460
|
+
var import_node_util = require("util");
|
|
461
|
+
var import_adb = require("@yume-chan/adb");
|
|
462
|
+
var import_adb_scrcpy = require("@yume-chan/adb-scrcpy");
|
|
463
|
+
var import_adb_server_node_tcp = require("@yume-chan/adb-server-node-tcp");
|
|
464
|
+
var import_fetch_scrcpy_server = require("@yume-chan/fetch-scrcpy-server");
|
|
465
|
+
var import_scrcpy = require("@yume-chan/scrcpy");
|
|
466
|
+
var import_stream_extra = require("@yume-chan/stream-extra");
|
|
467
|
+
var import_cors = __toESM(require("cors"));
|
|
468
|
+
var import_express = __toESM(require("express"));
|
|
469
|
+
var import_socket = require("socket.io");
|
|
470
|
+
var promiseExec = (0, import_node_util.promisify)(import_node_child_process.exec);
|
|
471
|
+
var ScrcpyServer = class {
|
|
472
|
+
// 用于保存上次设备列表的JSON字符串,用于比较变化
|
|
473
|
+
constructor() {
|
|
474
|
+
this.defaultPort = 5700;
|
|
475
|
+
this.adbClient = null;
|
|
476
|
+
this.currentDeviceId = null;
|
|
477
|
+
this.devicePollInterval = null;
|
|
478
|
+
this.lastDeviceList = "";
|
|
479
|
+
this.app = (0, import_express.default)();
|
|
480
|
+
this.httpServer = (0, import_node_http.createServer)(this.app);
|
|
481
|
+
this.io = new import_socket.Server(this.httpServer, {
|
|
482
|
+
cors: {
|
|
483
|
+
origin: [
|
|
484
|
+
/^http:\/\/localhost(:\d+)?$/,
|
|
485
|
+
/^http:\/\/127\.0\.0\.1(:\d+)?$/
|
|
486
|
+
],
|
|
487
|
+
methods: ["GET", "POST"],
|
|
488
|
+
credentials: true
|
|
489
|
+
}
|
|
490
|
+
});
|
|
491
|
+
this.app.use(
|
|
492
|
+
(0, import_cors.default)({
|
|
493
|
+
origin: "*",
|
|
494
|
+
credentials: true
|
|
495
|
+
})
|
|
496
|
+
);
|
|
497
|
+
this.setupSocketHandlers();
|
|
498
|
+
this.setupApiRoutes();
|
|
499
|
+
}
|
|
500
|
+
// setup API routes
|
|
501
|
+
setupApiRoutes() {
|
|
502
|
+
this.app.get("/api/devices", async (req, res) => {
|
|
503
|
+
try {
|
|
504
|
+
const devices = await this.getDevicesList();
|
|
505
|
+
res.json({ devices, currentDeviceId: this.currentDeviceId });
|
|
506
|
+
} catch (error) {
|
|
507
|
+
res.status(500).json({ error: error.message || "Failed to get devices list" });
|
|
508
|
+
}
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
// get devices list
|
|
512
|
+
async getDevicesList() {
|
|
513
|
+
try {
|
|
514
|
+
debugPage("start to get devices list");
|
|
515
|
+
const client = await this.getAdbClient();
|
|
516
|
+
if (!client) {
|
|
517
|
+
console.warn("failed to get adb client");
|
|
518
|
+
return [];
|
|
519
|
+
}
|
|
520
|
+
debugPage("success to get adb client, start to request devices list");
|
|
521
|
+
let devices;
|
|
522
|
+
try {
|
|
523
|
+
devices = await client.getDevices();
|
|
524
|
+
debugPage("original devices list:", devices);
|
|
525
|
+
} catch (error) {
|
|
526
|
+
console.error("failed to get devices list:", error);
|
|
527
|
+
return [];
|
|
528
|
+
}
|
|
529
|
+
if (!devices || devices.length === 0) {
|
|
530
|
+
return [];
|
|
531
|
+
}
|
|
532
|
+
const formattedDevices = devices.map((device) => {
|
|
533
|
+
const result = {
|
|
534
|
+
id: device.serial,
|
|
535
|
+
name: device.product || device.model || device.serial,
|
|
536
|
+
status: device.state || "device"
|
|
537
|
+
};
|
|
538
|
+
return result;
|
|
539
|
+
});
|
|
540
|
+
return formattedDevices;
|
|
541
|
+
} catch (error) {
|
|
542
|
+
console.error("failed to get devices list:", error);
|
|
543
|
+
return [];
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
// get adb client
|
|
547
|
+
async getAdbClient() {
|
|
548
|
+
try {
|
|
549
|
+
if (!this.adbClient) {
|
|
550
|
+
await promiseExec("adb start-server");
|
|
551
|
+
debugPage("adb server started");
|
|
552
|
+
debugPage("initialize adb client");
|
|
553
|
+
this.adbClient = new import_adb.AdbServerClient(
|
|
554
|
+
new import_adb_server_node_tcp.AdbServerNodeTcpConnector({
|
|
555
|
+
host: "localhost",
|
|
556
|
+
port: 5037
|
|
557
|
+
})
|
|
558
|
+
);
|
|
559
|
+
await debugPage("success to initialize adb client");
|
|
560
|
+
} else {
|
|
561
|
+
debugPage("use existing adb client");
|
|
562
|
+
}
|
|
563
|
+
return this.adbClient;
|
|
564
|
+
} catch (error) {
|
|
565
|
+
console.error("failed to get adb client:", error);
|
|
566
|
+
return null;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
// get adb object
|
|
570
|
+
async getAdb(deviceId) {
|
|
571
|
+
try {
|
|
572
|
+
const client = await this.getAdbClient();
|
|
573
|
+
if (!client) {
|
|
574
|
+
return null;
|
|
575
|
+
}
|
|
576
|
+
if (deviceId) {
|
|
577
|
+
this.currentDeviceId = deviceId;
|
|
578
|
+
return new import_adb.Adb(await client.createTransport({ serial: deviceId }));
|
|
579
|
+
}
|
|
580
|
+
const devices = await client.getDevices();
|
|
581
|
+
if (devices.length === 0) {
|
|
582
|
+
return null;
|
|
583
|
+
}
|
|
584
|
+
this.currentDeviceId = devices[0].serial;
|
|
585
|
+
return new import_adb.Adb(await client.createTransport(devices[0]));
|
|
586
|
+
} catch (error) {
|
|
587
|
+
console.error("failed to get adb client:", error);
|
|
588
|
+
return null;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
// start scrcpy
|
|
592
|
+
async startScrcpy(adb, options = {}) {
|
|
593
|
+
try {
|
|
594
|
+
await import_adb_scrcpy.AdbScrcpyClient.pushServer(
|
|
595
|
+
adb,
|
|
596
|
+
import_stream_extra.ReadableStream.from((0, import_node_fs2.createReadStream)(import_fetch_scrcpy_server.BIN))
|
|
597
|
+
);
|
|
598
|
+
const scrcpyOptions = new import_scrcpy.ScrcpyOptions3_1({
|
|
599
|
+
// default options
|
|
600
|
+
audio: false,
|
|
601
|
+
control: true,
|
|
602
|
+
maxSize: 1024,
|
|
603
|
+
// use videoBitRate as property name
|
|
604
|
+
videoBitRate: 2e6,
|
|
605
|
+
// override default values with user provided options
|
|
606
|
+
...options
|
|
607
|
+
});
|
|
608
|
+
return await import_adb_scrcpy.AdbScrcpyClient.start(
|
|
609
|
+
adb,
|
|
610
|
+
import_scrcpy.DefaultServerPath,
|
|
611
|
+
new import_adb_scrcpy.AdbScrcpyOptions2_1(scrcpyOptions)
|
|
612
|
+
);
|
|
613
|
+
} catch (error) {
|
|
614
|
+
console.error("failed to start scrcpy:", error);
|
|
615
|
+
throw error;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
// setup Socket.IO connection handlers
|
|
619
|
+
setupSocketHandlers() {
|
|
620
|
+
this.io.on("connection", async (socket) => {
|
|
621
|
+
debugPage(
|
|
622
|
+
"client connected, id: %s, client address: %s",
|
|
623
|
+
socket.id,
|
|
624
|
+
socket.handshake.address
|
|
625
|
+
);
|
|
626
|
+
let scrcpyClient = null;
|
|
627
|
+
let adb = null;
|
|
628
|
+
const sendDevicesList = async () => {
|
|
629
|
+
try {
|
|
630
|
+
debugPage("Socket request to get devices list");
|
|
631
|
+
const devices = await this.getDevicesList();
|
|
632
|
+
debugPage("send devices list to client:", devices);
|
|
633
|
+
socket.emit("devices-list", {
|
|
634
|
+
devices,
|
|
635
|
+
currentDeviceId: this.currentDeviceId
|
|
636
|
+
});
|
|
637
|
+
} catch (error) {
|
|
638
|
+
console.error("failed to send devices list:", error);
|
|
639
|
+
socket.emit("error", { message: "failed to get devices list" });
|
|
640
|
+
}
|
|
641
|
+
};
|
|
642
|
+
await sendDevicesList();
|
|
643
|
+
socket.on("get-devices", async () => {
|
|
644
|
+
debugPage("received client request to get devices list");
|
|
645
|
+
await sendDevicesList();
|
|
646
|
+
});
|
|
647
|
+
socket.on("switch-device", async (deviceId) => {
|
|
648
|
+
debugPage("received client request to switch device:", deviceId);
|
|
649
|
+
try {
|
|
650
|
+
if (scrcpyClient) {
|
|
651
|
+
await scrcpyClient.close();
|
|
652
|
+
scrcpyClient = null;
|
|
653
|
+
}
|
|
654
|
+
this.currentDeviceId = deviceId;
|
|
655
|
+
debugPage("device switched to:", deviceId);
|
|
656
|
+
socket.emit("device-switched", { deviceId });
|
|
657
|
+
this.io.emit("global-device-switched", {
|
|
658
|
+
deviceId,
|
|
659
|
+
timestamp: Date.now()
|
|
660
|
+
});
|
|
661
|
+
} catch (error) {
|
|
662
|
+
console.error("failed to switch device:", error);
|
|
663
|
+
socket.emit("error", {
|
|
664
|
+
message: `Failed to switch device: ${error?.message || "Unknown error"}`
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
});
|
|
668
|
+
socket.on("connect-device", async (options) => {
|
|
669
|
+
try {
|
|
670
|
+
debugPage(
|
|
671
|
+
"received device connection request, options: %s, client id: %s",
|
|
672
|
+
options,
|
|
673
|
+
socket.id
|
|
674
|
+
);
|
|
675
|
+
adb = await this.getAdb(this.currentDeviceId || void 0);
|
|
676
|
+
if (!adb) {
|
|
677
|
+
console.error("no available device found");
|
|
678
|
+
socket.emit("error", { message: "No device found" });
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
debugPage(
|
|
682
|
+
"starting scrcpy service, device id: %s",
|
|
683
|
+
this.currentDeviceId
|
|
684
|
+
);
|
|
685
|
+
scrcpyClient = await this.startScrcpy(adb, options);
|
|
686
|
+
debugPage("scrcpy service started successfully");
|
|
687
|
+
debugPage(
|
|
688
|
+
"check scrcpyClient object structure: %s",
|
|
689
|
+
Object.getOwnPropertyNames(scrcpyClient).map((name) => {
|
|
690
|
+
const type = typeof scrcpyClient[name];
|
|
691
|
+
const isPromise = type === "object" && scrcpyClient[name] && typeof scrcpyClient[name].then === "function";
|
|
692
|
+
return `${name}: ${type}${isPromise ? " (Promise)" : ""}`;
|
|
693
|
+
})
|
|
694
|
+
);
|
|
695
|
+
try {
|
|
696
|
+
if (scrcpyClient.videoStream) {
|
|
697
|
+
debugPage(
|
|
698
|
+
"videoStream exists, type: %s",
|
|
699
|
+
typeof scrcpyClient.videoStream
|
|
700
|
+
);
|
|
701
|
+
let videoStream;
|
|
702
|
+
if (typeof scrcpyClient.videoStream === "object" && typeof scrcpyClient.videoStream.then === "function") {
|
|
703
|
+
debugPage(
|
|
704
|
+
"videoStream is a Promise, waiting for resolution..."
|
|
705
|
+
);
|
|
706
|
+
videoStream = await scrcpyClient.videoStream;
|
|
707
|
+
} else {
|
|
708
|
+
debugPage("videoStream is not a Promise, directly use");
|
|
709
|
+
videoStream = scrcpyClient.videoStream;
|
|
710
|
+
}
|
|
711
|
+
debugPage(
|
|
712
|
+
"video stream fetched successfully, metadata: %s",
|
|
713
|
+
videoStream.metadata
|
|
714
|
+
);
|
|
715
|
+
const metadata = videoStream.metadata || {};
|
|
716
|
+
debugPage("original metadata: %s", metadata);
|
|
717
|
+
if (!metadata.codec) {
|
|
718
|
+
debugPage(
|
|
719
|
+
"metadata does not have codec field, use H264 by default"
|
|
720
|
+
);
|
|
721
|
+
metadata.codec = import_scrcpy.ScrcpyVideoCodecId.H264;
|
|
722
|
+
}
|
|
723
|
+
if (!metadata.width || !metadata.height) {
|
|
724
|
+
debugPage(
|
|
725
|
+
"metadata does not have width or height field, use default values"
|
|
726
|
+
);
|
|
727
|
+
metadata.width = metadata.width || 1080;
|
|
728
|
+
metadata.height = metadata.height || 1920;
|
|
729
|
+
}
|
|
730
|
+
debugPage(
|
|
731
|
+
"prepare to send video-metadata event to client, data: %s",
|
|
732
|
+
JSON.stringify(metadata)
|
|
733
|
+
);
|
|
734
|
+
socket.emit("video-metadata", metadata);
|
|
735
|
+
debugPage(
|
|
736
|
+
"video-metadata event sent to client, id: %s",
|
|
737
|
+
socket.id
|
|
738
|
+
);
|
|
739
|
+
const { stream } = videoStream;
|
|
740
|
+
const reader = stream.getReader();
|
|
741
|
+
const processStream = async () => {
|
|
742
|
+
try {
|
|
743
|
+
while (true) {
|
|
744
|
+
const { done, value } = await reader.read();
|
|
745
|
+
if (done)
|
|
746
|
+
break;
|
|
747
|
+
const frameType = value.type || "data";
|
|
748
|
+
socket.emit("video-data", {
|
|
749
|
+
data: Array.from(value.data),
|
|
750
|
+
type: frameType,
|
|
751
|
+
timestamp: Date.now(),
|
|
752
|
+
// fix keyframe access
|
|
753
|
+
keyFrame: value.keyFrame
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
} catch (error) {
|
|
757
|
+
console.error("error processing video stream:", error);
|
|
758
|
+
socket.emit("error", {
|
|
759
|
+
message: "video stream processing error"
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
};
|
|
763
|
+
processStream();
|
|
764
|
+
} else {
|
|
765
|
+
console.error(
|
|
766
|
+
"scrcpyClient object does not have videoStream property"
|
|
767
|
+
);
|
|
768
|
+
socket.emit("error", {
|
|
769
|
+
message: "Video stream not available in scrcpy client"
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
} catch (error) {
|
|
773
|
+
console.error("error processing video stream:", error);
|
|
774
|
+
socket.emit("error", {
|
|
775
|
+
message: `Video stream processing error: ${error.message}`
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
if (scrcpyClient?.controller) {
|
|
779
|
+
socket.emit("control-ready");
|
|
780
|
+
}
|
|
781
|
+
} catch (error) {
|
|
782
|
+
console.error("failed to connect device:", error);
|
|
783
|
+
socket.emit("error", {
|
|
784
|
+
message: `Failed to connect device: ${error?.message || "Unknown error"}`
|
|
785
|
+
});
|
|
786
|
+
}
|
|
787
|
+
});
|
|
788
|
+
socket.on("disconnect", async (reason) => {
|
|
789
|
+
debugPage("client disconnected, id: %s, reason: %s", socket.id, reason);
|
|
790
|
+
if (scrcpyClient) {
|
|
791
|
+
try {
|
|
792
|
+
debugPage("closing scrcpy client");
|
|
793
|
+
await scrcpyClient.close();
|
|
794
|
+
} catch (error) {
|
|
795
|
+
console.error("failed to close scrcpy client:", error);
|
|
796
|
+
}
|
|
797
|
+
scrcpyClient = null;
|
|
798
|
+
}
|
|
799
|
+
});
|
|
800
|
+
});
|
|
801
|
+
}
|
|
802
|
+
// launch server
|
|
803
|
+
async launch(port) {
|
|
804
|
+
this.port = port || this.defaultPort;
|
|
805
|
+
return new Promise((resolve) => {
|
|
806
|
+
this.httpServer.listen(this.port, () => {
|
|
807
|
+
console.log(`Scrcpy server running at: http://localhost:${this.port}`);
|
|
808
|
+
this.startDeviceMonitoring();
|
|
809
|
+
resolve(this);
|
|
810
|
+
});
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
// start device monitoring
|
|
814
|
+
startDeviceMonitoring() {
|
|
815
|
+
this.devicePollInterval = setInterval(async () => {
|
|
816
|
+
try {
|
|
817
|
+
const devices = await this.getDevicesList();
|
|
818
|
+
const currentDevicesJson = JSON.stringify(devices);
|
|
819
|
+
if (this.lastDeviceList !== currentDevicesJson) {
|
|
820
|
+
debugPage("devices list changed, push to all connected clients");
|
|
821
|
+
this.lastDeviceList = currentDevicesJson;
|
|
822
|
+
if (!this.currentDeviceId && devices.length > 0) {
|
|
823
|
+
const onlineDevices = devices.filter(
|
|
824
|
+
(device) => device.status.toLowerCase() === "device"
|
|
825
|
+
);
|
|
826
|
+
if (onlineDevices.length > 0) {
|
|
827
|
+
this.currentDeviceId = onlineDevices[0].id;
|
|
828
|
+
debugPage(
|
|
829
|
+
"auto select the first online device:",
|
|
830
|
+
this.currentDeviceId
|
|
831
|
+
);
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
this.io.emit("devices-list", {
|
|
835
|
+
devices,
|
|
836
|
+
currentDeviceId: this.currentDeviceId
|
|
837
|
+
});
|
|
838
|
+
}
|
|
839
|
+
} catch (error) {
|
|
840
|
+
console.error("device monitoring error:", error);
|
|
841
|
+
}
|
|
842
|
+
}, 3e3);
|
|
843
|
+
}
|
|
844
|
+
// close server
|
|
845
|
+
close() {
|
|
846
|
+
if (this.devicePollInterval) {
|
|
847
|
+
clearInterval(this.devicePollInterval);
|
|
848
|
+
this.devicePollInterval = null;
|
|
849
|
+
}
|
|
850
|
+
if (this.httpServer) {
|
|
851
|
+
return this.httpServer.close();
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
};
|
|
855
|
+
|
|
856
|
+
// src/playground/bin.ts
|
|
857
|
+
var staticDir = import_node_path2.default.join(__dirname, "../../static");
|
|
858
|
+
var playgroundServer = new import_midscene_server.default(
|
|
859
|
+
AndroidDevice,
|
|
860
|
+
AndroidAgent,
|
|
861
|
+
staticDir
|
|
862
|
+
);
|
|
863
|
+
var scrcpyServer = new ScrcpyServer();
|
|
864
|
+
Promise.all([playgroundServer.launch(5800), scrcpyServer.launch(5700)]).then(() => {
|
|
865
|
+
console.log(
|
|
866
|
+
`Midscene playground server is running on http://localhost:${playgroundServer.port}`
|
|
867
|
+
);
|
|
868
|
+
(0, import_open.default)(`http://localhost:${playgroundServer.port}`);
|
|
869
|
+
}).catch((error) => {
|
|
870
|
+
console.error("Failed to start servers:", error);
|
|
871
|
+
process.exit(1);
|
|
872
|
+
});
|