@midscene/android 1.10.9 → 1.10.10-beta-20260805080137.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 +620 -141
- package/dist/es/index.mjs +619 -140
- package/dist/lib/cli.js +620 -141
- package/dist/lib/index.js +619 -140
- package/dist/types/index.d.ts +94 -2
- package/package.json +4 -4
package/dist/es/cli.mjs
CHANGED
|
@@ -18,8 +18,10 @@ import { ADB, getSdkRootFromEnv } from "appium-adb";
|
|
|
18
18
|
var __webpack_modules__ = {
|
|
19
19
|
"./src/scrcpy-manager.ts" (__unused_rspack_module, __webpack_exports__, __webpack_require__) {
|
|
20
20
|
__webpack_require__.d(__webpack_exports__, {
|
|
21
|
+
GJ: ()=>isScrcpyFreshFrameUnavailableError,
|
|
21
22
|
ScrcpyScreenshotManager: ()=>ScrcpyScreenshotManager,
|
|
22
|
-
|
|
23
|
+
Xr: ()=>SCRCPY_VIDEO_BIT_RATE_NETWORK_HINT,
|
|
24
|
+
ov: ()=>DEFAULT_SCRCPY_CONFIG
|
|
23
25
|
});
|
|
24
26
|
var node_fs__rspack_import_0 = __webpack_require__("node:fs");
|
|
25
27
|
var node_module__rspack_import_1 = __webpack_require__("node:module");
|
|
@@ -54,6 +56,14 @@ var __webpack_modules__ = {
|
|
|
54
56
|
const CONNECTION_WAIT_MS = 1000;
|
|
55
57
|
const MAX_SERVER_OUTPUT_LINES = 100;
|
|
56
58
|
const SERVER_OUTPUT_DRAIN_TIMEOUT_MS = 500;
|
|
59
|
+
const MAX_FRAME_AGE_US = 500000n;
|
|
60
|
+
const FRAME_FRESHNESS_WARN_INTERVAL_MS = 5000;
|
|
61
|
+
const TRANSPORT_BACKLOG_WARN_INTERVAL_MS = 5000;
|
|
62
|
+
const DEVICE_UPTIME_COMMAND = [
|
|
63
|
+
'dumpsys',
|
|
64
|
+
'power'
|
|
65
|
+
];
|
|
66
|
+
const SCRCPY_VIDEO_BIT_RATE_NETWORK_HINT = 'The appropriate scrcpy video bitrate depends on network conditions. For constrained remote links, consider setting scrcpyConfig.videoBitRate to 4_000_000 (4 Mbps) as a starting point, and lower it further if backlog persists.';
|
|
57
67
|
const BUSY_LOOP_WINDOW_MS = 1000;
|
|
58
68
|
const BUSY_LOOP_MAX_READS = 500;
|
|
59
69
|
const BUSY_LOOP_COOLDOWN_MS = 50;
|
|
@@ -64,6 +74,24 @@ var __webpack_modules__ = {
|
|
|
64
74
|
idleTimeoutMs: DEFAULT_IDLE_TIMEOUT_MS,
|
|
65
75
|
videoBitRate: DEFAULT_VIDEO_BIT_RATE
|
|
66
76
|
};
|
|
77
|
+
const SCRCPY_FRESH_FRAME_UNAVAILABLE_ERROR_CODE = 'ERR_SCRCPY_FRESH_FRAME_UNAVAILABLE';
|
|
78
|
+
class ScrcpyFreshFrameUnavailableError extends Error {
|
|
79
|
+
constructor(message, options){
|
|
80
|
+
super(message, options), _define_property(this, "code", SCRCPY_FRESH_FRAME_UNAVAILABLE_ERROR_CODE);
|
|
81
|
+
this.name = 'ScrcpyFreshFrameUnavailableError';
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function isScrcpyFreshFrameUnavailableError(error) {
|
|
85
|
+
return 'object' == typeof error && null !== error && 'code' in error && error.code === SCRCPY_FRESH_FRAME_UNAVAILABLE_ERROR_CODE;
|
|
86
|
+
}
|
|
87
|
+
function parseDeviceUptimeMs(output) {
|
|
88
|
+
const match = output.match(/mLastWakeTime=(\d+)\s+\((?:(\d+) ms ago|in (\d+) ms|now)\)/);
|
|
89
|
+
if (!match) throw new Error(`Unable to read Android device uptime from dumpsys power: ${output.trim() || '<empty output>'}`);
|
|
90
|
+
const referenceMs = BigInt(match[1]);
|
|
91
|
+
if (match[2]) return referenceMs + BigInt(match[2]);
|
|
92
|
+
if (match[3]) return referenceMs - BigInt(match[3]);
|
|
93
|
+
return referenceMs;
|
|
94
|
+
}
|
|
67
95
|
function isKeyFrameNalType(nalUnitType) {
|
|
68
96
|
return nalUnitType === NAL_TYPE_IDR || nalUnitType === NAL_TYPE_SPS || nalUnitType === NAL_TYPE_PPS;
|
|
69
97
|
}
|
|
@@ -246,24 +274,25 @@ var __webpack_modules__ = {
|
|
|
246
274
|
debugScrcpy(`Received SPS/PPS configuration: ${this.spsHeader.length}B`);
|
|
247
275
|
return;
|
|
248
276
|
}
|
|
277
|
+
const receivedAtUs = this.monotonicTimeUs();
|
|
278
|
+
if (!this.isFrameFresh(packet.pts)) return;
|
|
249
279
|
const frameBuffer = Buffer.from(packet.data);
|
|
250
280
|
const isKeyFrame = detectH264KeyFrame(frameBuffer);
|
|
251
281
|
if (isKeyFrame && this.spsHeader) {
|
|
282
|
+
const timing = this.estimateFrameTiming(packet.pts, receivedAtUs);
|
|
252
283
|
this.lastRawKeyframe = frameBuffer;
|
|
253
|
-
this.lastRawKeyframeAt =
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
capturedAt: this.lastRawKeyframeAt
|
|
266
|
-
};
|
|
284
|
+
this.lastRawKeyframeAt = timing.capturedAt;
|
|
285
|
+
this.lastRawKeyframePtsUs = packet.pts;
|
|
286
|
+
this.lastRawKeyframeEstimatedAgeMs = timing.estimatedAgeMs;
|
|
287
|
+
const frame = {
|
|
288
|
+
data: frameBuffer,
|
|
289
|
+
header: this.spsHeader,
|
|
290
|
+
ptsUs: packet.pts,
|
|
291
|
+
estimatedAgeMs: timing.estimatedAgeMs,
|
|
292
|
+
capturedAt: this.lastRawKeyframeAt
|
|
293
|
+
};
|
|
294
|
+
if (this.keyframeResolvers.length > 0) this.notifyKeyframeWaiters(frame);
|
|
295
|
+
if (this.keyframeListeners.size > 0 && this.isFrameAgeAcceptable(packet.pts, receivedAtUs)) {
|
|
267
296
|
for (const listener of this.keyframeListeners)try {
|
|
268
297
|
listener(frame);
|
|
269
298
|
} catch (error) {
|
|
@@ -273,6 +302,159 @@ var __webpack_modules__ = {
|
|
|
273
302
|
}
|
|
274
303
|
}
|
|
275
304
|
}
|
|
305
|
+
async readDeviceClockCalibration() {
|
|
306
|
+
const startedAtUs = this.monotonicTimeUs();
|
|
307
|
+
const startedAtWallMs = Date.now();
|
|
308
|
+
const shellProtocol = this.adb.subprocess.shellProtocol;
|
|
309
|
+
let output;
|
|
310
|
+
if (shellProtocol) {
|
|
311
|
+
const result = await shellProtocol.spawnWaitText(DEVICE_UPTIME_COMMAND);
|
|
312
|
+
if (0 !== result.exitCode) throw new Error(`Unable to read Android device uptime (exit ${result.exitCode}): ${result.stderr.trim() || result.stdout.trim()}`);
|
|
313
|
+
output = result.stdout;
|
|
314
|
+
} else output = await this.adb.subprocess.noneProtocol.spawnWaitText(DEVICE_UPTIME_COMMAND);
|
|
315
|
+
const finishedAtUs = this.monotonicTimeUs();
|
|
316
|
+
const finishedAtWallMs = Date.now();
|
|
317
|
+
return {
|
|
318
|
+
deviceUptimeUs: 1000n * parseDeviceUptimeMs(output),
|
|
319
|
+
hostMonotonicUs: startedAtUs + (finishedAtUs - startedAtUs) / 2n,
|
|
320
|
+
hostWallTimeMs: startedAtWallMs + (finishedAtWallMs - startedAtWallMs) / 2,
|
|
321
|
+
roundTripUs: finishedAtUs - startedAtUs
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
async ensureFrameClockCalibration() {
|
|
325
|
+
if (this.deviceClockCalibration) return;
|
|
326
|
+
this.deviceClockCalibration = await this.readDeviceClockCalibration();
|
|
327
|
+
debugScrcpy(`Calibrated scrcpy frame clock (RTT=${Number(this.deviceClockCalibration.roundTripUs / 1000n)}ms)`);
|
|
328
|
+
}
|
|
329
|
+
async setFreshnessBarrier(reason) {
|
|
330
|
+
const generation = ++this.frameFreshnessBarrierGeneration;
|
|
331
|
+
this.frameFreshnessBarrierPending = true;
|
|
332
|
+
this.clearFrameCache();
|
|
333
|
+
try {
|
|
334
|
+
const calibration = await this.readDeviceClockCalibration();
|
|
335
|
+
const barrierPtsUs = (calibration.deviceUptimeUs / 1000n + 1n) * 1000n;
|
|
336
|
+
if (generation !== this.frameFreshnessBarrierGeneration) return this.frameFreshnessBarrierPtsUs ?? barrierPtsUs;
|
|
337
|
+
this.deviceClockCalibration = calibration;
|
|
338
|
+
this.frameFreshnessBarrierPtsUs = null === this.frameFreshnessBarrierPtsUs || barrierPtsUs > this.frameFreshnessBarrierPtsUs ? barrierPtsUs : this.frameFreshnessBarrierPtsUs;
|
|
339
|
+
this.frameFreshnessBarrierReason = reason;
|
|
340
|
+
this.frameFreshnessBarrierPending = false;
|
|
341
|
+
this.frameFreshnessError = null;
|
|
342
|
+
this.lastFramePtsUs = null;
|
|
343
|
+
this.clearFrameCache();
|
|
344
|
+
debugScrcpy(`Armed frame freshness barrier at PTS ${this.frameFreshnessBarrierPtsUs}µs (${reason}, clock RTT=${Number(calibration.roundTripUs / 1000n)}ms)`);
|
|
345
|
+
return this.frameFreshnessBarrierPtsUs;
|
|
346
|
+
} catch (error) {
|
|
347
|
+
if (generation === this.frameFreshnessBarrierGeneration) {
|
|
348
|
+
this.frameFreshnessBarrierPending = false;
|
|
349
|
+
this.frameFreshnessError = new Error(`Unable to establish scrcpy frame freshness barrier (${reason}): ${error instanceof Error ? error.message : String(error)}`, {
|
|
350
|
+
cause: error
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
throw this.frameFreshnessError ?? error;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
isFrameFresh(packetPtsUs) {
|
|
357
|
+
if (this.frameFreshnessBarrierPending) return false;
|
|
358
|
+
if (void 0 !== packetPtsUs) {
|
|
359
|
+
if (null !== this.lastFramePtsUs && packetPtsUs < this.lastFramePtsUs) {
|
|
360
|
+
this.frameFreshnessError = new Error('Scrcpy frame PTS moved backwards; refusing frames until the device clock barrier is refreshed');
|
|
361
|
+
this.frameFreshnessBarrierPending = true;
|
|
362
|
+
this.clearFrameCache();
|
|
363
|
+
this.warnFrameFreshness();
|
|
364
|
+
return false;
|
|
365
|
+
}
|
|
366
|
+
this.lastFramePtsUs = packetPtsUs;
|
|
367
|
+
}
|
|
368
|
+
if (null === this.frameFreshnessBarrierPtsUs) return true;
|
|
369
|
+
if (void 0 === packetPtsUs) {
|
|
370
|
+
this.frameFreshnessError = new Error('Scrcpy frame has no PTS metadata; cannot prove that it is newer than the freshness barrier');
|
|
371
|
+
this.warnFrameFreshness();
|
|
372
|
+
return false;
|
|
373
|
+
}
|
|
374
|
+
if (packetPtsUs >= this.frameFreshnessBarrierPtsUs) {
|
|
375
|
+
if (this.frameFreshnessError) debugScrcpy(`Scrcpy video crossed the ${this.frameFreshnessBarrierReason ?? 'active'} freshness barrier at PTS ${packetPtsUs}µs`);
|
|
376
|
+
this.frameFreshnessError = null;
|
|
377
|
+
return true;
|
|
378
|
+
}
|
|
379
|
+
const behindBarrierUs = this.frameFreshnessBarrierPtsUs - packetPtsUs;
|
|
380
|
+
this.frameFreshnessError = new Error(`Scrcpy frame predates the ${this.frameFreshnessBarrierReason ?? 'active'} freshness barrier by ${Number(behindBarrierUs) / 1000}ms; refusing to use it`);
|
|
381
|
+
this.clearFrameCache();
|
|
382
|
+
this.warnFrameFreshness();
|
|
383
|
+
return false;
|
|
384
|
+
}
|
|
385
|
+
estimateFrameAgeUs(packetPtsUs, hostMonotonicUs = this.monotonicTimeUs()) {
|
|
386
|
+
const calibration = this.deviceClockCalibration;
|
|
387
|
+
if (void 0 === packetPtsUs || !calibration) return null;
|
|
388
|
+
const estimatedDeviceNowUs = calibration.deviceUptimeUs + (hostMonotonicUs - calibration.hostMonotonicUs);
|
|
389
|
+
return estimatedDeviceNowUs > packetPtsUs ? estimatedDeviceNowUs - packetPtsUs : 0n;
|
|
390
|
+
}
|
|
391
|
+
isFrameAgeAcceptable(packetPtsUs, hostMonotonicUs = this.monotonicTimeUs()) {
|
|
392
|
+
const ageUs = this.estimateFrameAgeUs(packetPtsUs, hostMonotonicUs);
|
|
393
|
+
if (null === ageUs) {
|
|
394
|
+
this.frameFreshnessError = new Error(void 0 === packetPtsUs ? 'Scrcpy frame has no PTS metadata; cannot prove its absolute age' : 'Scrcpy frame clock is not calibrated; cannot prove its absolute age');
|
|
395
|
+
this.warnFrameFreshness();
|
|
396
|
+
return false;
|
|
397
|
+
}
|
|
398
|
+
if (ageUs <= MAX_FRAME_AGE_US) {
|
|
399
|
+
if (this.frameFreshnessError) {
|
|
400
|
+
debugScrcpy(`Scrcpy frame age recovered (${Number(ageUs / 1000n)}ms)`);
|
|
401
|
+
this.frameFreshnessError = null;
|
|
402
|
+
}
|
|
403
|
+
return true;
|
|
404
|
+
}
|
|
405
|
+
this.frameFreshnessError = new Error(`Scrcpy frame absolute age is ${Number(ageUs / 1000n)}ms, exceeding the ${Number(MAX_FRAME_AGE_US / 1000n)}ms limit`);
|
|
406
|
+
this.warnFrameFreshness();
|
|
407
|
+
return false;
|
|
408
|
+
}
|
|
409
|
+
warnFrameFreshness() {
|
|
410
|
+
if (!this.frameFreshnessError) return;
|
|
411
|
+
const now = Date.now();
|
|
412
|
+
if (now - this.lastFrameFreshnessWarningAt >= FRAME_FRESHNESS_WARN_INTERVAL_MS) {
|
|
413
|
+
warnScrcpy(this.frameFreshnessError.message);
|
|
414
|
+
this.lastFrameFreshnessWarningAt = now;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
warnTransportBacklog(error) {
|
|
418
|
+
const now = Date.now();
|
|
419
|
+
if (now - this.lastTransportBacklogWarningAt < TRANSPORT_BACKLOG_WARN_INTERVAL_MS) return;
|
|
420
|
+
const cause = this.frameFreshnessError ?? error;
|
|
421
|
+
const causeMessage = cause instanceof Error ? cause.message : String(cause);
|
|
422
|
+
const currentBitRateMbps = this.options.videoBitRate / 1000000;
|
|
423
|
+
warnScrcpy(`No usable scrcpy frame crossed the active freshness target within ${FRESH_FRAME_TIMEOUT_MS}ms; closing the stale stream epoch and falling back to ADB screenshot. This may indicate transport backlog or a static screen that emitted no new frame. ${SCRCPY_VIDEO_BIT_RATE_NETWORK_HINT} Current videoBitRate: ${this.options.videoBitRate} bps (${currentBitRateMbps} Mbps).\nError: ${causeMessage}`);
|
|
424
|
+
this.lastTransportBacklogWarningAt = now;
|
|
425
|
+
}
|
|
426
|
+
estimateFrameTiming(packetPtsUs, receivedAtUs) {
|
|
427
|
+
const calibration = this.deviceClockCalibration;
|
|
428
|
+
if (void 0 === packetPtsUs || !calibration) return {
|
|
429
|
+
capturedAt: Date.now()
|
|
430
|
+
};
|
|
431
|
+
const estimatedAgeUs = this.estimateFrameAgeUs(packetPtsUs, receivedAtUs) ?? 0n;
|
|
432
|
+
const estimatedAgeMs = Number(estimatedAgeUs) / 1000;
|
|
433
|
+
const receivedAtWallTimeMs = calibration.hostWallTimeMs + Number(receivedAtUs - calibration.hostMonotonicUs) / 1000;
|
|
434
|
+
return {
|
|
435
|
+
capturedAt: receivedAtWallTimeMs - estimatedAgeMs,
|
|
436
|
+
estimatedAgeMs
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
clearFrameCache() {
|
|
440
|
+
this.lastRawKeyframe = null;
|
|
441
|
+
this.lastRawKeyframeAt = 0;
|
|
442
|
+
this.lastRawKeyframePtsUs = void 0;
|
|
443
|
+
this.lastRawKeyframeEstimatedAgeMs = void 0;
|
|
444
|
+
}
|
|
445
|
+
monotonicTimeUs() {
|
|
446
|
+
return process.hrtime.bigint() / 1000n;
|
|
447
|
+
}
|
|
448
|
+
resetFrameFreshnessState() {
|
|
449
|
+
this.frameFreshnessBarrierPtsUs = null;
|
|
450
|
+
this.frameFreshnessBarrierReason = null;
|
|
451
|
+
this.frameFreshnessBarrierPending = false;
|
|
452
|
+
this.frameFreshnessBarrierGeneration = 0;
|
|
453
|
+
this.deviceClockCalibration = null;
|
|
454
|
+
this.lastFramePtsUs = null;
|
|
455
|
+
this.frameFreshnessError = null;
|
|
456
|
+
this.lastFrameFreshnessWarningAt = 0;
|
|
457
|
+
}
|
|
276
458
|
subscribeKeyframes(listener) {
|
|
277
459
|
this.keyframeListeners.add(listener);
|
|
278
460
|
this.resetIdleTimer();
|
|
@@ -282,10 +464,17 @@ var __webpack_modules__ = {
|
|
|
282
464
|
};
|
|
283
465
|
}
|
|
284
466
|
getLatestRawKeyframe() {
|
|
467
|
+
const frame = this.getCachedKeyframeCandidate();
|
|
468
|
+
if (!frame || !this.isFrameAgeAcceptable(frame.ptsUs)) return null;
|
|
469
|
+
return frame;
|
|
470
|
+
}
|
|
471
|
+
getCachedKeyframeCandidate() {
|
|
285
472
|
if (!this.lastRawKeyframe || !this.spsHeader) return null;
|
|
286
473
|
return {
|
|
287
474
|
data: this.lastRawKeyframe,
|
|
288
475
|
header: this.spsHeader,
|
|
476
|
+
ptsUs: this.lastRawKeyframePtsUs,
|
|
477
|
+
estimatedAgeMs: this.lastRawKeyframeEstimatedAgeMs,
|
|
289
478
|
capturedAt: this.lastRawKeyframeAt
|
|
290
479
|
};
|
|
291
480
|
}
|
|
@@ -295,55 +484,94 @@ var __webpack_modules__ = {
|
|
|
295
484
|
frame.data
|
|
296
485
|
]));
|
|
297
486
|
}
|
|
487
|
+
async waitForUsableKeyframe(timeoutMs) {
|
|
488
|
+
const deadline = Date.now() + timeoutMs;
|
|
489
|
+
let candidate = this.getCachedKeyframeCandidate();
|
|
490
|
+
while(true){
|
|
491
|
+
if (candidate && this.isFrameAgeAcceptable(candidate.ptsUs)) return candidate;
|
|
492
|
+
const remainingMs = deadline - Date.now();
|
|
493
|
+
if (remainingMs <= 0) throw new Error(`No fresh keyframe received within ${timeoutMs}ms`);
|
|
494
|
+
candidate = await this.waitForNextKeyframe(remainingMs);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
async prepareFreshFrame() {
|
|
498
|
+
await this.ensureConnected();
|
|
499
|
+
await this.ensureFrameClockCalibration();
|
|
500
|
+
await this.waitForKeyframe();
|
|
501
|
+
await this.waitForUsableKeyframe(MAX_KEYFRAME_WAIT_MS);
|
|
502
|
+
}
|
|
503
|
+
async waitForPlanningFrame() {
|
|
504
|
+
let planningBarrierArmed = false;
|
|
505
|
+
let deadline = Date.now() + FRESH_FRAME_TIMEOUT_MS;
|
|
506
|
+
let candidate = this.getCachedKeyframeCandidate();
|
|
507
|
+
while(true){
|
|
508
|
+
if (candidate) {
|
|
509
|
+
if (void 0 === candidate.ptsUs) throw new Error('Scrcpy frame has no PTS metadata; cannot prove planning freshness');
|
|
510
|
+
const ageUs = this.estimateFrameAgeUs(candidate.ptsUs);
|
|
511
|
+
if (null === ageUs) throw new Error('Scrcpy frame clock is not calibrated; cannot prove planning freshness');
|
|
512
|
+
if (ageUs <= MAX_FRAME_AGE_US) return candidate;
|
|
513
|
+
if (planningBarrierArmed) this.clearFrameCache();
|
|
514
|
+
else {
|
|
515
|
+
debugScrcpy(`Planning candidate PTS ${candidate.ptsUs}µs has absolute age ${Number(ageUs) / 1000}ms, exceeding the ${Number(MAX_FRAME_AGE_US / 1000n)}ms limit; arming a planning freshness barrier`);
|
|
516
|
+
await this.setFreshnessBarrier('stale planning frame');
|
|
517
|
+
planningBarrierArmed = true;
|
|
518
|
+
deadline = Date.now() + FRESH_FRAME_TIMEOUT_MS;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
const remainingMs = deadline - Date.now();
|
|
522
|
+
if (remainingMs <= 0) throw new Error(`No scrcpy frame crossed the active freshness target within ${FRESH_FRAME_TIMEOUT_MS}ms`);
|
|
523
|
+
candidate = await this.waitForNextKeyframe(remainingMs);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
async closeStaleStreamAndCreateFallbackError(error) {
|
|
527
|
+
this.warnTransportBacklog(error);
|
|
528
|
+
const causeMessage = error instanceof Error ? error.message : String(error);
|
|
529
|
+
await this.disconnect();
|
|
530
|
+
return new ScrcpyFreshFrameUnavailableError(`Unable to obtain a fresh scrcpy frame; the stale stream epoch was closed so the caller can use ADB screenshot fallback. ${causeMessage}`, {
|
|
531
|
+
cause: error
|
|
532
|
+
});
|
|
533
|
+
}
|
|
298
534
|
async getScreenshotJpeg() {
|
|
299
535
|
const perfStart = Date.now();
|
|
300
536
|
const t1 = Date.now();
|
|
301
537
|
await this.ensureConnected();
|
|
302
538
|
const connectTime = Date.now() - t1;
|
|
303
539
|
const t2 = Date.now();
|
|
304
|
-
|
|
305
|
-
const spsWaitTime = Date.now() - t2;
|
|
306
|
-
const t3 = Date.now();
|
|
307
|
-
let keyframeBuffer;
|
|
308
|
-
let frameSource;
|
|
540
|
+
let frame;
|
|
309
541
|
try {
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
this.spsHeader,
|
|
316
|
-
this.lastRawKeyframe
|
|
317
|
-
]);
|
|
318
|
-
frameSource = 'cached';
|
|
319
|
-
} else {
|
|
320
|
-
keyframeBuffer = await this.waitForNextKeyframe(MAX_KEYFRAME_WAIT_MS);
|
|
321
|
-
frameSource = 'fresh-retry';
|
|
322
|
-
}
|
|
542
|
+
await this.ensureFrameClockCalibration();
|
|
543
|
+
await this.waitForKeyframe();
|
|
544
|
+
frame = await this.waitForPlanningFrame();
|
|
545
|
+
} catch (error) {
|
|
546
|
+
throw await this.closeStaleStreamAndCreateFallbackError(error);
|
|
323
547
|
}
|
|
324
|
-
const frameWaitTime = Date.now() -
|
|
548
|
+
const frameWaitTime = Date.now() - t2;
|
|
549
|
+
const keyframeBuffer = Buffer.concat([
|
|
550
|
+
frame.header,
|
|
551
|
+
frame.data
|
|
552
|
+
]);
|
|
325
553
|
this.resetIdleTimer();
|
|
326
|
-
debugScrcpy(`Decoding H.264 stream: ${keyframeBuffer.length} bytes (
|
|
327
|
-
const
|
|
554
|
+
debugScrcpy(`Decoding H.264 stream: ${keyframeBuffer.length} bytes (post-barrier)`);
|
|
555
|
+
const t5 = Date.now();
|
|
328
556
|
const result = await this.decodeH264ToJpeg(keyframeBuffer);
|
|
329
|
-
const decodeTime = Date.now() -
|
|
557
|
+
const decodeTime = Date.now() - t5;
|
|
330
558
|
const totalTime = Date.now() - perfStart;
|
|
331
|
-
debugScrcpy(`Performance: total=${totalTime}ms (connect=${connectTime}ms,
|
|
559
|
+
debugScrcpy(`Performance: total=${totalTime}ms (connect=${connectTime}ms, frameWait=${frameWaitTime}ms, decode=${decodeTime}ms)`);
|
|
332
560
|
return result;
|
|
333
561
|
}
|
|
334
562
|
getResolution() {
|
|
335
563
|
return this.videoResolution;
|
|
336
564
|
}
|
|
337
|
-
notifyKeyframeWaiters(
|
|
565
|
+
notifyKeyframeWaiters(frame) {
|
|
338
566
|
const resolvers = this.keyframeResolvers;
|
|
339
567
|
this.keyframeResolvers = [];
|
|
340
|
-
for (const resolve of resolvers)resolve(
|
|
568
|
+
for (const resolve of resolvers)resolve(frame);
|
|
341
569
|
}
|
|
342
570
|
waitForNextKeyframe(timeoutMs) {
|
|
343
571
|
return new Promise((resolve, reject)=>{
|
|
344
|
-
const wrappedResolve = (
|
|
572
|
+
const wrappedResolve = (frame)=>{
|
|
345
573
|
clearTimeout(timer);
|
|
346
|
-
resolve(
|
|
574
|
+
resolve(frame);
|
|
347
575
|
};
|
|
348
576
|
const timer = setTimeout(()=>{
|
|
349
577
|
this.keyframeResolvers = this.keyframeResolvers.filter((r)=>r !== wrappedResolve);
|
|
@@ -466,11 +694,11 @@ var __webpack_modules__ = {
|
|
|
466
694
|
this.videoStream = null;
|
|
467
695
|
this.streamReader = null;
|
|
468
696
|
this.spsHeader = null;
|
|
469
|
-
this.
|
|
470
|
-
this.lastRawKeyframeAt = 0;
|
|
697
|
+
this.clearFrameCache();
|
|
471
698
|
this.isInitialized = false;
|
|
472
699
|
this.keyframeResolvers = [];
|
|
473
700
|
this.keyframeListeners.clear();
|
|
701
|
+
this.resetFrameFreshnessState();
|
|
474
702
|
if (reader) try {
|
|
475
703
|
await reader.cancel();
|
|
476
704
|
} catch (error) {
|
|
@@ -500,8 +728,19 @@ var __webpack_modules__ = {
|
|
|
500
728
|
_define_property(this, "keyframeListeners", new Set());
|
|
501
729
|
_define_property(this, "lastRawKeyframe", null);
|
|
502
730
|
_define_property(this, "lastRawKeyframeAt", 0);
|
|
731
|
+
_define_property(this, "lastRawKeyframePtsUs", void 0);
|
|
732
|
+
_define_property(this, "lastRawKeyframeEstimatedAgeMs", void 0);
|
|
503
733
|
_define_property(this, "videoResolution", null);
|
|
504
734
|
_define_property(this, "streamReader", null);
|
|
735
|
+
_define_property(this, "frameFreshnessBarrierPtsUs", null);
|
|
736
|
+
_define_property(this, "frameFreshnessBarrierReason", null);
|
|
737
|
+
_define_property(this, "frameFreshnessBarrierPending", false);
|
|
738
|
+
_define_property(this, "frameFreshnessBarrierGeneration", 0);
|
|
739
|
+
_define_property(this, "deviceClockCalibration", null);
|
|
740
|
+
_define_property(this, "lastFramePtsUs", null);
|
|
741
|
+
_define_property(this, "frameFreshnessError", null);
|
|
742
|
+
_define_property(this, "lastFrameFreshnessWarningAt", 0);
|
|
743
|
+
_define_property(this, "lastTransportBacklogWarningAt", 0);
|
|
505
744
|
this.adb = adb;
|
|
506
745
|
const requestedBitRate = options.videoBitRate ?? DEFAULT_VIDEO_BIT_RATE;
|
|
507
746
|
const clampedBitRate = Math.min(requestedBitRate, MAX_VIDEO_BIT_RATE);
|
|
@@ -736,6 +975,7 @@ const DEFAULT_ADB_SERVER_ENDPOINT = {
|
|
|
736
975
|
class ScrcpyDeviceAdapter {
|
|
737
976
|
isEnabled() {
|
|
738
977
|
if (!this.isConfigured()) return false;
|
|
978
|
+
if (this.freshnessRecoveryPending || this.recoveryPromise) return false;
|
|
739
979
|
return null === this.retryAfter || Date.now() >= this.retryAfter;
|
|
740
980
|
}
|
|
741
981
|
getStatus() {
|
|
@@ -747,12 +987,18 @@ class ScrcpyDeviceAdapter {
|
|
|
747
987
|
};
|
|
748
988
|
}
|
|
749
989
|
isConfigured() {
|
|
750
|
-
return this.scrcpyConfig?.enabled ?? scrcpy_manager.
|
|
990
|
+
return this.scrcpyConfig?.enabled ?? scrcpy_manager.ov.enabled;
|
|
751
991
|
}
|
|
752
992
|
async initialize(deviceInfo) {
|
|
993
|
+
if (this.recoveryPromise) {
|
|
994
|
+
await this.recoveryPromise;
|
|
995
|
+
if (this.manager?.isConnected()) return;
|
|
996
|
+
}
|
|
997
|
+
this.freshnessRecoveryPending = false;
|
|
753
998
|
try {
|
|
754
999
|
const manager = await this.ensureManager(deviceInfo);
|
|
755
1000
|
await manager.ensureConnected();
|
|
1001
|
+
await this.applyPendingActionBarrier(manager);
|
|
756
1002
|
this.clearFailure();
|
|
757
1003
|
} catch (error) {
|
|
758
1004
|
this.recordFailure(error);
|
|
@@ -762,24 +1008,27 @@ class ScrcpyDeviceAdapter {
|
|
|
762
1008
|
recordFailure(error) {
|
|
763
1009
|
this.lastError = error instanceof Error ? error.message : String(error);
|
|
764
1010
|
this.retryAfter = Date.now() + SCRCPY_RETRY_COOLDOWN_MS;
|
|
1011
|
+
this.freshnessRecoveryPending = false;
|
|
765
1012
|
}
|
|
766
1013
|
clearFailure() {
|
|
767
1014
|
this.lastError = null;
|
|
768
1015
|
this.retryAfter = null;
|
|
1016
|
+
this.freshnessRecoveryPending = false;
|
|
769
1017
|
}
|
|
770
1018
|
ensureRetryReady() {
|
|
1019
|
+
if (this.freshnessRecoveryPending || this.recoveryPromise) throw new Error('scrcpy freshness recovery is in progress');
|
|
771
1020
|
if (null === this.retryAfter || Date.now() >= this.retryAfter) return;
|
|
772
1021
|
throw new Error(`scrcpy retry is cooling down until ${new Date(this.retryAfter).toISOString()}. Last error: ${this.lastError}`);
|
|
773
1022
|
}
|
|
774
1023
|
resolveConfig(deviceInfo) {
|
|
775
1024
|
if (this.resolvedConfig) return this.resolvedConfig;
|
|
776
1025
|
const config = this.scrcpyConfig;
|
|
777
|
-
const maxSize = config?.maxSize ?? scrcpy_manager.
|
|
778
|
-
const videoBitRate = config?.videoBitRate ?? scrcpy_manager.
|
|
1026
|
+
const maxSize = config?.maxSize ?? scrcpy_manager.ov.maxSize;
|
|
1027
|
+
const videoBitRate = config?.videoBitRate ?? scrcpy_manager.ov.videoBitRate;
|
|
779
1028
|
this.resolvedConfig = {
|
|
780
1029
|
enabled: this.isConfigured(),
|
|
781
1030
|
maxSize,
|
|
782
|
-
idleTimeoutMs: config?.idleTimeoutMs ?? scrcpy_manager.
|
|
1031
|
+
idleTimeoutMs: config?.idleTimeoutMs ?? scrcpy_manager.ov.idleTimeoutMs,
|
|
783
1032
|
videoBitRate
|
|
784
1033
|
};
|
|
785
1034
|
return this.resolvedConfig;
|
|
@@ -813,24 +1062,40 @@ class ScrcpyDeviceAdapter {
|
|
|
813
1062
|
}
|
|
814
1063
|
async screenshotBase64(deviceInfo) {
|
|
815
1064
|
this.ensureRetryReady();
|
|
1065
|
+
let manager = null;
|
|
816
1066
|
try {
|
|
817
|
-
|
|
1067
|
+
manager = await this.ensureManager(deviceInfo);
|
|
1068
|
+
await manager.ensureConnected();
|
|
1069
|
+
await this.applyPendingActionBarrier(manager);
|
|
818
1070
|
const screenshotBuffer = await manager.getScreenshotJpeg();
|
|
819
1071
|
this.clearFailure();
|
|
820
1072
|
return createImgBase64ByFormat('jpeg', screenshotBuffer.toString('base64'));
|
|
821
1073
|
} catch (error) {
|
|
1074
|
+
if ((0, scrcpy_manager.GJ)(error)) {
|
|
1075
|
+
this.markFreshnessRecoveryPending(manager, error);
|
|
1076
|
+
throw error;
|
|
1077
|
+
}
|
|
822
1078
|
this.recordFailure(error);
|
|
823
1079
|
throw error;
|
|
824
1080
|
}
|
|
825
1081
|
}
|
|
826
1082
|
async subscribeKeyframes(deviceInfo, listener) {
|
|
827
1083
|
this.ensureRetryReady();
|
|
1084
|
+
this.keyframeListeners.add(listener);
|
|
828
1085
|
try {
|
|
829
1086
|
const manager = await this.ensureManager(deviceInfo);
|
|
830
1087
|
await manager.ensureConnected();
|
|
1088
|
+
await this.applyPendingActionBarrier(manager);
|
|
1089
|
+
await manager.ensureFrameClockCalibration();
|
|
831
1090
|
this.clearFailure();
|
|
832
|
-
|
|
1091
|
+
this.attachKeyframeListener(manager, listener);
|
|
1092
|
+
return ()=>{
|
|
1093
|
+
this.keyframeListeners.delete(listener);
|
|
1094
|
+
this.keyframeUnsubscribers.get(listener)?.();
|
|
1095
|
+
this.keyframeUnsubscribers.delete(listener);
|
|
1096
|
+
};
|
|
833
1097
|
} catch (error) {
|
|
1098
|
+
this.keyframeListeners.delete(listener);
|
|
834
1099
|
this.recordFailure(error);
|
|
835
1100
|
throw error;
|
|
836
1101
|
}
|
|
@@ -838,6 +1103,83 @@ class ScrcpyDeviceAdapter {
|
|
|
838
1103
|
getLatestRawKeyframe() {
|
|
839
1104
|
return this.manager?.getLatestRawKeyframe() ?? null;
|
|
840
1105
|
}
|
|
1106
|
+
attachKeyframeListener(manager, listener) {
|
|
1107
|
+
this.keyframeUnsubscribers.get(listener)?.();
|
|
1108
|
+
this.keyframeUnsubscribers.set(listener, manager.subscribeKeyframes(listener));
|
|
1109
|
+
}
|
|
1110
|
+
attachKeyframeListeners(manager) {
|
|
1111
|
+
this.keyframeUnsubscribers.clear();
|
|
1112
|
+
for (const listener of this.keyframeListeners)this.attachKeyframeListener(manager, listener);
|
|
1113
|
+
}
|
|
1114
|
+
markFreshnessRecoveryPending(manager, error) {
|
|
1115
|
+
this.lastError = error.message;
|
|
1116
|
+
this.retryAfter = null;
|
|
1117
|
+
this.freshnessRecoveryPending = true;
|
|
1118
|
+
this.keyframeUnsubscribers.clear();
|
|
1119
|
+
if (manager && this.manager === manager) this.manager = null;
|
|
1120
|
+
}
|
|
1121
|
+
async applyPendingActionBarrier(manager) {
|
|
1122
|
+
if (!this.pendingActionBarrier) return;
|
|
1123
|
+
await manager.setFreshnessBarrier('completed input action while scrcpy was unavailable');
|
|
1124
|
+
this.pendingActionBarrier = false;
|
|
1125
|
+
}
|
|
1126
|
+
recoverAfterAdbScreenshot(deviceInfo) {
|
|
1127
|
+
if (!this.freshnessRecoveryPending || this.recoveryPromise) return;
|
|
1128
|
+
const generation = this.lifecycleGeneration;
|
|
1129
|
+
const recovery = (async ()=>{
|
|
1130
|
+
let manager = null;
|
|
1131
|
+
try {
|
|
1132
|
+
manager = await this.ensureManager(deviceInfo);
|
|
1133
|
+
await manager.ensureConnected();
|
|
1134
|
+
await this.applyPendingActionBarrier(manager);
|
|
1135
|
+
await manager.prepareFreshFrame();
|
|
1136
|
+
if (generation !== this.lifecycleGeneration) {
|
|
1137
|
+
await manager.disconnect();
|
|
1138
|
+
if (this.manager === manager) this.manager = null;
|
|
1139
|
+
return;
|
|
1140
|
+
}
|
|
1141
|
+
this.attachKeyframeListeners(manager);
|
|
1142
|
+
this.clearFailure();
|
|
1143
|
+
debugAdapter('Scrcpy freshness recovery completed in background');
|
|
1144
|
+
} catch (error) {
|
|
1145
|
+
if (manager) {
|
|
1146
|
+
await manager.disconnect();
|
|
1147
|
+
if (this.manager === manager) this.manager = null;
|
|
1148
|
+
}
|
|
1149
|
+
this.freshnessRecoveryPending = false;
|
|
1150
|
+
this.recordFailure(error);
|
|
1151
|
+
debugAdapter(`Scrcpy background freshness recovery failed: ${error}`);
|
|
1152
|
+
throw error;
|
|
1153
|
+
}
|
|
1154
|
+
})();
|
|
1155
|
+
this.recoveryPromise = recovery;
|
|
1156
|
+
recovery.catch(()=>{}).finally(()=>{
|
|
1157
|
+
if (this.recoveryPromise === recovery) this.recoveryPromise = null;
|
|
1158
|
+
});
|
|
1159
|
+
}
|
|
1160
|
+
async markActionBarrier() {
|
|
1161
|
+
const manager = this.manager;
|
|
1162
|
+
if (!manager?.isConnected()) {
|
|
1163
|
+
this.pendingActionBarrier = true;
|
|
1164
|
+
return;
|
|
1165
|
+
}
|
|
1166
|
+
try {
|
|
1167
|
+
await manager.setFreshnessBarrier('completed input action');
|
|
1168
|
+
this.pendingActionBarrier = false;
|
|
1169
|
+
this.clearFailure();
|
|
1170
|
+
} catch (error) {
|
|
1171
|
+
this.pendingActionBarrier = true;
|
|
1172
|
+
this.recordFailure(error);
|
|
1173
|
+
debugAdapter(`Unable to mark scrcpy action barrier; disabling this stream: ${error}`);
|
|
1174
|
+
try {
|
|
1175
|
+
await manager.disconnect();
|
|
1176
|
+
} catch (disconnectError) {
|
|
1177
|
+
debugAdapter(`Error disconnecting scrcpy after barrier failure: ${disconnectError}`);
|
|
1178
|
+
} finally{
|
|
1179
|
+
if (this.manager === manager) this.manager = null;
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
841
1183
|
async decodeRawKeyframeToJpegBase64(frame) {
|
|
842
1184
|
if (!this.manager) throw new Error('scrcpy manager is not initialized');
|
|
843
1185
|
const jpegBuffer = await this.manager.decodeRawKeyframeToJpeg(frame);
|
|
@@ -861,6 +1203,13 @@ class ScrcpyDeviceAdapter {
|
|
|
861
1203
|
return resolution.width / physicalWidth;
|
|
862
1204
|
}
|
|
863
1205
|
async disconnect() {
|
|
1206
|
+
this.lifecycleGeneration += 1;
|
|
1207
|
+
this.freshnessRecoveryPending = false;
|
|
1208
|
+
this.pendingActionBarrier = false;
|
|
1209
|
+
for (const unsubscribe of this.keyframeUnsubscribers.values())unsubscribe();
|
|
1210
|
+
this.keyframeUnsubscribers.clear();
|
|
1211
|
+
this.keyframeListeners.clear();
|
|
1212
|
+
if (this.recoveryPromise) await this.recoveryPromise.catch(()=>{});
|
|
864
1213
|
if (this.manager) {
|
|
865
1214
|
try {
|
|
866
1215
|
await this.manager.disconnect();
|
|
@@ -880,6 +1229,12 @@ class ScrcpyDeviceAdapter {
|
|
|
880
1229
|
_define_property(this, "resolvedConfig", void 0);
|
|
881
1230
|
_define_property(this, "lastError", void 0);
|
|
882
1231
|
_define_property(this, "retryAfter", void 0);
|
|
1232
|
+
_define_property(this, "freshnessRecoveryPending", void 0);
|
|
1233
|
+
_define_property(this, "recoveryPromise", void 0);
|
|
1234
|
+
_define_property(this, "lifecycleGeneration", void 0);
|
|
1235
|
+
_define_property(this, "pendingActionBarrier", void 0);
|
|
1236
|
+
_define_property(this, "keyframeListeners", void 0);
|
|
1237
|
+
_define_property(this, "keyframeUnsubscribers", void 0);
|
|
883
1238
|
this.deviceId = deviceId;
|
|
884
1239
|
this.scrcpyConfig = scrcpyConfig;
|
|
885
1240
|
this.resolveAdbServerEndpoint = resolveAdbServerEndpoint;
|
|
@@ -887,6 +1242,12 @@ class ScrcpyDeviceAdapter {
|
|
|
887
1242
|
this.resolvedConfig = null;
|
|
888
1243
|
this.lastError = null;
|
|
889
1244
|
this.retryAfter = null;
|
|
1245
|
+
this.freshnessRecoveryPending = false;
|
|
1246
|
+
this.recoveryPromise = null;
|
|
1247
|
+
this.lifecycleGeneration = 0;
|
|
1248
|
+
this.pendingActionBarrier = false;
|
|
1249
|
+
this.keyframeListeners = new Set();
|
|
1250
|
+
this.keyframeUnsubscribers = new Map();
|
|
890
1251
|
}
|
|
891
1252
|
}
|
|
892
1253
|
const TAG_RE = /<(\/?)([A-Za-z_][A-Za-z0-9_.\-:]*)([^>]*?)(\/?)>/g;
|
|
@@ -1113,6 +1474,20 @@ async function captureAndroidUITree(options) {
|
|
|
1113
1474
|
}
|
|
1114
1475
|
throw new Error(`Unable to capture Android UI tree after ${failures.length} attempt(s): ${failures.join('; ')}`);
|
|
1115
1476
|
}
|
|
1477
|
+
function createVisualActionRegistry(definitions, onActionSettled) {
|
|
1478
|
+
const registeredActions = {};
|
|
1479
|
+
for (const actionName of Object.keys(definitions)){
|
|
1480
|
+
const dispatch = definitions[actionName];
|
|
1481
|
+
registeredActions[actionName] = async (...args)=>{
|
|
1482
|
+
try {
|
|
1483
|
+
return await dispatch(...args);
|
|
1484
|
+
} finally{
|
|
1485
|
+
await onActionSettled(actionName);
|
|
1486
|
+
}
|
|
1487
|
+
};
|
|
1488
|
+
}
|
|
1489
|
+
return registeredActions;
|
|
1490
|
+
}
|
|
1116
1491
|
function device_define_property(obj, key, value) {
|
|
1117
1492
|
if (key in obj) Object.defineProperty(obj, key, {
|
|
1118
1493
|
value: value,
|
|
@@ -1213,19 +1588,12 @@ class AndroidDevice {
|
|
|
1213
1588
|
}
|
|
1214
1589
|
},
|
|
1215
1590
|
call: async (param)=>{
|
|
1216
|
-
const element = param.locate;
|
|
1217
|
-
const startPoint = element ? {
|
|
1218
|
-
left: element.center[0],
|
|
1219
|
-
top: element.center[1]
|
|
1220
|
-
} : void 0;
|
|
1221
1591
|
if (!param || !param.direction) throw new Error('PullGesture requires a direction parameter');
|
|
1222
|
-
|
|
1223
|
-
else if ('up' === param.direction) await this.pullUp(startPoint, param.distance, param.duration);
|
|
1224
|
-
else throw new Error(`Unknown pull direction: ${param.direction}`);
|
|
1592
|
+
await this.visualActions.pullGesture(param);
|
|
1225
1593
|
}
|
|
1226
1594
|
})
|
|
1227
1595
|
];
|
|
1228
|
-
const platformActions = createPlatformActions(this);
|
|
1596
|
+
const platformActions = createPlatformActions(this.visualActions);
|
|
1229
1597
|
let platformSpecificActions = Object.values(platformActions);
|
|
1230
1598
|
if (this.options?.exposeRunAdbShellAction === false) platformSpecificActions = platformSpecificActions.filter((action)=>action !== platformActions.RunAdbShell);
|
|
1231
1599
|
const customActions = this.customActions || [];
|
|
@@ -1235,6 +1603,16 @@ class AndroidDevice {
|
|
|
1235
1603
|
...customActions
|
|
1236
1604
|
];
|
|
1237
1605
|
}
|
|
1606
|
+
async performPullGesture(param) {
|
|
1607
|
+
const element = param.locate;
|
|
1608
|
+
const startPoint = element ? {
|
|
1609
|
+
left: element.center[0],
|
|
1610
|
+
top: element.center[1]
|
|
1611
|
+
} : void 0;
|
|
1612
|
+
if ('down' === param.direction) await this.pullDownRaw(startPoint, param.distance, param.duration);
|
|
1613
|
+
else if ('up' === param.direction) await this.pullUpRaw(startPoint, param.distance, param.duration);
|
|
1614
|
+
else throw new Error(`Unknown pull direction: ${param.direction}`);
|
|
1615
|
+
}
|
|
1238
1616
|
async performActionScroll(param) {
|
|
1239
1617
|
const element = param.locate;
|
|
1240
1618
|
const startingPoint = element ? {
|
|
@@ -1242,20 +1620,32 @@ class AndroidDevice {
|
|
|
1242
1620
|
top: element.center[1]
|
|
1243
1621
|
} : void 0;
|
|
1244
1622
|
const scrollToEventName = param?.scrollType;
|
|
1245
|
-
if ('scrollToTop' === scrollToEventName) await this.
|
|
1246
|
-
else if ('scrollToBottom' === scrollToEventName) await this.
|
|
1247
|
-
else if ('scrollToRight' === scrollToEventName) await this.
|
|
1248
|
-
else if ('scrollToLeft' === scrollToEventName) await this.
|
|
1623
|
+
if ('scrollToTop' === scrollToEventName) await this.scrollUntilTopRaw(startingPoint);
|
|
1624
|
+
else if ('scrollToBottom' === scrollToEventName) await this.scrollUntilBottomRaw(startingPoint);
|
|
1625
|
+
else if ('scrollToRight' === scrollToEventName) await this.scrollUntilRightRaw(startingPoint);
|
|
1626
|
+
else if ('scrollToLeft' === scrollToEventName) await this.scrollUntilLeftRaw(startingPoint);
|
|
1249
1627
|
else if ('singleAction' !== scrollToEventName && scrollToEventName) throw new Error(`Unknown scroll event type: ${scrollToEventName}, param: ${JSON.stringify(param)}`);
|
|
1250
1628
|
else {
|
|
1251
|
-
if (param?.direction !== 'down' && param && param.direction) if ('up' === param.direction) await this.
|
|
1252
|
-
else if ('left' === param.direction) await this.
|
|
1253
|
-
else if ('right' === param.direction) await this.
|
|
1629
|
+
if (param?.direction !== 'down' && param && param.direction) if ('up' === param.direction) await this.scrollUpRaw(param.distance || void 0, startingPoint);
|
|
1630
|
+
else if ('left' === param.direction) await this.scrollLeftRaw(param.distance || void 0, startingPoint);
|
|
1631
|
+
else if ('right' === param.direction) await this.scrollRightRaw(param.distance || void 0, startingPoint);
|
|
1254
1632
|
else throw new Error(`Unknown scroll direction: ${param.direction}`);
|
|
1255
|
-
else await this.
|
|
1633
|
+
else await this.scrollDownRaw(param?.distance || void 0, startingPoint);
|
|
1256
1634
|
await sleep(500);
|
|
1257
1635
|
}
|
|
1258
1636
|
}
|
|
1637
|
+
async runAdbShellRaw(param, context) {
|
|
1638
|
+
const adb = await this.getAdb();
|
|
1639
|
+
const stdout = await runAdbShellStdoutOrThrow(adb, param.command, void 0 === param.timeout ? void 0 : {
|
|
1640
|
+
timeout: param.timeout
|
|
1641
|
+
});
|
|
1642
|
+
const planningFeedback = buildRunAdbShellPlanningFeedback({
|
|
1643
|
+
command: param.command,
|
|
1644
|
+
stdout
|
|
1645
|
+
});
|
|
1646
|
+
if (planningFeedback && context?.task) context.task.planningFeedback = planningFeedback;
|
|
1647
|
+
return stdout;
|
|
1648
|
+
}
|
|
1259
1649
|
describe() {
|
|
1260
1650
|
return this.description || `DeviceId: ${this.deviceId}`;
|
|
1261
1651
|
}
|
|
@@ -1352,15 +1742,15 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1352
1742
|
const adapter = this.getScrcpyAdapter();
|
|
1353
1743
|
if (!adapter.isEnabled()) throw new Error('scrcpy is not available for frame observation');
|
|
1354
1744
|
const deviceInfo = await this.getDevicePhysicalInfo();
|
|
1355
|
-
|
|
1356
|
-
const unsubscribe = await adapter.subscribeKeyframes(deviceInfo, (frame)=>{
|
|
1357
|
-
latest = frame;
|
|
1358
|
-
});
|
|
1745
|
+
const unsubscribe = await adapter.subscribeKeyframes(deviceInfo, ()=>{});
|
|
1359
1746
|
return {
|
|
1360
|
-
latest: ()=>
|
|
1747
|
+
latest: ()=>{
|
|
1748
|
+
const latest = adapter.getLatestRawKeyframe();
|
|
1749
|
+
return latest ? {
|
|
1361
1750
|
ref: latest,
|
|
1362
1751
|
capturedAt: latest.capturedAt
|
|
1363
|
-
} : null
|
|
1752
|
+
} : null;
|
|
1753
|
+
},
|
|
1364
1754
|
decode: async (refs)=>{
|
|
1365
1755
|
const images = [];
|
|
1366
1756
|
for (const frameRef of refs)images.push(await adapter.decodeRawKeyframeToJpegBase64(frameRef.ref));
|
|
@@ -1394,6 +1784,9 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1394
1784
|
return this.appNameMapping[normalizedAppName];
|
|
1395
1785
|
}
|
|
1396
1786
|
async launch(uri) {
|
|
1787
|
+
return this.visualActions.launch(uri);
|
|
1788
|
+
}
|
|
1789
|
+
async launchRaw(uri) {
|
|
1397
1790
|
const adb = await this.getAdb();
|
|
1398
1791
|
this.uri = uri;
|
|
1399
1792
|
try {
|
|
@@ -1419,6 +1812,9 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1419
1812
|
return this;
|
|
1420
1813
|
}
|
|
1421
1814
|
async terminate(uri) {
|
|
1815
|
+
await this.visualActions.terminate(uri);
|
|
1816
|
+
}
|
|
1817
|
+
async terminateRaw(uri) {
|
|
1422
1818
|
const packagePart = uri.includes('/') ? uri.split('/')[0] : uri;
|
|
1423
1819
|
const resolved = this.resolvePackageName(packagePart) ?? packagePart;
|
|
1424
1820
|
const adb = await this.getAdb();
|
|
@@ -1434,6 +1830,9 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1434
1830
|
}
|
|
1435
1831
|
}
|
|
1436
1832
|
async execYadb(keyboardContent) {
|
|
1833
|
+
await this.visualActions.execYadb(keyboardContent);
|
|
1834
|
+
}
|
|
1835
|
+
async execYadbRaw(keyboardContent) {
|
|
1437
1836
|
this.warnYadbOnNonDefaultDisplay('keyboard input');
|
|
1438
1837
|
await this.ensureYadb();
|
|
1439
1838
|
const adb = await this.getAdb();
|
|
@@ -1690,14 +2089,15 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1690
2089
|
async screenshotBase64() {
|
|
1691
2090
|
debugDevice('screenshotBase64 begin');
|
|
1692
2091
|
const adapter = this.getScrcpyAdapter();
|
|
2092
|
+
let scrcpyDeviceInfo = null;
|
|
1693
2093
|
if (adapter.isEnabled()) try {
|
|
1694
2094
|
debugDevice('Attempting scrcpy screenshot...');
|
|
1695
|
-
|
|
1696
|
-
const result = await adapter.screenshotBase64(
|
|
2095
|
+
scrcpyDeviceInfo = await this.getDevicePhysicalInfo();
|
|
2096
|
+
const result = await adapter.screenshotBase64(scrcpyDeviceInfo);
|
|
1697
2097
|
debugDevice('screenshotBase64 end (scrcpy mode)');
|
|
1698
2098
|
return result;
|
|
1699
2099
|
} catch (error) {
|
|
1700
|
-
warnDevice(`Scrcpy screenshot failed, falling back to standard ADB method
|
|
2100
|
+
warnDevice(`Scrcpy screenshot failed, falling back to standard ADB method. This may be caused by transport backlog. ${scrcpy_manager.Xr}\nError: ${error}`);
|
|
1701
2101
|
}
|
|
1702
2102
|
const adb = await this.getAdb();
|
|
1703
2103
|
let screenshotBuffer;
|
|
@@ -1775,9 +2175,13 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1775
2175
|
});
|
|
1776
2176
|
}
|
|
1777
2177
|
debugDevice('screenshotBase64 end');
|
|
2178
|
+
if (scrcpyDeviceInfo) adapter.recoverAfterAdbScreenshot(scrcpyDeviceInfo);
|
|
1778
2179
|
return result;
|
|
1779
2180
|
}
|
|
1780
2181
|
async clearInput(element) {
|
|
2182
|
+
await this.visualActions.clearInput(element);
|
|
2183
|
+
}
|
|
2184
|
+
async clearInputRaw(element) {
|
|
1781
2185
|
if (element) await this.tapPoint({
|
|
1782
2186
|
x: element.center[0],
|
|
1783
2187
|
y: element.center[1]
|
|
@@ -1811,6 +2215,9 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1811
2215
|
return '';
|
|
1812
2216
|
}
|
|
1813
2217
|
async scrollUntilTop(startPoint) {
|
|
2218
|
+
await this.visualActions.scrollUntilTop(startPoint);
|
|
2219
|
+
}
|
|
2220
|
+
async scrollUntilTopRaw(startPoint) {
|
|
1814
2221
|
if (startPoint) {
|
|
1815
2222
|
const { height } = await this.size();
|
|
1816
2223
|
const start = {
|
|
@@ -1825,10 +2232,13 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1825
2232
|
await sleep(1000);
|
|
1826
2233
|
return;
|
|
1827
2234
|
}
|
|
1828
|
-
await repeat(defaultScrollUntilTimes, ()=>this.
|
|
2235
|
+
await repeat(defaultScrollUntilTimes, ()=>this.scrollRaw(0, -9999999, defaultFastScrollDuration));
|
|
1829
2236
|
await sleep(1000);
|
|
1830
2237
|
}
|
|
1831
2238
|
async scrollUntilBottom(startPoint) {
|
|
2239
|
+
await this.visualActions.scrollUntilBottom(startPoint);
|
|
2240
|
+
}
|
|
2241
|
+
async scrollUntilBottomRaw(startPoint) {
|
|
1832
2242
|
if (startPoint) {
|
|
1833
2243
|
const start = {
|
|
1834
2244
|
x: Math.round(startPoint.left),
|
|
@@ -1842,10 +2252,13 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1842
2252
|
await sleep(1000);
|
|
1843
2253
|
return;
|
|
1844
2254
|
}
|
|
1845
|
-
await repeat(defaultScrollUntilTimes, ()=>this.
|
|
2255
|
+
await repeat(defaultScrollUntilTimes, ()=>this.scrollRaw(0, 9999999, defaultFastScrollDuration));
|
|
1846
2256
|
await sleep(1000);
|
|
1847
2257
|
}
|
|
1848
2258
|
async scrollUntilLeft(startPoint) {
|
|
2259
|
+
await this.visualActions.scrollUntilLeft(startPoint);
|
|
2260
|
+
}
|
|
2261
|
+
async scrollUntilLeftRaw(startPoint) {
|
|
1849
2262
|
if (startPoint) {
|
|
1850
2263
|
const { width } = await this.size();
|
|
1851
2264
|
const start = {
|
|
@@ -1860,10 +2273,13 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1860
2273
|
await sleep(1000);
|
|
1861
2274
|
return;
|
|
1862
2275
|
}
|
|
1863
|
-
await repeat(defaultScrollUntilTimes, ()=>this.
|
|
2276
|
+
await repeat(defaultScrollUntilTimes, ()=>this.scrollRaw(-9999999, 0, defaultFastScrollDuration));
|
|
1864
2277
|
await sleep(1000);
|
|
1865
2278
|
}
|
|
1866
2279
|
async scrollUntilRight(startPoint) {
|
|
2280
|
+
await this.visualActions.scrollUntilRight(startPoint);
|
|
2281
|
+
}
|
|
2282
|
+
async scrollUntilRightRaw(startPoint) {
|
|
1867
2283
|
if (startPoint) {
|
|
1868
2284
|
const start = {
|
|
1869
2285
|
x: Math.round(startPoint.left),
|
|
@@ -1877,10 +2293,13 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1877
2293
|
await sleep(1000);
|
|
1878
2294
|
return;
|
|
1879
2295
|
}
|
|
1880
|
-
await repeat(defaultScrollUntilTimes, ()=>this.
|
|
2296
|
+
await repeat(defaultScrollUntilTimes, ()=>this.scrollRaw(9999999, 0, defaultFastScrollDuration));
|
|
1881
2297
|
await sleep(1000);
|
|
1882
2298
|
}
|
|
1883
2299
|
async scrollUp(distance, startPoint) {
|
|
2300
|
+
await this.visualActions.scrollUp(distance, startPoint);
|
|
2301
|
+
}
|
|
2302
|
+
async scrollUpRaw(distance, startPoint) {
|
|
1884
2303
|
const { height } = await this.size();
|
|
1885
2304
|
const scrollDistance = Math.round(distance || height);
|
|
1886
2305
|
const hasExplicitDistance = void 0 !== distance;
|
|
@@ -1894,9 +2313,12 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1894
2313
|
await this.dragPoint(start, end);
|
|
1895
2314
|
return;
|
|
1896
2315
|
}
|
|
1897
|
-
await this.
|
|
2316
|
+
await this.scrollRaw(0, -scrollDistance, void 0, hasExplicitDistance, 'up');
|
|
1898
2317
|
}
|
|
1899
2318
|
async scrollDown(distance, startPoint) {
|
|
2319
|
+
await this.visualActions.scrollDown(distance, startPoint);
|
|
2320
|
+
}
|
|
2321
|
+
async scrollDownRaw(distance, startPoint) {
|
|
1900
2322
|
const { height } = await this.size();
|
|
1901
2323
|
const scrollDistance = Math.round(distance || height);
|
|
1902
2324
|
const hasExplicitDistance = void 0 !== distance;
|
|
@@ -1910,9 +2332,12 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1910
2332
|
await this.dragPoint(start, end);
|
|
1911
2333
|
return;
|
|
1912
2334
|
}
|
|
1913
|
-
await this.
|
|
2335
|
+
await this.scrollRaw(0, scrollDistance, void 0, hasExplicitDistance, 'down');
|
|
1914
2336
|
}
|
|
1915
2337
|
async scrollLeft(distance, startPoint) {
|
|
2338
|
+
await this.visualActions.scrollLeft(distance, startPoint);
|
|
2339
|
+
}
|
|
2340
|
+
async scrollLeftRaw(distance, startPoint) {
|
|
1916
2341
|
const { width } = await this.size();
|
|
1917
2342
|
const scrollDistance = Math.round(distance || width);
|
|
1918
2343
|
const hasExplicitDistance = void 0 !== distance;
|
|
@@ -1926,9 +2351,12 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1926
2351
|
await this.dragPoint(start, end);
|
|
1927
2352
|
return;
|
|
1928
2353
|
}
|
|
1929
|
-
await this.
|
|
2354
|
+
await this.scrollRaw(-scrollDistance, 0, void 0, hasExplicitDistance, 'left');
|
|
1930
2355
|
}
|
|
1931
2356
|
async scrollRight(distance, startPoint) {
|
|
2357
|
+
await this.visualActions.scrollRight(distance, startPoint);
|
|
2358
|
+
}
|
|
2359
|
+
async scrollRightRaw(distance, startPoint) {
|
|
1932
2360
|
const { width } = await this.size();
|
|
1933
2361
|
const scrollDistance = Math.round(distance || width);
|
|
1934
2362
|
const hasExplicitDistance = void 0 !== distance;
|
|
@@ -1942,7 +2370,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1942
2370
|
await this.dragPoint(start, end);
|
|
1943
2371
|
return;
|
|
1944
2372
|
}
|
|
1945
|
-
await this.
|
|
2373
|
+
await this.scrollRaw(scrollDistance, 0, void 0, hasExplicitDistance, 'right');
|
|
1946
2374
|
}
|
|
1947
2375
|
async ensureYadb() {
|
|
1948
2376
|
if (!this.yadbPushed) {
|
|
@@ -1971,11 +2399,11 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1971
2399
|
const reason = IME_STRATEGY === IME_STRATEGY_ALWAYS_YADB ? "imeStrategy 'always-yadb' requires yadb" : 'text contains characters that require yadb (non-ASCII, format specifiers, or mixed quotes)';
|
|
1972
2400
|
throw new Error(`${reason}, but yadb (app_process) cannot target non-default displayId=${this.options?.displayId}. Use displayId=0 or a different imeStrategy.`);
|
|
1973
2401
|
}
|
|
1974
|
-
if (needsYadb) await this.
|
|
2402
|
+
if (needsYadb) await this.execYadbRaw(escapeForShell(text));
|
|
1975
2403
|
else await this.shellInputText(text, {
|
|
1976
2404
|
keyboardTypeDelay: typeDelay
|
|
1977
2405
|
});
|
|
1978
|
-
if (true === shouldAutoDismissKeyboard) await this.
|
|
2406
|
+
if (true === shouldAutoDismissKeyboard) await this.hideKeyboardRaw(options);
|
|
1979
2407
|
}
|
|
1980
2408
|
normalizeKeyName(key) {
|
|
1981
2409
|
const keyMap = {
|
|
@@ -2045,6 +2473,9 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
2045
2473
|
await adb.shell(`input${this.getDisplayArg()} swipe ${fromX} ${fromY} ${toX} ${toY} ${duration}`);
|
|
2046
2474
|
}
|
|
2047
2475
|
async scroll(deltaX, deltaY, duration, warnOnClamp = false, direction) {
|
|
2476
|
+
await this.visualActions.scroll(deltaX, deltaY, duration, warnOnClamp, direction);
|
|
2477
|
+
}
|
|
2478
|
+
async scrollRaw(deltaX, deltaY, duration, warnOnClamp = false, direction) {
|
|
2048
2479
|
if (0 === deltaX && 0 === deltaY) throw new Error('Scroll distance cannot be zero in both directions');
|
|
2049
2480
|
const { width, height } = await this.size();
|
|
2050
2481
|
const n = 4;
|
|
@@ -2109,12 +2540,21 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
2109
2540
|
}
|
|
2110
2541
|
}
|
|
2111
2542
|
async back() {
|
|
2543
|
+
await this.visualActions.back();
|
|
2544
|
+
}
|
|
2545
|
+
async backRaw() {
|
|
2112
2546
|
await this.shellInputKeyevent(4);
|
|
2113
2547
|
}
|
|
2114
2548
|
async home() {
|
|
2549
|
+
await this.visualActions.home();
|
|
2550
|
+
}
|
|
2551
|
+
async homeRaw() {
|
|
2115
2552
|
await this.shellInputKeyevent(3);
|
|
2116
2553
|
}
|
|
2117
2554
|
async recentApps() {
|
|
2555
|
+
await this.visualActions.recentApps();
|
|
2556
|
+
}
|
|
2557
|
+
async recentAppsRaw() {
|
|
2118
2558
|
await this.shellInputKeyevent(187);
|
|
2119
2559
|
}
|
|
2120
2560
|
async longPressPoint(point, duration = 2000) {
|
|
@@ -2123,6 +2563,9 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
2123
2563
|
await adb.shell(`input${this.getDisplayArg()} swipe ${adjustedX} ${adjustedY} ${adjustedX} ${adjustedY} ${duration}`);
|
|
2124
2564
|
}
|
|
2125
2565
|
async pullDown(startPoint, distance, duration = 800) {
|
|
2566
|
+
await this.visualActions.pullDown(startPoint, distance, duration);
|
|
2567
|
+
}
|
|
2568
|
+
async pullDownRaw(startPoint, distance, duration = 800) {
|
|
2126
2569
|
const { width, height } = await this.size();
|
|
2127
2570
|
const start = startPoint ? {
|
|
2128
2571
|
x: Math.round(startPoint.left),
|
|
@@ -2136,13 +2579,19 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
2136
2579
|
x: start.x,
|
|
2137
2580
|
y: start.y + pullDistance
|
|
2138
2581
|
};
|
|
2139
|
-
await this.
|
|
2582
|
+
await this.pullDragRaw(start, end, duration);
|
|
2140
2583
|
await sleep(200);
|
|
2141
2584
|
}
|
|
2142
2585
|
async pullDrag(from, to, duration) {
|
|
2586
|
+
await this.visualActions.pullDrag(from, to, duration);
|
|
2587
|
+
}
|
|
2588
|
+
async pullDragRaw(from, to, duration) {
|
|
2143
2589
|
await this.swipePoint(from, to, duration);
|
|
2144
2590
|
}
|
|
2145
2591
|
async pullUp(startPoint, distance, duration = 600) {
|
|
2592
|
+
await this.visualActions.pullUp(startPoint, distance, duration);
|
|
2593
|
+
}
|
|
2594
|
+
async pullUpRaw(startPoint, distance, duration = 600) {
|
|
2146
2595
|
const { width, height } = await this.size();
|
|
2147
2596
|
const start = startPoint ? {
|
|
2148
2597
|
x: Math.round(startPoint.left),
|
|
@@ -2156,7 +2605,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
2156
2605
|
x: start.x,
|
|
2157
2606
|
y: start.y - pullDistance
|
|
2158
2607
|
};
|
|
2159
|
-
await this.
|
|
2608
|
+
await this.pullDragRaw(start, end, duration);
|
|
2160
2609
|
await sleep(100);
|
|
2161
2610
|
}
|
|
2162
2611
|
getDisplayArg() {
|
|
@@ -2219,6 +2668,9 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
2219
2668
|
}
|
|
2220
2669
|
}
|
|
2221
2670
|
async hideKeyboard(options, timeoutMs = 1000) {
|
|
2671
|
+
return this.visualActions.hideKeyboard(options, timeoutMs);
|
|
2672
|
+
}
|
|
2673
|
+
async hideKeyboardRaw(options, timeoutMs = 1000) {
|
|
2222
2674
|
const adb = await this.getAdb();
|
|
2223
2675
|
const keyboardDismissStrategy = options?.keyboardDismissStrategy ?? this.options?.keyboardDismissStrategy ?? 'esc-first';
|
|
2224
2676
|
const keyboardStatus = await adb.isSoftKeyboardPresent();
|
|
@@ -2273,55 +2725,91 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
2273
2725
|
device_define_property(this, "interfaceType", 'android');
|
|
2274
2726
|
device_define_property(this, "uri", void 0);
|
|
2275
2727
|
device_define_property(this, "options", void 0);
|
|
2728
|
+
device_define_property(this, "visualActions", createVisualActionRegistry({
|
|
2729
|
+
tap: (point)=>this.tapPoint(point),
|
|
2730
|
+
doubleClick: (point)=>this.doubleTapPoint(point),
|
|
2731
|
+
longPress: (point, opts)=>this.longPressPoint(point, opts?.duration),
|
|
2732
|
+
dragAndDrop: (from, to)=>this.dragPoint(from, to),
|
|
2733
|
+
keyboardPress: (keyName)=>this.pressKey(keyName),
|
|
2734
|
+
typeText: async (value, opts)=>{
|
|
2735
|
+
const target = opts?.target;
|
|
2736
|
+
if (target && opts?.replace !== false) await this.clearInputRaw(target);
|
|
2737
|
+
else if (target) await this.tapPoint({
|
|
2738
|
+
x: target.center[0],
|
|
2739
|
+
y: target.center[1]
|
|
2740
|
+
});
|
|
2741
|
+
if (opts?.focusOnly) return;
|
|
2742
|
+
await this.typeText(value, opts);
|
|
2743
|
+
},
|
|
2744
|
+
clearInput: (target)=>this.clearInputRaw(target),
|
|
2745
|
+
cursorMove: async (direction, times = 1)=>{
|
|
2746
|
+
const arrowKey = 'left' === direction ? 'ArrowLeft' : 'ArrowRight';
|
|
2747
|
+
for(let index = 0; index < times; index++)await this.pressKey(arrowKey);
|
|
2748
|
+
},
|
|
2749
|
+
swipe: async (start, end, opts)=>{
|
|
2750
|
+
const duration = opts?.duration ?? 300;
|
|
2751
|
+
const repeatCount = opts?.repeat ?? 1;
|
|
2752
|
+
for(let index = 0; index < repeatCount; index++)await this.dragPoint(start, end, duration);
|
|
2753
|
+
},
|
|
2754
|
+
pinch: async (center, opts)=>{
|
|
2755
|
+
if ('number' == typeof this.options?.displayId && 0 !== this.options.displayId) throw new Error(`Pinch is not supported on a non-default display (displayId=${this.options.displayId}). The underlying yadb tool only injects gestures into the default display.`);
|
|
2756
|
+
const { x: adjCenterX, y: adjCenterY } = await this.adjustCoordinates(Math.round(center.x), Math.round(center.y));
|
|
2757
|
+
const ratio = 0 !== adjCenterX && 0 !== center.x ? adjCenterX / center.x : 1;
|
|
2758
|
+
const adjStartDist = Math.round(opts.startDistance * ratio);
|
|
2759
|
+
const adjEndDist = Math.round(opts.endDistance * ratio);
|
|
2760
|
+
await this.ensureYadb();
|
|
2761
|
+
const adb = await this.getAdb();
|
|
2762
|
+
await adb.shell(`app_process -Djava.class.path=/data/local/tmp/yadb /data/local/tmp com.ysbing.yadb.Main -pinch ${adjCenterX} ${adjCenterY} ${adjStartDist} ${adjEndDist} ${opts.duration}`);
|
|
2763
|
+
},
|
|
2764
|
+
actionScroll: (param)=>this.performActionScroll(param),
|
|
2765
|
+
back: ()=>this.backRaw(),
|
|
2766
|
+
home: ()=>this.homeRaw(),
|
|
2767
|
+
recentApps: ()=>this.recentAppsRaw(),
|
|
2768
|
+
pullGesture: (param)=>this.performPullGesture(param),
|
|
2769
|
+
launch: (uri)=>this.launchRaw(uri),
|
|
2770
|
+
terminate: (uri)=>this.terminateRaw(uri),
|
|
2771
|
+
runAdbShell: (param, context)=>this.runAdbShellRaw(param, context),
|
|
2772
|
+
execYadb: (keyboardContent)=>this.execYadbRaw(keyboardContent),
|
|
2773
|
+
scrollUntilTop: (startPoint)=>this.scrollUntilTopRaw(startPoint),
|
|
2774
|
+
scrollUntilBottom: (startPoint)=>this.scrollUntilBottomRaw(startPoint),
|
|
2775
|
+
scrollUntilLeft: (startPoint)=>this.scrollUntilLeftRaw(startPoint),
|
|
2776
|
+
scrollUntilRight: (startPoint)=>this.scrollUntilRightRaw(startPoint),
|
|
2777
|
+
scrollUp: (distance, startPoint)=>this.scrollUpRaw(distance, startPoint),
|
|
2778
|
+
scrollDown: (distance, startPoint)=>this.scrollDownRaw(distance, startPoint),
|
|
2779
|
+
scrollLeft: (distance, startPoint)=>this.scrollLeftRaw(distance, startPoint),
|
|
2780
|
+
scrollRight: (distance, startPoint)=>this.scrollRightRaw(distance, startPoint),
|
|
2781
|
+
scroll: (deltaX, deltaY, duration, warnOnClamp, direction)=>this.scrollRaw(deltaX, deltaY, duration, warnOnClamp, direction),
|
|
2782
|
+
pullDown: (startPoint, distance, duration)=>this.pullDownRaw(startPoint, distance, duration),
|
|
2783
|
+
pullDrag: (from, to, duration)=>this.pullDragRaw(from, to, duration),
|
|
2784
|
+
pullUp: (startPoint, distance, duration)=>this.pullUpRaw(startPoint, distance, duration),
|
|
2785
|
+
hideKeyboard: (options, timeoutMs)=>this.hideKeyboardRaw(options, timeoutMs)
|
|
2786
|
+
}, async ()=>{
|
|
2787
|
+
await this.scrcpyAdapter?.markActionBarrier();
|
|
2788
|
+
}));
|
|
2276
2789
|
device_define_property(this, "inputPrimitives", {
|
|
2277
2790
|
pointer: {
|
|
2278
|
-
tap:
|
|
2279
|
-
doubleClick:
|
|
2280
|
-
longPress:
|
|
2281
|
-
dragAndDrop:
|
|
2791
|
+
tap: this.visualActions.tap,
|
|
2792
|
+
doubleClick: this.visualActions.doubleClick,
|
|
2793
|
+
longPress: this.visualActions.longPress,
|
|
2794
|
+
dragAndDrop: this.visualActions.dragAndDrop
|
|
2282
2795
|
},
|
|
2283
2796
|
keyboard: {
|
|
2284
|
-
keyboardPress:
|
|
2285
|
-
typeText:
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
else if (target) await this.tapPoint({
|
|
2289
|
-
x: target.center[0],
|
|
2290
|
-
y: target.center[1]
|
|
2291
|
-
});
|
|
2292
|
-
if (opts?.focusOnly) return;
|
|
2293
|
-
await this.typeText(value, opts);
|
|
2294
|
-
},
|
|
2295
|
-
clearInput: (target)=>this.clearInput(target),
|
|
2296
|
-
cursorMove: async (direction, times = 1)=>{
|
|
2297
|
-
const arrowKey = 'left' === direction ? 'ArrowLeft' : 'ArrowRight';
|
|
2298
|
-
for(let i = 0; i < times; i++)await this.pressKey(arrowKey);
|
|
2299
|
-
}
|
|
2797
|
+
keyboardPress: this.visualActions.keyboardPress,
|
|
2798
|
+
typeText: this.visualActions.typeText,
|
|
2799
|
+
clearInput: this.visualActions.clearInput,
|
|
2800
|
+
cursorMove: this.visualActions.cursorMove
|
|
2300
2801
|
},
|
|
2301
2802
|
touch: {
|
|
2302
|
-
swipe:
|
|
2303
|
-
|
|
2304
|
-
const repeatCount = opts?.repeat ?? 1;
|
|
2305
|
-
for(let i = 0; i < repeatCount; i++)await this.dragPoint(start, end, duration);
|
|
2306
|
-
},
|
|
2307
|
-
pinch: async (center, opts)=>{
|
|
2308
|
-
if ('number' == typeof this.options?.displayId && 0 !== this.options.displayId) throw new Error(`Pinch is not supported on a non-default display (displayId=${this.options.displayId}). The underlying yadb tool only injects gestures into the default display.`);
|
|
2309
|
-
const { x: adjCenterX, y: adjCenterY } = await this.adjustCoordinates(Math.round(center.x), Math.round(center.y));
|
|
2310
|
-
const ratio = 0 !== adjCenterX && 0 !== center.x ? adjCenterX / center.x : 1;
|
|
2311
|
-
const adjStartDist = Math.round(opts.startDistance * ratio);
|
|
2312
|
-
const adjEndDist = Math.round(opts.endDistance * ratio);
|
|
2313
|
-
await this.ensureYadb();
|
|
2314
|
-
const adb = await this.getAdb();
|
|
2315
|
-
await adb.shell(`app_process -Djava.class.path=/data/local/tmp/yadb /data/local/tmp com.ysbing.yadb.Main -pinch ${adjCenterX} ${adjCenterY} ${adjStartDist} ${adjEndDist} ${opts.duration}`);
|
|
2316
|
-
}
|
|
2803
|
+
swipe: this.visualActions.swipe,
|
|
2804
|
+
pinch: this.visualActions.pinch
|
|
2317
2805
|
},
|
|
2318
2806
|
scroll: {
|
|
2319
|
-
scroll:
|
|
2807
|
+
scroll: this.visualActions.actionScroll
|
|
2320
2808
|
},
|
|
2321
2809
|
system: {
|
|
2322
|
-
backButton:
|
|
2323
|
-
homeButton:
|
|
2324
|
-
recentAppsButton:
|
|
2810
|
+
backButton: this.visualActions.back,
|
|
2811
|
+
homeButton: this.visualActions.home,
|
|
2812
|
+
recentAppsButton: this.visualActions.recentApps
|
|
2325
2813
|
}
|
|
2326
2814
|
});
|
|
2327
2815
|
node_assert(deviceId, 'deviceId is required for AndroidDevice');
|
|
@@ -2343,7 +2831,7 @@ const launchParamSchema = z.object({
|
|
|
2343
2831
|
const terminateParamSchema = z.object({
|
|
2344
2832
|
uri: z.string().describe('Package name or app name to terminate. Use the exact package name, e.g. com.android.settings.')
|
|
2345
2833
|
});
|
|
2346
|
-
const createPlatformActions = (
|
|
2834
|
+
const createPlatformActions = (visualActions)=>({
|
|
2347
2835
|
RunAdbShell: defineAction({
|
|
2348
2836
|
name: 'RunAdbShell',
|
|
2349
2837
|
description: 'Execute an ADB shell command on the Android device and return the command stdout. Read the returned stdout to decide the next step; the stdout may indicate either success or failure.',
|
|
@@ -2354,16 +2842,7 @@ const createPlatformActions = (device)=>({
|
|
|
2354
2842
|
},
|
|
2355
2843
|
call: async (param, context)=>{
|
|
2356
2844
|
if (!param.command || '' === param.command.trim()) throw new Error('RunAdbShell requires a non-empty command parameter');
|
|
2357
|
-
|
|
2358
|
-
const stdout = await runAdbShellStdoutOrThrow(adb, param.command, void 0 === param.timeout ? void 0 : {
|
|
2359
|
-
timeout: param.timeout
|
|
2360
|
-
});
|
|
2361
|
-
const planningFeedback = buildRunAdbShellPlanningFeedback({
|
|
2362
|
-
command: param.command,
|
|
2363
|
-
stdout
|
|
2364
|
-
});
|
|
2365
|
-
if (planningFeedback && context?.task) context.task.planningFeedback = planningFeedback;
|
|
2366
|
-
return stdout;
|
|
2845
|
+
return visualActions.runAdbShell(param, context);
|
|
2367
2846
|
}
|
|
2368
2847
|
}),
|
|
2369
2848
|
Launch: defineAction({
|
|
@@ -2376,7 +2855,7 @@ const createPlatformActions = (device)=>({
|
|
|
2376
2855
|
},
|
|
2377
2856
|
call: async (param)=>{
|
|
2378
2857
|
if (!param.uri || '' === param.uri.trim()) throw new Error('Launch requires a non-empty uri parameter');
|
|
2379
|
-
await
|
|
2858
|
+
await visualActions.launch(param.uri);
|
|
2380
2859
|
}
|
|
2381
2860
|
}),
|
|
2382
2861
|
Terminate: defineAction({
|
|
@@ -2386,7 +2865,7 @@ const createPlatformActions = (device)=>({
|
|
|
2386
2865
|
paramSchema: terminateParamSchema,
|
|
2387
2866
|
call: async (param)=>{
|
|
2388
2867
|
if (!param.uri || '' === param.uri.trim()) throw new Error('Terminate requires a non-empty uri parameter');
|
|
2389
|
-
await
|
|
2868
|
+
await visualActions.terminate(param.uri);
|
|
2390
2869
|
}
|
|
2391
2870
|
})
|
|
2392
2871
|
});
|
|
@@ -2585,7 +3064,7 @@ class AndroidMidsceneTools extends BaseMidsceneTools {
|
|
|
2585
3064
|
const tools = new AndroidMidsceneTools();
|
|
2586
3065
|
runToolsCLI(tools, 'midscene-android', {
|
|
2587
3066
|
stripPrefix: 'android_',
|
|
2588
|
-
version: "1.10.
|
|
3067
|
+
version: "1.10.10-beta-20260805080137.0",
|
|
2589
3068
|
extraCommands: createReportCliCommands()
|
|
2590
3069
|
}).catch((e)=>{
|
|
2591
3070
|
process.exit(reportCLIError(e));
|