@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/index.mjs
CHANGED
|
@@ -17,8 +17,10 @@ import { BaseMidsceneTools } from "@midscene/shared/agent-tools/base-tools";
|
|
|
17
17
|
var __webpack_modules__ = {
|
|
18
18
|
"./src/scrcpy-manager.ts" (__unused_rspack_module, __webpack_exports__, __webpack_require__) {
|
|
19
19
|
__webpack_require__.d(__webpack_exports__, {
|
|
20
|
+
GJ: ()=>isScrcpyFreshFrameUnavailableError,
|
|
20
21
|
ScrcpyScreenshotManager: ()=>ScrcpyScreenshotManager,
|
|
21
|
-
|
|
22
|
+
Xr: ()=>SCRCPY_VIDEO_BIT_RATE_NETWORK_HINT,
|
|
23
|
+
ov: ()=>DEFAULT_SCRCPY_CONFIG
|
|
22
24
|
});
|
|
23
25
|
var node_fs__rspack_import_0 = __webpack_require__("node:fs");
|
|
24
26
|
var node_module__rspack_import_1 = __webpack_require__("node:module");
|
|
@@ -53,6 +55,14 @@ var __webpack_modules__ = {
|
|
|
53
55
|
const CONNECTION_WAIT_MS = 1000;
|
|
54
56
|
const MAX_SERVER_OUTPUT_LINES = 100;
|
|
55
57
|
const SERVER_OUTPUT_DRAIN_TIMEOUT_MS = 500;
|
|
58
|
+
const MAX_FRAME_AGE_US = 500000n;
|
|
59
|
+
const FRAME_FRESHNESS_WARN_INTERVAL_MS = 5000;
|
|
60
|
+
const TRANSPORT_BACKLOG_WARN_INTERVAL_MS = 5000;
|
|
61
|
+
const DEVICE_UPTIME_COMMAND = [
|
|
62
|
+
'dumpsys',
|
|
63
|
+
'power'
|
|
64
|
+
];
|
|
65
|
+
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.';
|
|
56
66
|
const BUSY_LOOP_WINDOW_MS = 1000;
|
|
57
67
|
const BUSY_LOOP_MAX_READS = 500;
|
|
58
68
|
const BUSY_LOOP_COOLDOWN_MS = 50;
|
|
@@ -63,6 +73,24 @@ var __webpack_modules__ = {
|
|
|
63
73
|
idleTimeoutMs: DEFAULT_IDLE_TIMEOUT_MS,
|
|
64
74
|
videoBitRate: DEFAULT_VIDEO_BIT_RATE
|
|
65
75
|
};
|
|
76
|
+
const SCRCPY_FRESH_FRAME_UNAVAILABLE_ERROR_CODE = 'ERR_SCRCPY_FRESH_FRAME_UNAVAILABLE';
|
|
77
|
+
class ScrcpyFreshFrameUnavailableError extends Error {
|
|
78
|
+
constructor(message, options){
|
|
79
|
+
super(message, options), _define_property(this, "code", SCRCPY_FRESH_FRAME_UNAVAILABLE_ERROR_CODE);
|
|
80
|
+
this.name = 'ScrcpyFreshFrameUnavailableError';
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function isScrcpyFreshFrameUnavailableError(error) {
|
|
84
|
+
return 'object' == typeof error && null !== error && 'code' in error && error.code === SCRCPY_FRESH_FRAME_UNAVAILABLE_ERROR_CODE;
|
|
85
|
+
}
|
|
86
|
+
function parseDeviceUptimeMs(output) {
|
|
87
|
+
const match = output.match(/mLastWakeTime=(\d+)\s+\((?:(\d+) ms ago|in (\d+) ms|now)\)/);
|
|
88
|
+
if (!match) throw new Error(`Unable to read Android device uptime from dumpsys power: ${output.trim() || '<empty output>'}`);
|
|
89
|
+
const referenceMs = BigInt(match[1]);
|
|
90
|
+
if (match[2]) return referenceMs + BigInt(match[2]);
|
|
91
|
+
if (match[3]) return referenceMs - BigInt(match[3]);
|
|
92
|
+
return referenceMs;
|
|
93
|
+
}
|
|
66
94
|
function isKeyFrameNalType(nalUnitType) {
|
|
67
95
|
return nalUnitType === NAL_TYPE_IDR || nalUnitType === NAL_TYPE_SPS || nalUnitType === NAL_TYPE_PPS;
|
|
68
96
|
}
|
|
@@ -245,24 +273,25 @@ var __webpack_modules__ = {
|
|
|
245
273
|
debugScrcpy(`Received SPS/PPS configuration: ${this.spsHeader.length}B`);
|
|
246
274
|
return;
|
|
247
275
|
}
|
|
276
|
+
const receivedAtUs = this.monotonicTimeUs();
|
|
277
|
+
if (!this.isFrameFresh(packet.pts)) return;
|
|
248
278
|
const frameBuffer = Buffer.from(packet.data);
|
|
249
279
|
const isKeyFrame = detectH264KeyFrame(frameBuffer);
|
|
250
280
|
if (isKeyFrame && this.spsHeader) {
|
|
281
|
+
const timing = this.estimateFrameTiming(packet.pts, receivedAtUs);
|
|
251
282
|
this.lastRawKeyframe = frameBuffer;
|
|
252
|
-
this.lastRawKeyframeAt =
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
capturedAt: this.lastRawKeyframeAt
|
|
265
|
-
};
|
|
283
|
+
this.lastRawKeyframeAt = timing.capturedAt;
|
|
284
|
+
this.lastRawKeyframePtsUs = packet.pts;
|
|
285
|
+
this.lastRawKeyframeEstimatedAgeMs = timing.estimatedAgeMs;
|
|
286
|
+
const frame = {
|
|
287
|
+
data: frameBuffer,
|
|
288
|
+
header: this.spsHeader,
|
|
289
|
+
ptsUs: packet.pts,
|
|
290
|
+
estimatedAgeMs: timing.estimatedAgeMs,
|
|
291
|
+
capturedAt: this.lastRawKeyframeAt
|
|
292
|
+
};
|
|
293
|
+
if (this.keyframeResolvers.length > 0) this.notifyKeyframeWaiters(frame);
|
|
294
|
+
if (this.keyframeListeners.size > 0 && this.isFrameAgeAcceptable(packet.pts, receivedAtUs)) {
|
|
266
295
|
for (const listener of this.keyframeListeners)try {
|
|
267
296
|
listener(frame);
|
|
268
297
|
} catch (error) {
|
|
@@ -272,6 +301,159 @@ var __webpack_modules__ = {
|
|
|
272
301
|
}
|
|
273
302
|
}
|
|
274
303
|
}
|
|
304
|
+
async readDeviceClockCalibration() {
|
|
305
|
+
const startedAtUs = this.monotonicTimeUs();
|
|
306
|
+
const startedAtWallMs = Date.now();
|
|
307
|
+
const shellProtocol = this.adb.subprocess.shellProtocol;
|
|
308
|
+
let output;
|
|
309
|
+
if (shellProtocol) {
|
|
310
|
+
const result = await shellProtocol.spawnWaitText(DEVICE_UPTIME_COMMAND);
|
|
311
|
+
if (0 !== result.exitCode) throw new Error(`Unable to read Android device uptime (exit ${result.exitCode}): ${result.stderr.trim() || result.stdout.trim()}`);
|
|
312
|
+
output = result.stdout;
|
|
313
|
+
} else output = await this.adb.subprocess.noneProtocol.spawnWaitText(DEVICE_UPTIME_COMMAND);
|
|
314
|
+
const finishedAtUs = this.monotonicTimeUs();
|
|
315
|
+
const finishedAtWallMs = Date.now();
|
|
316
|
+
return {
|
|
317
|
+
deviceUptimeUs: 1000n * parseDeviceUptimeMs(output),
|
|
318
|
+
hostMonotonicUs: startedAtUs + (finishedAtUs - startedAtUs) / 2n,
|
|
319
|
+
hostWallTimeMs: startedAtWallMs + (finishedAtWallMs - startedAtWallMs) / 2,
|
|
320
|
+
roundTripUs: finishedAtUs - startedAtUs
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
async ensureFrameClockCalibration() {
|
|
324
|
+
if (this.deviceClockCalibration) return;
|
|
325
|
+
this.deviceClockCalibration = await this.readDeviceClockCalibration();
|
|
326
|
+
debugScrcpy(`Calibrated scrcpy frame clock (RTT=${Number(this.deviceClockCalibration.roundTripUs / 1000n)}ms)`);
|
|
327
|
+
}
|
|
328
|
+
async setFreshnessBarrier(reason) {
|
|
329
|
+
const generation = ++this.frameFreshnessBarrierGeneration;
|
|
330
|
+
this.frameFreshnessBarrierPending = true;
|
|
331
|
+
this.clearFrameCache();
|
|
332
|
+
try {
|
|
333
|
+
const calibration = await this.readDeviceClockCalibration();
|
|
334
|
+
const barrierPtsUs = (calibration.deviceUptimeUs / 1000n + 1n) * 1000n;
|
|
335
|
+
if (generation !== this.frameFreshnessBarrierGeneration) return this.frameFreshnessBarrierPtsUs ?? barrierPtsUs;
|
|
336
|
+
this.deviceClockCalibration = calibration;
|
|
337
|
+
this.frameFreshnessBarrierPtsUs = null === this.frameFreshnessBarrierPtsUs || barrierPtsUs > this.frameFreshnessBarrierPtsUs ? barrierPtsUs : this.frameFreshnessBarrierPtsUs;
|
|
338
|
+
this.frameFreshnessBarrierReason = reason;
|
|
339
|
+
this.frameFreshnessBarrierPending = false;
|
|
340
|
+
this.frameFreshnessError = null;
|
|
341
|
+
this.lastFramePtsUs = null;
|
|
342
|
+
this.clearFrameCache();
|
|
343
|
+
debugScrcpy(`Armed frame freshness barrier at PTS ${this.frameFreshnessBarrierPtsUs}µs (${reason}, clock RTT=${Number(calibration.roundTripUs / 1000n)}ms)`);
|
|
344
|
+
return this.frameFreshnessBarrierPtsUs;
|
|
345
|
+
} catch (error) {
|
|
346
|
+
if (generation === this.frameFreshnessBarrierGeneration) {
|
|
347
|
+
this.frameFreshnessBarrierPending = false;
|
|
348
|
+
this.frameFreshnessError = new Error(`Unable to establish scrcpy frame freshness barrier (${reason}): ${error instanceof Error ? error.message : String(error)}`, {
|
|
349
|
+
cause: error
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
throw this.frameFreshnessError ?? error;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
isFrameFresh(packetPtsUs) {
|
|
356
|
+
if (this.frameFreshnessBarrierPending) return false;
|
|
357
|
+
if (void 0 !== packetPtsUs) {
|
|
358
|
+
if (null !== this.lastFramePtsUs && packetPtsUs < this.lastFramePtsUs) {
|
|
359
|
+
this.frameFreshnessError = new Error('Scrcpy frame PTS moved backwards; refusing frames until the device clock barrier is refreshed');
|
|
360
|
+
this.frameFreshnessBarrierPending = true;
|
|
361
|
+
this.clearFrameCache();
|
|
362
|
+
this.warnFrameFreshness();
|
|
363
|
+
return false;
|
|
364
|
+
}
|
|
365
|
+
this.lastFramePtsUs = packetPtsUs;
|
|
366
|
+
}
|
|
367
|
+
if (null === this.frameFreshnessBarrierPtsUs) return true;
|
|
368
|
+
if (void 0 === packetPtsUs) {
|
|
369
|
+
this.frameFreshnessError = new Error('Scrcpy frame has no PTS metadata; cannot prove that it is newer than the freshness barrier');
|
|
370
|
+
this.warnFrameFreshness();
|
|
371
|
+
return false;
|
|
372
|
+
}
|
|
373
|
+
if (packetPtsUs >= this.frameFreshnessBarrierPtsUs) {
|
|
374
|
+
if (this.frameFreshnessError) debugScrcpy(`Scrcpy video crossed the ${this.frameFreshnessBarrierReason ?? 'active'} freshness barrier at PTS ${packetPtsUs}µs`);
|
|
375
|
+
this.frameFreshnessError = null;
|
|
376
|
+
return true;
|
|
377
|
+
}
|
|
378
|
+
const behindBarrierUs = this.frameFreshnessBarrierPtsUs - packetPtsUs;
|
|
379
|
+
this.frameFreshnessError = new Error(`Scrcpy frame predates the ${this.frameFreshnessBarrierReason ?? 'active'} freshness barrier by ${Number(behindBarrierUs) / 1000}ms; refusing to use it`);
|
|
380
|
+
this.clearFrameCache();
|
|
381
|
+
this.warnFrameFreshness();
|
|
382
|
+
return false;
|
|
383
|
+
}
|
|
384
|
+
estimateFrameAgeUs(packetPtsUs, hostMonotonicUs = this.monotonicTimeUs()) {
|
|
385
|
+
const calibration = this.deviceClockCalibration;
|
|
386
|
+
if (void 0 === packetPtsUs || !calibration) return null;
|
|
387
|
+
const estimatedDeviceNowUs = calibration.deviceUptimeUs + (hostMonotonicUs - calibration.hostMonotonicUs);
|
|
388
|
+
return estimatedDeviceNowUs > packetPtsUs ? estimatedDeviceNowUs - packetPtsUs : 0n;
|
|
389
|
+
}
|
|
390
|
+
isFrameAgeAcceptable(packetPtsUs, hostMonotonicUs = this.monotonicTimeUs()) {
|
|
391
|
+
const ageUs = this.estimateFrameAgeUs(packetPtsUs, hostMonotonicUs);
|
|
392
|
+
if (null === ageUs) {
|
|
393
|
+
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');
|
|
394
|
+
this.warnFrameFreshness();
|
|
395
|
+
return false;
|
|
396
|
+
}
|
|
397
|
+
if (ageUs <= MAX_FRAME_AGE_US) {
|
|
398
|
+
if (this.frameFreshnessError) {
|
|
399
|
+
debugScrcpy(`Scrcpy frame age recovered (${Number(ageUs / 1000n)}ms)`);
|
|
400
|
+
this.frameFreshnessError = null;
|
|
401
|
+
}
|
|
402
|
+
return true;
|
|
403
|
+
}
|
|
404
|
+
this.frameFreshnessError = new Error(`Scrcpy frame absolute age is ${Number(ageUs / 1000n)}ms, exceeding the ${Number(MAX_FRAME_AGE_US / 1000n)}ms limit`);
|
|
405
|
+
this.warnFrameFreshness();
|
|
406
|
+
return false;
|
|
407
|
+
}
|
|
408
|
+
warnFrameFreshness() {
|
|
409
|
+
if (!this.frameFreshnessError) return;
|
|
410
|
+
const now = Date.now();
|
|
411
|
+
if (now - this.lastFrameFreshnessWarningAt >= FRAME_FRESHNESS_WARN_INTERVAL_MS) {
|
|
412
|
+
warnScrcpy(this.frameFreshnessError.message);
|
|
413
|
+
this.lastFrameFreshnessWarningAt = now;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
warnTransportBacklog(error) {
|
|
417
|
+
const now = Date.now();
|
|
418
|
+
if (now - this.lastTransportBacklogWarningAt < TRANSPORT_BACKLOG_WARN_INTERVAL_MS) return;
|
|
419
|
+
const cause = this.frameFreshnessError ?? error;
|
|
420
|
+
const causeMessage = cause instanceof Error ? cause.message : String(cause);
|
|
421
|
+
const currentBitRateMbps = this.options.videoBitRate / 1000000;
|
|
422
|
+
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}`);
|
|
423
|
+
this.lastTransportBacklogWarningAt = now;
|
|
424
|
+
}
|
|
425
|
+
estimateFrameTiming(packetPtsUs, receivedAtUs) {
|
|
426
|
+
const calibration = this.deviceClockCalibration;
|
|
427
|
+
if (void 0 === packetPtsUs || !calibration) return {
|
|
428
|
+
capturedAt: Date.now()
|
|
429
|
+
};
|
|
430
|
+
const estimatedAgeUs = this.estimateFrameAgeUs(packetPtsUs, receivedAtUs) ?? 0n;
|
|
431
|
+
const estimatedAgeMs = Number(estimatedAgeUs) / 1000;
|
|
432
|
+
const receivedAtWallTimeMs = calibration.hostWallTimeMs + Number(receivedAtUs - calibration.hostMonotonicUs) / 1000;
|
|
433
|
+
return {
|
|
434
|
+
capturedAt: receivedAtWallTimeMs - estimatedAgeMs,
|
|
435
|
+
estimatedAgeMs
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
clearFrameCache() {
|
|
439
|
+
this.lastRawKeyframe = null;
|
|
440
|
+
this.lastRawKeyframeAt = 0;
|
|
441
|
+
this.lastRawKeyframePtsUs = void 0;
|
|
442
|
+
this.lastRawKeyframeEstimatedAgeMs = void 0;
|
|
443
|
+
}
|
|
444
|
+
monotonicTimeUs() {
|
|
445
|
+
return process.hrtime.bigint() / 1000n;
|
|
446
|
+
}
|
|
447
|
+
resetFrameFreshnessState() {
|
|
448
|
+
this.frameFreshnessBarrierPtsUs = null;
|
|
449
|
+
this.frameFreshnessBarrierReason = null;
|
|
450
|
+
this.frameFreshnessBarrierPending = false;
|
|
451
|
+
this.frameFreshnessBarrierGeneration = 0;
|
|
452
|
+
this.deviceClockCalibration = null;
|
|
453
|
+
this.lastFramePtsUs = null;
|
|
454
|
+
this.frameFreshnessError = null;
|
|
455
|
+
this.lastFrameFreshnessWarningAt = 0;
|
|
456
|
+
}
|
|
275
457
|
subscribeKeyframes(listener) {
|
|
276
458
|
this.keyframeListeners.add(listener);
|
|
277
459
|
this.resetIdleTimer();
|
|
@@ -281,10 +463,17 @@ var __webpack_modules__ = {
|
|
|
281
463
|
};
|
|
282
464
|
}
|
|
283
465
|
getLatestRawKeyframe() {
|
|
466
|
+
const frame = this.getCachedKeyframeCandidate();
|
|
467
|
+
if (!frame || !this.isFrameAgeAcceptable(frame.ptsUs)) return null;
|
|
468
|
+
return frame;
|
|
469
|
+
}
|
|
470
|
+
getCachedKeyframeCandidate() {
|
|
284
471
|
if (!this.lastRawKeyframe || !this.spsHeader) return null;
|
|
285
472
|
return {
|
|
286
473
|
data: this.lastRawKeyframe,
|
|
287
474
|
header: this.spsHeader,
|
|
475
|
+
ptsUs: this.lastRawKeyframePtsUs,
|
|
476
|
+
estimatedAgeMs: this.lastRawKeyframeEstimatedAgeMs,
|
|
288
477
|
capturedAt: this.lastRawKeyframeAt
|
|
289
478
|
};
|
|
290
479
|
}
|
|
@@ -294,55 +483,94 @@ var __webpack_modules__ = {
|
|
|
294
483
|
frame.data
|
|
295
484
|
]));
|
|
296
485
|
}
|
|
486
|
+
async waitForUsableKeyframe(timeoutMs) {
|
|
487
|
+
const deadline = Date.now() + timeoutMs;
|
|
488
|
+
let candidate = this.getCachedKeyframeCandidate();
|
|
489
|
+
while(true){
|
|
490
|
+
if (candidate && this.isFrameAgeAcceptable(candidate.ptsUs)) return candidate;
|
|
491
|
+
const remainingMs = deadline - Date.now();
|
|
492
|
+
if (remainingMs <= 0) throw new Error(`No fresh keyframe received within ${timeoutMs}ms`);
|
|
493
|
+
candidate = await this.waitForNextKeyframe(remainingMs);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
async prepareFreshFrame() {
|
|
497
|
+
await this.ensureConnected();
|
|
498
|
+
await this.ensureFrameClockCalibration();
|
|
499
|
+
await this.waitForKeyframe();
|
|
500
|
+
await this.waitForUsableKeyframe(MAX_KEYFRAME_WAIT_MS);
|
|
501
|
+
}
|
|
502
|
+
async waitForPlanningFrame() {
|
|
503
|
+
let planningBarrierArmed = false;
|
|
504
|
+
let deadline = Date.now() + FRESH_FRAME_TIMEOUT_MS;
|
|
505
|
+
let candidate = this.getCachedKeyframeCandidate();
|
|
506
|
+
while(true){
|
|
507
|
+
if (candidate) {
|
|
508
|
+
if (void 0 === candidate.ptsUs) throw new Error('Scrcpy frame has no PTS metadata; cannot prove planning freshness');
|
|
509
|
+
const ageUs = this.estimateFrameAgeUs(candidate.ptsUs);
|
|
510
|
+
if (null === ageUs) throw new Error('Scrcpy frame clock is not calibrated; cannot prove planning freshness');
|
|
511
|
+
if (ageUs <= MAX_FRAME_AGE_US) return candidate;
|
|
512
|
+
if (planningBarrierArmed) this.clearFrameCache();
|
|
513
|
+
else {
|
|
514
|
+
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`);
|
|
515
|
+
await this.setFreshnessBarrier('stale planning frame');
|
|
516
|
+
planningBarrierArmed = true;
|
|
517
|
+
deadline = Date.now() + FRESH_FRAME_TIMEOUT_MS;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
const remainingMs = deadline - Date.now();
|
|
521
|
+
if (remainingMs <= 0) throw new Error(`No scrcpy frame crossed the active freshness target within ${FRESH_FRAME_TIMEOUT_MS}ms`);
|
|
522
|
+
candidate = await this.waitForNextKeyframe(remainingMs);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
async closeStaleStreamAndCreateFallbackError(error) {
|
|
526
|
+
this.warnTransportBacklog(error);
|
|
527
|
+
const causeMessage = error instanceof Error ? error.message : String(error);
|
|
528
|
+
await this.disconnect();
|
|
529
|
+
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}`, {
|
|
530
|
+
cause: error
|
|
531
|
+
});
|
|
532
|
+
}
|
|
297
533
|
async getScreenshotJpeg() {
|
|
298
534
|
const perfStart = Date.now();
|
|
299
535
|
const t1 = Date.now();
|
|
300
536
|
await this.ensureConnected();
|
|
301
537
|
const connectTime = Date.now() - t1;
|
|
302
538
|
const t2 = Date.now();
|
|
303
|
-
|
|
304
|
-
const spsWaitTime = Date.now() - t2;
|
|
305
|
-
const t3 = Date.now();
|
|
306
|
-
let keyframeBuffer;
|
|
307
|
-
let frameSource;
|
|
539
|
+
let frame;
|
|
308
540
|
try {
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
this.spsHeader,
|
|
315
|
-
this.lastRawKeyframe
|
|
316
|
-
]);
|
|
317
|
-
frameSource = 'cached';
|
|
318
|
-
} else {
|
|
319
|
-
keyframeBuffer = await this.waitForNextKeyframe(MAX_KEYFRAME_WAIT_MS);
|
|
320
|
-
frameSource = 'fresh-retry';
|
|
321
|
-
}
|
|
541
|
+
await this.ensureFrameClockCalibration();
|
|
542
|
+
await this.waitForKeyframe();
|
|
543
|
+
frame = await this.waitForPlanningFrame();
|
|
544
|
+
} catch (error) {
|
|
545
|
+
throw await this.closeStaleStreamAndCreateFallbackError(error);
|
|
322
546
|
}
|
|
323
|
-
const frameWaitTime = Date.now() -
|
|
547
|
+
const frameWaitTime = Date.now() - t2;
|
|
548
|
+
const keyframeBuffer = Buffer.concat([
|
|
549
|
+
frame.header,
|
|
550
|
+
frame.data
|
|
551
|
+
]);
|
|
324
552
|
this.resetIdleTimer();
|
|
325
|
-
debugScrcpy(`Decoding H.264 stream: ${keyframeBuffer.length} bytes (
|
|
326
|
-
const
|
|
553
|
+
debugScrcpy(`Decoding H.264 stream: ${keyframeBuffer.length} bytes (post-barrier)`);
|
|
554
|
+
const t5 = Date.now();
|
|
327
555
|
const result = await this.decodeH264ToJpeg(keyframeBuffer);
|
|
328
|
-
const decodeTime = Date.now() -
|
|
556
|
+
const decodeTime = Date.now() - t5;
|
|
329
557
|
const totalTime = Date.now() - perfStart;
|
|
330
|
-
debugScrcpy(`Performance: total=${totalTime}ms (connect=${connectTime}ms,
|
|
558
|
+
debugScrcpy(`Performance: total=${totalTime}ms (connect=${connectTime}ms, frameWait=${frameWaitTime}ms, decode=${decodeTime}ms)`);
|
|
331
559
|
return result;
|
|
332
560
|
}
|
|
333
561
|
getResolution() {
|
|
334
562
|
return this.videoResolution;
|
|
335
563
|
}
|
|
336
|
-
notifyKeyframeWaiters(
|
|
564
|
+
notifyKeyframeWaiters(frame) {
|
|
337
565
|
const resolvers = this.keyframeResolvers;
|
|
338
566
|
this.keyframeResolvers = [];
|
|
339
|
-
for (const resolve of resolvers)resolve(
|
|
567
|
+
for (const resolve of resolvers)resolve(frame);
|
|
340
568
|
}
|
|
341
569
|
waitForNextKeyframe(timeoutMs) {
|
|
342
570
|
return new Promise((resolve, reject)=>{
|
|
343
|
-
const wrappedResolve = (
|
|
571
|
+
const wrappedResolve = (frame)=>{
|
|
344
572
|
clearTimeout(timer);
|
|
345
|
-
resolve(
|
|
573
|
+
resolve(frame);
|
|
346
574
|
};
|
|
347
575
|
const timer = setTimeout(()=>{
|
|
348
576
|
this.keyframeResolvers = this.keyframeResolvers.filter((r)=>r !== wrappedResolve);
|
|
@@ -465,11 +693,11 @@ var __webpack_modules__ = {
|
|
|
465
693
|
this.videoStream = null;
|
|
466
694
|
this.streamReader = null;
|
|
467
695
|
this.spsHeader = null;
|
|
468
|
-
this.
|
|
469
|
-
this.lastRawKeyframeAt = 0;
|
|
696
|
+
this.clearFrameCache();
|
|
470
697
|
this.isInitialized = false;
|
|
471
698
|
this.keyframeResolvers = [];
|
|
472
699
|
this.keyframeListeners.clear();
|
|
700
|
+
this.resetFrameFreshnessState();
|
|
473
701
|
if (reader) try {
|
|
474
702
|
await reader.cancel();
|
|
475
703
|
} catch (error) {
|
|
@@ -499,8 +727,19 @@ var __webpack_modules__ = {
|
|
|
499
727
|
_define_property(this, "keyframeListeners", new Set());
|
|
500
728
|
_define_property(this, "lastRawKeyframe", null);
|
|
501
729
|
_define_property(this, "lastRawKeyframeAt", 0);
|
|
730
|
+
_define_property(this, "lastRawKeyframePtsUs", void 0);
|
|
731
|
+
_define_property(this, "lastRawKeyframeEstimatedAgeMs", void 0);
|
|
502
732
|
_define_property(this, "videoResolution", null);
|
|
503
733
|
_define_property(this, "streamReader", null);
|
|
734
|
+
_define_property(this, "frameFreshnessBarrierPtsUs", null);
|
|
735
|
+
_define_property(this, "frameFreshnessBarrierReason", null);
|
|
736
|
+
_define_property(this, "frameFreshnessBarrierPending", false);
|
|
737
|
+
_define_property(this, "frameFreshnessBarrierGeneration", 0);
|
|
738
|
+
_define_property(this, "deviceClockCalibration", null);
|
|
739
|
+
_define_property(this, "lastFramePtsUs", null);
|
|
740
|
+
_define_property(this, "frameFreshnessError", null);
|
|
741
|
+
_define_property(this, "lastFrameFreshnessWarningAt", 0);
|
|
742
|
+
_define_property(this, "lastTransportBacklogWarningAt", 0);
|
|
504
743
|
this.adb = adb;
|
|
505
744
|
const requestedBitRate = options.videoBitRate ?? DEFAULT_VIDEO_BIT_RATE;
|
|
506
745
|
const clampedBitRate = Math.min(requestedBitRate, MAX_VIDEO_BIT_RATE);
|
|
@@ -639,6 +878,7 @@ const DEFAULT_ADB_SERVER_ENDPOINT = {
|
|
|
639
878
|
class ScrcpyDeviceAdapter {
|
|
640
879
|
isEnabled() {
|
|
641
880
|
if (!this.isConfigured()) return false;
|
|
881
|
+
if (this.freshnessRecoveryPending || this.recoveryPromise) return false;
|
|
642
882
|
return null === this.retryAfter || Date.now() >= this.retryAfter;
|
|
643
883
|
}
|
|
644
884
|
getStatus() {
|
|
@@ -650,12 +890,18 @@ class ScrcpyDeviceAdapter {
|
|
|
650
890
|
};
|
|
651
891
|
}
|
|
652
892
|
isConfigured() {
|
|
653
|
-
return this.scrcpyConfig?.enabled ?? scrcpy_manager.
|
|
893
|
+
return this.scrcpyConfig?.enabled ?? scrcpy_manager.ov.enabled;
|
|
654
894
|
}
|
|
655
895
|
async initialize(deviceInfo) {
|
|
896
|
+
if (this.recoveryPromise) {
|
|
897
|
+
await this.recoveryPromise;
|
|
898
|
+
if (this.manager?.isConnected()) return;
|
|
899
|
+
}
|
|
900
|
+
this.freshnessRecoveryPending = false;
|
|
656
901
|
try {
|
|
657
902
|
const manager = await this.ensureManager(deviceInfo);
|
|
658
903
|
await manager.ensureConnected();
|
|
904
|
+
await this.applyPendingActionBarrier(manager);
|
|
659
905
|
this.clearFailure();
|
|
660
906
|
} catch (error) {
|
|
661
907
|
this.recordFailure(error);
|
|
@@ -665,24 +911,27 @@ class ScrcpyDeviceAdapter {
|
|
|
665
911
|
recordFailure(error) {
|
|
666
912
|
this.lastError = error instanceof Error ? error.message : String(error);
|
|
667
913
|
this.retryAfter = Date.now() + SCRCPY_RETRY_COOLDOWN_MS;
|
|
914
|
+
this.freshnessRecoveryPending = false;
|
|
668
915
|
}
|
|
669
916
|
clearFailure() {
|
|
670
917
|
this.lastError = null;
|
|
671
918
|
this.retryAfter = null;
|
|
919
|
+
this.freshnessRecoveryPending = false;
|
|
672
920
|
}
|
|
673
921
|
ensureRetryReady() {
|
|
922
|
+
if (this.freshnessRecoveryPending || this.recoveryPromise) throw new Error('scrcpy freshness recovery is in progress');
|
|
674
923
|
if (null === this.retryAfter || Date.now() >= this.retryAfter) return;
|
|
675
924
|
throw new Error(`scrcpy retry is cooling down until ${new Date(this.retryAfter).toISOString()}. Last error: ${this.lastError}`);
|
|
676
925
|
}
|
|
677
926
|
resolveConfig(deviceInfo) {
|
|
678
927
|
if (this.resolvedConfig) return this.resolvedConfig;
|
|
679
928
|
const config = this.scrcpyConfig;
|
|
680
|
-
const maxSize = config?.maxSize ?? scrcpy_manager.
|
|
681
|
-
const videoBitRate = config?.videoBitRate ?? scrcpy_manager.
|
|
929
|
+
const maxSize = config?.maxSize ?? scrcpy_manager.ov.maxSize;
|
|
930
|
+
const videoBitRate = config?.videoBitRate ?? scrcpy_manager.ov.videoBitRate;
|
|
682
931
|
this.resolvedConfig = {
|
|
683
932
|
enabled: this.isConfigured(),
|
|
684
933
|
maxSize,
|
|
685
|
-
idleTimeoutMs: config?.idleTimeoutMs ?? scrcpy_manager.
|
|
934
|
+
idleTimeoutMs: config?.idleTimeoutMs ?? scrcpy_manager.ov.idleTimeoutMs,
|
|
686
935
|
videoBitRate
|
|
687
936
|
};
|
|
688
937
|
return this.resolvedConfig;
|
|
@@ -716,24 +965,40 @@ class ScrcpyDeviceAdapter {
|
|
|
716
965
|
}
|
|
717
966
|
async screenshotBase64(deviceInfo) {
|
|
718
967
|
this.ensureRetryReady();
|
|
968
|
+
let manager = null;
|
|
719
969
|
try {
|
|
720
|
-
|
|
970
|
+
manager = await this.ensureManager(deviceInfo);
|
|
971
|
+
await manager.ensureConnected();
|
|
972
|
+
await this.applyPendingActionBarrier(manager);
|
|
721
973
|
const screenshotBuffer = await manager.getScreenshotJpeg();
|
|
722
974
|
this.clearFailure();
|
|
723
975
|
return createImgBase64ByFormat('jpeg', screenshotBuffer.toString('base64'));
|
|
724
976
|
} catch (error) {
|
|
977
|
+
if ((0, scrcpy_manager.GJ)(error)) {
|
|
978
|
+
this.markFreshnessRecoveryPending(manager, error);
|
|
979
|
+
throw error;
|
|
980
|
+
}
|
|
725
981
|
this.recordFailure(error);
|
|
726
982
|
throw error;
|
|
727
983
|
}
|
|
728
984
|
}
|
|
729
985
|
async subscribeKeyframes(deviceInfo, listener) {
|
|
730
986
|
this.ensureRetryReady();
|
|
987
|
+
this.keyframeListeners.add(listener);
|
|
731
988
|
try {
|
|
732
989
|
const manager = await this.ensureManager(deviceInfo);
|
|
733
990
|
await manager.ensureConnected();
|
|
991
|
+
await this.applyPendingActionBarrier(manager);
|
|
992
|
+
await manager.ensureFrameClockCalibration();
|
|
734
993
|
this.clearFailure();
|
|
735
|
-
|
|
994
|
+
this.attachKeyframeListener(manager, listener);
|
|
995
|
+
return ()=>{
|
|
996
|
+
this.keyframeListeners.delete(listener);
|
|
997
|
+
this.keyframeUnsubscribers.get(listener)?.();
|
|
998
|
+
this.keyframeUnsubscribers.delete(listener);
|
|
999
|
+
};
|
|
736
1000
|
} catch (error) {
|
|
1001
|
+
this.keyframeListeners.delete(listener);
|
|
737
1002
|
this.recordFailure(error);
|
|
738
1003
|
throw error;
|
|
739
1004
|
}
|
|
@@ -741,6 +1006,83 @@ class ScrcpyDeviceAdapter {
|
|
|
741
1006
|
getLatestRawKeyframe() {
|
|
742
1007
|
return this.manager?.getLatestRawKeyframe() ?? null;
|
|
743
1008
|
}
|
|
1009
|
+
attachKeyframeListener(manager, listener) {
|
|
1010
|
+
this.keyframeUnsubscribers.get(listener)?.();
|
|
1011
|
+
this.keyframeUnsubscribers.set(listener, manager.subscribeKeyframes(listener));
|
|
1012
|
+
}
|
|
1013
|
+
attachKeyframeListeners(manager) {
|
|
1014
|
+
this.keyframeUnsubscribers.clear();
|
|
1015
|
+
for (const listener of this.keyframeListeners)this.attachKeyframeListener(manager, listener);
|
|
1016
|
+
}
|
|
1017
|
+
markFreshnessRecoveryPending(manager, error) {
|
|
1018
|
+
this.lastError = error.message;
|
|
1019
|
+
this.retryAfter = null;
|
|
1020
|
+
this.freshnessRecoveryPending = true;
|
|
1021
|
+
this.keyframeUnsubscribers.clear();
|
|
1022
|
+
if (manager && this.manager === manager) this.manager = null;
|
|
1023
|
+
}
|
|
1024
|
+
async applyPendingActionBarrier(manager) {
|
|
1025
|
+
if (!this.pendingActionBarrier) return;
|
|
1026
|
+
await manager.setFreshnessBarrier('completed input action while scrcpy was unavailable');
|
|
1027
|
+
this.pendingActionBarrier = false;
|
|
1028
|
+
}
|
|
1029
|
+
recoverAfterAdbScreenshot(deviceInfo) {
|
|
1030
|
+
if (!this.freshnessRecoveryPending || this.recoveryPromise) return;
|
|
1031
|
+
const generation = this.lifecycleGeneration;
|
|
1032
|
+
const recovery = (async ()=>{
|
|
1033
|
+
let manager = null;
|
|
1034
|
+
try {
|
|
1035
|
+
manager = await this.ensureManager(deviceInfo);
|
|
1036
|
+
await manager.ensureConnected();
|
|
1037
|
+
await this.applyPendingActionBarrier(manager);
|
|
1038
|
+
await manager.prepareFreshFrame();
|
|
1039
|
+
if (generation !== this.lifecycleGeneration) {
|
|
1040
|
+
await manager.disconnect();
|
|
1041
|
+
if (this.manager === manager) this.manager = null;
|
|
1042
|
+
return;
|
|
1043
|
+
}
|
|
1044
|
+
this.attachKeyframeListeners(manager);
|
|
1045
|
+
this.clearFailure();
|
|
1046
|
+
debugAdapter('Scrcpy freshness recovery completed in background');
|
|
1047
|
+
} catch (error) {
|
|
1048
|
+
if (manager) {
|
|
1049
|
+
await manager.disconnect();
|
|
1050
|
+
if (this.manager === manager) this.manager = null;
|
|
1051
|
+
}
|
|
1052
|
+
this.freshnessRecoveryPending = false;
|
|
1053
|
+
this.recordFailure(error);
|
|
1054
|
+
debugAdapter(`Scrcpy background freshness recovery failed: ${error}`);
|
|
1055
|
+
throw error;
|
|
1056
|
+
}
|
|
1057
|
+
})();
|
|
1058
|
+
this.recoveryPromise = recovery;
|
|
1059
|
+
recovery.catch(()=>{}).finally(()=>{
|
|
1060
|
+
if (this.recoveryPromise === recovery) this.recoveryPromise = null;
|
|
1061
|
+
});
|
|
1062
|
+
}
|
|
1063
|
+
async markActionBarrier() {
|
|
1064
|
+
const manager = this.manager;
|
|
1065
|
+
if (!manager?.isConnected()) {
|
|
1066
|
+
this.pendingActionBarrier = true;
|
|
1067
|
+
return;
|
|
1068
|
+
}
|
|
1069
|
+
try {
|
|
1070
|
+
await manager.setFreshnessBarrier('completed input action');
|
|
1071
|
+
this.pendingActionBarrier = false;
|
|
1072
|
+
this.clearFailure();
|
|
1073
|
+
} catch (error) {
|
|
1074
|
+
this.pendingActionBarrier = true;
|
|
1075
|
+
this.recordFailure(error);
|
|
1076
|
+
debugAdapter(`Unable to mark scrcpy action barrier; disabling this stream: ${error}`);
|
|
1077
|
+
try {
|
|
1078
|
+
await manager.disconnect();
|
|
1079
|
+
} catch (disconnectError) {
|
|
1080
|
+
debugAdapter(`Error disconnecting scrcpy after barrier failure: ${disconnectError}`);
|
|
1081
|
+
} finally{
|
|
1082
|
+
if (this.manager === manager) this.manager = null;
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
744
1086
|
async decodeRawKeyframeToJpegBase64(frame) {
|
|
745
1087
|
if (!this.manager) throw new Error('scrcpy manager is not initialized');
|
|
746
1088
|
const jpegBuffer = await this.manager.decodeRawKeyframeToJpeg(frame);
|
|
@@ -764,6 +1106,13 @@ class ScrcpyDeviceAdapter {
|
|
|
764
1106
|
return resolution.width / physicalWidth;
|
|
765
1107
|
}
|
|
766
1108
|
async disconnect() {
|
|
1109
|
+
this.lifecycleGeneration += 1;
|
|
1110
|
+
this.freshnessRecoveryPending = false;
|
|
1111
|
+
this.pendingActionBarrier = false;
|
|
1112
|
+
for (const unsubscribe of this.keyframeUnsubscribers.values())unsubscribe();
|
|
1113
|
+
this.keyframeUnsubscribers.clear();
|
|
1114
|
+
this.keyframeListeners.clear();
|
|
1115
|
+
if (this.recoveryPromise) await this.recoveryPromise.catch(()=>{});
|
|
767
1116
|
if (this.manager) {
|
|
768
1117
|
try {
|
|
769
1118
|
await this.manager.disconnect();
|
|
@@ -783,6 +1132,12 @@ class ScrcpyDeviceAdapter {
|
|
|
783
1132
|
_define_property(this, "resolvedConfig", void 0);
|
|
784
1133
|
_define_property(this, "lastError", void 0);
|
|
785
1134
|
_define_property(this, "retryAfter", void 0);
|
|
1135
|
+
_define_property(this, "freshnessRecoveryPending", void 0);
|
|
1136
|
+
_define_property(this, "recoveryPromise", void 0);
|
|
1137
|
+
_define_property(this, "lifecycleGeneration", void 0);
|
|
1138
|
+
_define_property(this, "pendingActionBarrier", void 0);
|
|
1139
|
+
_define_property(this, "keyframeListeners", void 0);
|
|
1140
|
+
_define_property(this, "keyframeUnsubscribers", void 0);
|
|
786
1141
|
this.deviceId = deviceId;
|
|
787
1142
|
this.scrcpyConfig = scrcpyConfig;
|
|
788
1143
|
this.resolveAdbServerEndpoint = resolveAdbServerEndpoint;
|
|
@@ -790,6 +1145,12 @@ class ScrcpyDeviceAdapter {
|
|
|
790
1145
|
this.resolvedConfig = null;
|
|
791
1146
|
this.lastError = null;
|
|
792
1147
|
this.retryAfter = null;
|
|
1148
|
+
this.freshnessRecoveryPending = false;
|
|
1149
|
+
this.recoveryPromise = null;
|
|
1150
|
+
this.lifecycleGeneration = 0;
|
|
1151
|
+
this.pendingActionBarrier = false;
|
|
1152
|
+
this.keyframeListeners = new Set();
|
|
1153
|
+
this.keyframeUnsubscribers = new Map();
|
|
793
1154
|
}
|
|
794
1155
|
}
|
|
795
1156
|
const TAG_RE = /<(\/?)([A-Za-z_][A-Za-z0-9_.\-:]*)([^>]*?)(\/?)>/g;
|
|
@@ -1016,6 +1377,20 @@ async function captureAndroidUITree(options) {
|
|
|
1016
1377
|
}
|
|
1017
1378
|
throw new Error(`Unable to capture Android UI tree after ${failures.length} attempt(s): ${failures.join('; ')}`);
|
|
1018
1379
|
}
|
|
1380
|
+
function createVisualActionRegistry(definitions, onActionSettled) {
|
|
1381
|
+
const registeredActions = {};
|
|
1382
|
+
for (const actionName of Object.keys(definitions)){
|
|
1383
|
+
const dispatch = definitions[actionName];
|
|
1384
|
+
registeredActions[actionName] = async (...args)=>{
|
|
1385
|
+
try {
|
|
1386
|
+
return await dispatch(...args);
|
|
1387
|
+
} finally{
|
|
1388
|
+
await onActionSettled(actionName);
|
|
1389
|
+
}
|
|
1390
|
+
};
|
|
1391
|
+
}
|
|
1392
|
+
return registeredActions;
|
|
1393
|
+
}
|
|
1019
1394
|
function device_define_property(obj, key, value) {
|
|
1020
1395
|
if (key in obj) Object.defineProperty(obj, key, {
|
|
1021
1396
|
value: value,
|
|
@@ -1116,19 +1491,12 @@ class AndroidDevice {
|
|
|
1116
1491
|
}
|
|
1117
1492
|
},
|
|
1118
1493
|
call: async (param)=>{
|
|
1119
|
-
const element = param.locate;
|
|
1120
|
-
const startPoint = element ? {
|
|
1121
|
-
left: element.center[0],
|
|
1122
|
-
top: element.center[1]
|
|
1123
|
-
} : void 0;
|
|
1124
1494
|
if (!param || !param.direction) throw new Error('PullGesture requires a direction parameter');
|
|
1125
|
-
|
|
1126
|
-
else if ('up' === param.direction) await this.pullUp(startPoint, param.distance, param.duration);
|
|
1127
|
-
else throw new Error(`Unknown pull direction: ${param.direction}`);
|
|
1495
|
+
await this.visualActions.pullGesture(param);
|
|
1128
1496
|
}
|
|
1129
1497
|
})
|
|
1130
1498
|
];
|
|
1131
|
-
const platformActions = createPlatformActions(this);
|
|
1499
|
+
const platformActions = createPlatformActions(this.visualActions);
|
|
1132
1500
|
let platformSpecificActions = Object.values(platformActions);
|
|
1133
1501
|
if (this.options?.exposeRunAdbShellAction === false) platformSpecificActions = platformSpecificActions.filter((action)=>action !== platformActions.RunAdbShell);
|
|
1134
1502
|
const customActions = this.customActions || [];
|
|
@@ -1138,6 +1506,16 @@ class AndroidDevice {
|
|
|
1138
1506
|
...customActions
|
|
1139
1507
|
];
|
|
1140
1508
|
}
|
|
1509
|
+
async performPullGesture(param) {
|
|
1510
|
+
const element = param.locate;
|
|
1511
|
+
const startPoint = element ? {
|
|
1512
|
+
left: element.center[0],
|
|
1513
|
+
top: element.center[1]
|
|
1514
|
+
} : void 0;
|
|
1515
|
+
if ('down' === param.direction) await this.pullDownRaw(startPoint, param.distance, param.duration);
|
|
1516
|
+
else if ('up' === param.direction) await this.pullUpRaw(startPoint, param.distance, param.duration);
|
|
1517
|
+
else throw new Error(`Unknown pull direction: ${param.direction}`);
|
|
1518
|
+
}
|
|
1141
1519
|
async performActionScroll(param) {
|
|
1142
1520
|
const element = param.locate;
|
|
1143
1521
|
const startingPoint = element ? {
|
|
@@ -1145,20 +1523,32 @@ class AndroidDevice {
|
|
|
1145
1523
|
top: element.center[1]
|
|
1146
1524
|
} : void 0;
|
|
1147
1525
|
const scrollToEventName = param?.scrollType;
|
|
1148
|
-
if ('scrollToTop' === scrollToEventName) await this.
|
|
1149
|
-
else if ('scrollToBottom' === scrollToEventName) await this.
|
|
1150
|
-
else if ('scrollToRight' === scrollToEventName) await this.
|
|
1151
|
-
else if ('scrollToLeft' === scrollToEventName) await this.
|
|
1526
|
+
if ('scrollToTop' === scrollToEventName) await this.scrollUntilTopRaw(startingPoint);
|
|
1527
|
+
else if ('scrollToBottom' === scrollToEventName) await this.scrollUntilBottomRaw(startingPoint);
|
|
1528
|
+
else if ('scrollToRight' === scrollToEventName) await this.scrollUntilRightRaw(startingPoint);
|
|
1529
|
+
else if ('scrollToLeft' === scrollToEventName) await this.scrollUntilLeftRaw(startingPoint);
|
|
1152
1530
|
else if ('singleAction' !== scrollToEventName && scrollToEventName) throw new Error(`Unknown scroll event type: ${scrollToEventName}, param: ${JSON.stringify(param)}`);
|
|
1153
1531
|
else {
|
|
1154
|
-
if (param?.direction !== 'down' && param && param.direction) if ('up' === param.direction) await this.
|
|
1155
|
-
else if ('left' === param.direction) await this.
|
|
1156
|
-
else if ('right' === param.direction) await this.
|
|
1532
|
+
if (param?.direction !== 'down' && param && param.direction) if ('up' === param.direction) await this.scrollUpRaw(param.distance || void 0, startingPoint);
|
|
1533
|
+
else if ('left' === param.direction) await this.scrollLeftRaw(param.distance || void 0, startingPoint);
|
|
1534
|
+
else if ('right' === param.direction) await this.scrollRightRaw(param.distance || void 0, startingPoint);
|
|
1157
1535
|
else throw new Error(`Unknown scroll direction: ${param.direction}`);
|
|
1158
|
-
else await this.
|
|
1536
|
+
else await this.scrollDownRaw(param?.distance || void 0, startingPoint);
|
|
1159
1537
|
await sleep(500);
|
|
1160
1538
|
}
|
|
1161
1539
|
}
|
|
1540
|
+
async runAdbShellRaw(param, context) {
|
|
1541
|
+
const adb = await this.getAdb();
|
|
1542
|
+
const stdout = await runAdbShellStdoutOrThrow(adb, param.command, void 0 === param.timeout ? void 0 : {
|
|
1543
|
+
timeout: param.timeout
|
|
1544
|
+
});
|
|
1545
|
+
const planningFeedback = buildRunAdbShellPlanningFeedback({
|
|
1546
|
+
command: param.command,
|
|
1547
|
+
stdout
|
|
1548
|
+
});
|
|
1549
|
+
if (planningFeedback && context?.task) context.task.planningFeedback = planningFeedback;
|
|
1550
|
+
return stdout;
|
|
1551
|
+
}
|
|
1162
1552
|
describe() {
|
|
1163
1553
|
return this.description || `DeviceId: ${this.deviceId}`;
|
|
1164
1554
|
}
|
|
@@ -1255,15 +1645,15 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1255
1645
|
const adapter = this.getScrcpyAdapter();
|
|
1256
1646
|
if (!adapter.isEnabled()) throw new Error('scrcpy is not available for frame observation');
|
|
1257
1647
|
const deviceInfo = await this.getDevicePhysicalInfo();
|
|
1258
|
-
|
|
1259
|
-
const unsubscribe = await adapter.subscribeKeyframes(deviceInfo, (frame)=>{
|
|
1260
|
-
latest = frame;
|
|
1261
|
-
});
|
|
1648
|
+
const unsubscribe = await adapter.subscribeKeyframes(deviceInfo, ()=>{});
|
|
1262
1649
|
return {
|
|
1263
|
-
latest: ()=>
|
|
1650
|
+
latest: ()=>{
|
|
1651
|
+
const latest = adapter.getLatestRawKeyframe();
|
|
1652
|
+
return latest ? {
|
|
1264
1653
|
ref: latest,
|
|
1265
1654
|
capturedAt: latest.capturedAt
|
|
1266
|
-
} : null
|
|
1655
|
+
} : null;
|
|
1656
|
+
},
|
|
1267
1657
|
decode: async (refs)=>{
|
|
1268
1658
|
const images = [];
|
|
1269
1659
|
for (const frameRef of refs)images.push(await adapter.decodeRawKeyframeToJpegBase64(frameRef.ref));
|
|
@@ -1297,6 +1687,9 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1297
1687
|
return this.appNameMapping[normalizedAppName];
|
|
1298
1688
|
}
|
|
1299
1689
|
async launch(uri) {
|
|
1690
|
+
return this.visualActions.launch(uri);
|
|
1691
|
+
}
|
|
1692
|
+
async launchRaw(uri) {
|
|
1300
1693
|
const adb = await this.getAdb();
|
|
1301
1694
|
this.uri = uri;
|
|
1302
1695
|
try {
|
|
@@ -1322,6 +1715,9 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1322
1715
|
return this;
|
|
1323
1716
|
}
|
|
1324
1717
|
async terminate(uri) {
|
|
1718
|
+
await this.visualActions.terminate(uri);
|
|
1719
|
+
}
|
|
1720
|
+
async terminateRaw(uri) {
|
|
1325
1721
|
const packagePart = uri.includes('/') ? uri.split('/')[0] : uri;
|
|
1326
1722
|
const resolved = this.resolvePackageName(packagePart) ?? packagePart;
|
|
1327
1723
|
const adb = await this.getAdb();
|
|
@@ -1337,6 +1733,9 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1337
1733
|
}
|
|
1338
1734
|
}
|
|
1339
1735
|
async execYadb(keyboardContent) {
|
|
1736
|
+
await this.visualActions.execYadb(keyboardContent);
|
|
1737
|
+
}
|
|
1738
|
+
async execYadbRaw(keyboardContent) {
|
|
1340
1739
|
this.warnYadbOnNonDefaultDisplay('keyboard input');
|
|
1341
1740
|
await this.ensureYadb();
|
|
1342
1741
|
const adb = await this.getAdb();
|
|
@@ -1593,14 +1992,15 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1593
1992
|
async screenshotBase64() {
|
|
1594
1993
|
debugDevice('screenshotBase64 begin');
|
|
1595
1994
|
const adapter = this.getScrcpyAdapter();
|
|
1995
|
+
let scrcpyDeviceInfo = null;
|
|
1596
1996
|
if (adapter.isEnabled()) try {
|
|
1597
1997
|
debugDevice('Attempting scrcpy screenshot...');
|
|
1598
|
-
|
|
1599
|
-
const result = await adapter.screenshotBase64(
|
|
1998
|
+
scrcpyDeviceInfo = await this.getDevicePhysicalInfo();
|
|
1999
|
+
const result = await adapter.screenshotBase64(scrcpyDeviceInfo);
|
|
1600
2000
|
debugDevice('screenshotBase64 end (scrcpy mode)');
|
|
1601
2001
|
return result;
|
|
1602
2002
|
} catch (error) {
|
|
1603
|
-
warnDevice(`Scrcpy screenshot failed, falling back to standard ADB method
|
|
2003
|
+
warnDevice(`Scrcpy screenshot failed, falling back to standard ADB method. This may be caused by transport backlog. ${scrcpy_manager.Xr}\nError: ${error}`);
|
|
1604
2004
|
}
|
|
1605
2005
|
const adb = await this.getAdb();
|
|
1606
2006
|
let screenshotBuffer;
|
|
@@ -1678,9 +2078,13 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1678
2078
|
});
|
|
1679
2079
|
}
|
|
1680
2080
|
debugDevice('screenshotBase64 end');
|
|
2081
|
+
if (scrcpyDeviceInfo) adapter.recoverAfterAdbScreenshot(scrcpyDeviceInfo);
|
|
1681
2082
|
return result;
|
|
1682
2083
|
}
|
|
1683
2084
|
async clearInput(element) {
|
|
2085
|
+
await this.visualActions.clearInput(element);
|
|
2086
|
+
}
|
|
2087
|
+
async clearInputRaw(element) {
|
|
1684
2088
|
if (element) await this.tapPoint({
|
|
1685
2089
|
x: element.center[0],
|
|
1686
2090
|
y: element.center[1]
|
|
@@ -1714,6 +2118,9 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1714
2118
|
return '';
|
|
1715
2119
|
}
|
|
1716
2120
|
async scrollUntilTop(startPoint) {
|
|
2121
|
+
await this.visualActions.scrollUntilTop(startPoint);
|
|
2122
|
+
}
|
|
2123
|
+
async scrollUntilTopRaw(startPoint) {
|
|
1717
2124
|
if (startPoint) {
|
|
1718
2125
|
const { height } = await this.size();
|
|
1719
2126
|
const start = {
|
|
@@ -1728,10 +2135,13 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1728
2135
|
await sleep(1000);
|
|
1729
2136
|
return;
|
|
1730
2137
|
}
|
|
1731
|
-
await repeat(defaultScrollUntilTimes, ()=>this.
|
|
2138
|
+
await repeat(defaultScrollUntilTimes, ()=>this.scrollRaw(0, -9999999, defaultFastScrollDuration));
|
|
1732
2139
|
await sleep(1000);
|
|
1733
2140
|
}
|
|
1734
2141
|
async scrollUntilBottom(startPoint) {
|
|
2142
|
+
await this.visualActions.scrollUntilBottom(startPoint);
|
|
2143
|
+
}
|
|
2144
|
+
async scrollUntilBottomRaw(startPoint) {
|
|
1735
2145
|
if (startPoint) {
|
|
1736
2146
|
const start = {
|
|
1737
2147
|
x: Math.round(startPoint.left),
|
|
@@ -1745,10 +2155,13 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1745
2155
|
await sleep(1000);
|
|
1746
2156
|
return;
|
|
1747
2157
|
}
|
|
1748
|
-
await repeat(defaultScrollUntilTimes, ()=>this.
|
|
2158
|
+
await repeat(defaultScrollUntilTimes, ()=>this.scrollRaw(0, 9999999, defaultFastScrollDuration));
|
|
1749
2159
|
await sleep(1000);
|
|
1750
2160
|
}
|
|
1751
2161
|
async scrollUntilLeft(startPoint) {
|
|
2162
|
+
await this.visualActions.scrollUntilLeft(startPoint);
|
|
2163
|
+
}
|
|
2164
|
+
async scrollUntilLeftRaw(startPoint) {
|
|
1752
2165
|
if (startPoint) {
|
|
1753
2166
|
const { width } = await this.size();
|
|
1754
2167
|
const start = {
|
|
@@ -1763,10 +2176,13 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1763
2176
|
await sleep(1000);
|
|
1764
2177
|
return;
|
|
1765
2178
|
}
|
|
1766
|
-
await repeat(defaultScrollUntilTimes, ()=>this.
|
|
2179
|
+
await repeat(defaultScrollUntilTimes, ()=>this.scrollRaw(-9999999, 0, defaultFastScrollDuration));
|
|
1767
2180
|
await sleep(1000);
|
|
1768
2181
|
}
|
|
1769
2182
|
async scrollUntilRight(startPoint) {
|
|
2183
|
+
await this.visualActions.scrollUntilRight(startPoint);
|
|
2184
|
+
}
|
|
2185
|
+
async scrollUntilRightRaw(startPoint) {
|
|
1770
2186
|
if (startPoint) {
|
|
1771
2187
|
const start = {
|
|
1772
2188
|
x: Math.round(startPoint.left),
|
|
@@ -1780,10 +2196,13 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1780
2196
|
await sleep(1000);
|
|
1781
2197
|
return;
|
|
1782
2198
|
}
|
|
1783
|
-
await repeat(defaultScrollUntilTimes, ()=>this.
|
|
2199
|
+
await repeat(defaultScrollUntilTimes, ()=>this.scrollRaw(9999999, 0, defaultFastScrollDuration));
|
|
1784
2200
|
await sleep(1000);
|
|
1785
2201
|
}
|
|
1786
2202
|
async scrollUp(distance, startPoint) {
|
|
2203
|
+
await this.visualActions.scrollUp(distance, startPoint);
|
|
2204
|
+
}
|
|
2205
|
+
async scrollUpRaw(distance, startPoint) {
|
|
1787
2206
|
const { height } = await this.size();
|
|
1788
2207
|
const scrollDistance = Math.round(distance || height);
|
|
1789
2208
|
const hasExplicitDistance = void 0 !== distance;
|
|
@@ -1797,9 +2216,12 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1797
2216
|
await this.dragPoint(start, end);
|
|
1798
2217
|
return;
|
|
1799
2218
|
}
|
|
1800
|
-
await this.
|
|
2219
|
+
await this.scrollRaw(0, -scrollDistance, void 0, hasExplicitDistance, 'up');
|
|
1801
2220
|
}
|
|
1802
2221
|
async scrollDown(distance, startPoint) {
|
|
2222
|
+
await this.visualActions.scrollDown(distance, startPoint);
|
|
2223
|
+
}
|
|
2224
|
+
async scrollDownRaw(distance, startPoint) {
|
|
1803
2225
|
const { height } = await this.size();
|
|
1804
2226
|
const scrollDistance = Math.round(distance || height);
|
|
1805
2227
|
const hasExplicitDistance = void 0 !== distance;
|
|
@@ -1813,9 +2235,12 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1813
2235
|
await this.dragPoint(start, end);
|
|
1814
2236
|
return;
|
|
1815
2237
|
}
|
|
1816
|
-
await this.
|
|
2238
|
+
await this.scrollRaw(0, scrollDistance, void 0, hasExplicitDistance, 'down');
|
|
1817
2239
|
}
|
|
1818
2240
|
async scrollLeft(distance, startPoint) {
|
|
2241
|
+
await this.visualActions.scrollLeft(distance, startPoint);
|
|
2242
|
+
}
|
|
2243
|
+
async scrollLeftRaw(distance, startPoint) {
|
|
1819
2244
|
const { width } = await this.size();
|
|
1820
2245
|
const scrollDistance = Math.round(distance || width);
|
|
1821
2246
|
const hasExplicitDistance = void 0 !== distance;
|
|
@@ -1829,9 +2254,12 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1829
2254
|
await this.dragPoint(start, end);
|
|
1830
2255
|
return;
|
|
1831
2256
|
}
|
|
1832
|
-
await this.
|
|
2257
|
+
await this.scrollRaw(-scrollDistance, 0, void 0, hasExplicitDistance, 'left');
|
|
1833
2258
|
}
|
|
1834
2259
|
async scrollRight(distance, startPoint) {
|
|
2260
|
+
await this.visualActions.scrollRight(distance, startPoint);
|
|
2261
|
+
}
|
|
2262
|
+
async scrollRightRaw(distance, startPoint) {
|
|
1835
2263
|
const { width } = await this.size();
|
|
1836
2264
|
const scrollDistance = Math.round(distance || width);
|
|
1837
2265
|
const hasExplicitDistance = void 0 !== distance;
|
|
@@ -1845,7 +2273,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1845
2273
|
await this.dragPoint(start, end);
|
|
1846
2274
|
return;
|
|
1847
2275
|
}
|
|
1848
|
-
await this.
|
|
2276
|
+
await this.scrollRaw(scrollDistance, 0, void 0, hasExplicitDistance, 'right');
|
|
1849
2277
|
}
|
|
1850
2278
|
async ensureYadb() {
|
|
1851
2279
|
if (!this.yadbPushed) {
|
|
@@ -1874,11 +2302,11 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1874
2302
|
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)';
|
|
1875
2303
|
throw new Error(`${reason}, but yadb (app_process) cannot target non-default displayId=${this.options?.displayId}. Use displayId=0 or a different imeStrategy.`);
|
|
1876
2304
|
}
|
|
1877
|
-
if (needsYadb) await this.
|
|
2305
|
+
if (needsYadb) await this.execYadbRaw(escapeForShell(text));
|
|
1878
2306
|
else await this.shellInputText(text, {
|
|
1879
2307
|
keyboardTypeDelay: typeDelay
|
|
1880
2308
|
});
|
|
1881
|
-
if (true === shouldAutoDismissKeyboard) await this.
|
|
2309
|
+
if (true === shouldAutoDismissKeyboard) await this.hideKeyboardRaw(options);
|
|
1882
2310
|
}
|
|
1883
2311
|
normalizeKeyName(key) {
|
|
1884
2312
|
const keyMap = {
|
|
@@ -1948,6 +2376,9 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1948
2376
|
await adb.shell(`input${this.getDisplayArg()} swipe ${fromX} ${fromY} ${toX} ${toY} ${duration}`);
|
|
1949
2377
|
}
|
|
1950
2378
|
async scroll(deltaX, deltaY, duration, warnOnClamp = false, direction) {
|
|
2379
|
+
await this.visualActions.scroll(deltaX, deltaY, duration, warnOnClamp, direction);
|
|
2380
|
+
}
|
|
2381
|
+
async scrollRaw(deltaX, deltaY, duration, warnOnClamp = false, direction) {
|
|
1951
2382
|
if (0 === deltaX && 0 === deltaY) throw new Error('Scroll distance cannot be zero in both directions');
|
|
1952
2383
|
const { width, height } = await this.size();
|
|
1953
2384
|
const n = 4;
|
|
@@ -2012,12 +2443,21 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
2012
2443
|
}
|
|
2013
2444
|
}
|
|
2014
2445
|
async back() {
|
|
2446
|
+
await this.visualActions.back();
|
|
2447
|
+
}
|
|
2448
|
+
async backRaw() {
|
|
2015
2449
|
await this.shellInputKeyevent(4);
|
|
2016
2450
|
}
|
|
2017
2451
|
async home() {
|
|
2452
|
+
await this.visualActions.home();
|
|
2453
|
+
}
|
|
2454
|
+
async homeRaw() {
|
|
2018
2455
|
await this.shellInputKeyevent(3);
|
|
2019
2456
|
}
|
|
2020
2457
|
async recentApps() {
|
|
2458
|
+
await this.visualActions.recentApps();
|
|
2459
|
+
}
|
|
2460
|
+
async recentAppsRaw() {
|
|
2021
2461
|
await this.shellInputKeyevent(187);
|
|
2022
2462
|
}
|
|
2023
2463
|
async longPressPoint(point, duration = 2000) {
|
|
@@ -2026,6 +2466,9 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
2026
2466
|
await adb.shell(`input${this.getDisplayArg()} swipe ${adjustedX} ${adjustedY} ${adjustedX} ${adjustedY} ${duration}`);
|
|
2027
2467
|
}
|
|
2028
2468
|
async pullDown(startPoint, distance, duration = 800) {
|
|
2469
|
+
await this.visualActions.pullDown(startPoint, distance, duration);
|
|
2470
|
+
}
|
|
2471
|
+
async pullDownRaw(startPoint, distance, duration = 800) {
|
|
2029
2472
|
const { width, height } = await this.size();
|
|
2030
2473
|
const start = startPoint ? {
|
|
2031
2474
|
x: Math.round(startPoint.left),
|
|
@@ -2039,13 +2482,19 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
2039
2482
|
x: start.x,
|
|
2040
2483
|
y: start.y + pullDistance
|
|
2041
2484
|
};
|
|
2042
|
-
await this.
|
|
2485
|
+
await this.pullDragRaw(start, end, duration);
|
|
2043
2486
|
await sleep(200);
|
|
2044
2487
|
}
|
|
2045
2488
|
async pullDrag(from, to, duration) {
|
|
2489
|
+
await this.visualActions.pullDrag(from, to, duration);
|
|
2490
|
+
}
|
|
2491
|
+
async pullDragRaw(from, to, duration) {
|
|
2046
2492
|
await this.swipePoint(from, to, duration);
|
|
2047
2493
|
}
|
|
2048
2494
|
async pullUp(startPoint, distance, duration = 600) {
|
|
2495
|
+
await this.visualActions.pullUp(startPoint, distance, duration);
|
|
2496
|
+
}
|
|
2497
|
+
async pullUpRaw(startPoint, distance, duration = 600) {
|
|
2049
2498
|
const { width, height } = await this.size();
|
|
2050
2499
|
const start = startPoint ? {
|
|
2051
2500
|
x: Math.round(startPoint.left),
|
|
@@ -2059,7 +2508,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
2059
2508
|
x: start.x,
|
|
2060
2509
|
y: start.y - pullDistance
|
|
2061
2510
|
};
|
|
2062
|
-
await this.
|
|
2511
|
+
await this.pullDragRaw(start, end, duration);
|
|
2063
2512
|
await sleep(100);
|
|
2064
2513
|
}
|
|
2065
2514
|
getDisplayArg() {
|
|
@@ -2122,6 +2571,9 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
2122
2571
|
}
|
|
2123
2572
|
}
|
|
2124
2573
|
async hideKeyboard(options, timeoutMs = 1000) {
|
|
2574
|
+
return this.visualActions.hideKeyboard(options, timeoutMs);
|
|
2575
|
+
}
|
|
2576
|
+
async hideKeyboardRaw(options, timeoutMs = 1000) {
|
|
2125
2577
|
const adb = await this.getAdb();
|
|
2126
2578
|
const keyboardDismissStrategy = options?.keyboardDismissStrategy ?? this.options?.keyboardDismissStrategy ?? 'esc-first';
|
|
2127
2579
|
const keyboardStatus = await adb.isSoftKeyboardPresent();
|
|
@@ -2176,55 +2628,91 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
2176
2628
|
device_define_property(this, "interfaceType", 'android');
|
|
2177
2629
|
device_define_property(this, "uri", void 0);
|
|
2178
2630
|
device_define_property(this, "options", void 0);
|
|
2631
|
+
device_define_property(this, "visualActions", createVisualActionRegistry({
|
|
2632
|
+
tap: (point)=>this.tapPoint(point),
|
|
2633
|
+
doubleClick: (point)=>this.doubleTapPoint(point),
|
|
2634
|
+
longPress: (point, opts)=>this.longPressPoint(point, opts?.duration),
|
|
2635
|
+
dragAndDrop: (from, to)=>this.dragPoint(from, to),
|
|
2636
|
+
keyboardPress: (keyName)=>this.pressKey(keyName),
|
|
2637
|
+
typeText: async (value, opts)=>{
|
|
2638
|
+
const target = opts?.target;
|
|
2639
|
+
if (target && opts?.replace !== false) await this.clearInputRaw(target);
|
|
2640
|
+
else if (target) await this.tapPoint({
|
|
2641
|
+
x: target.center[0],
|
|
2642
|
+
y: target.center[1]
|
|
2643
|
+
});
|
|
2644
|
+
if (opts?.focusOnly) return;
|
|
2645
|
+
await this.typeText(value, opts);
|
|
2646
|
+
},
|
|
2647
|
+
clearInput: (target)=>this.clearInputRaw(target),
|
|
2648
|
+
cursorMove: async (direction, times = 1)=>{
|
|
2649
|
+
const arrowKey = 'left' === direction ? 'ArrowLeft' : 'ArrowRight';
|
|
2650
|
+
for(let index = 0; index < times; index++)await this.pressKey(arrowKey);
|
|
2651
|
+
},
|
|
2652
|
+
swipe: async (start, end, opts)=>{
|
|
2653
|
+
const duration = opts?.duration ?? 300;
|
|
2654
|
+
const repeatCount = opts?.repeat ?? 1;
|
|
2655
|
+
for(let index = 0; index < repeatCount; index++)await this.dragPoint(start, end, duration);
|
|
2656
|
+
},
|
|
2657
|
+
pinch: async (center, opts)=>{
|
|
2658
|
+
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.`);
|
|
2659
|
+
const { x: adjCenterX, y: adjCenterY } = await this.adjustCoordinates(Math.round(center.x), Math.round(center.y));
|
|
2660
|
+
const ratio = 0 !== adjCenterX && 0 !== center.x ? adjCenterX / center.x : 1;
|
|
2661
|
+
const adjStartDist = Math.round(opts.startDistance * ratio);
|
|
2662
|
+
const adjEndDist = Math.round(opts.endDistance * ratio);
|
|
2663
|
+
await this.ensureYadb();
|
|
2664
|
+
const adb = await this.getAdb();
|
|
2665
|
+
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}`);
|
|
2666
|
+
},
|
|
2667
|
+
actionScroll: (param)=>this.performActionScroll(param),
|
|
2668
|
+
back: ()=>this.backRaw(),
|
|
2669
|
+
home: ()=>this.homeRaw(),
|
|
2670
|
+
recentApps: ()=>this.recentAppsRaw(),
|
|
2671
|
+
pullGesture: (param)=>this.performPullGesture(param),
|
|
2672
|
+
launch: (uri)=>this.launchRaw(uri),
|
|
2673
|
+
terminate: (uri)=>this.terminateRaw(uri),
|
|
2674
|
+
runAdbShell: (param, context)=>this.runAdbShellRaw(param, context),
|
|
2675
|
+
execYadb: (keyboardContent)=>this.execYadbRaw(keyboardContent),
|
|
2676
|
+
scrollUntilTop: (startPoint)=>this.scrollUntilTopRaw(startPoint),
|
|
2677
|
+
scrollUntilBottom: (startPoint)=>this.scrollUntilBottomRaw(startPoint),
|
|
2678
|
+
scrollUntilLeft: (startPoint)=>this.scrollUntilLeftRaw(startPoint),
|
|
2679
|
+
scrollUntilRight: (startPoint)=>this.scrollUntilRightRaw(startPoint),
|
|
2680
|
+
scrollUp: (distance, startPoint)=>this.scrollUpRaw(distance, startPoint),
|
|
2681
|
+
scrollDown: (distance, startPoint)=>this.scrollDownRaw(distance, startPoint),
|
|
2682
|
+
scrollLeft: (distance, startPoint)=>this.scrollLeftRaw(distance, startPoint),
|
|
2683
|
+
scrollRight: (distance, startPoint)=>this.scrollRightRaw(distance, startPoint),
|
|
2684
|
+
scroll: (deltaX, deltaY, duration, warnOnClamp, direction)=>this.scrollRaw(deltaX, deltaY, duration, warnOnClamp, direction),
|
|
2685
|
+
pullDown: (startPoint, distance, duration)=>this.pullDownRaw(startPoint, distance, duration),
|
|
2686
|
+
pullDrag: (from, to, duration)=>this.pullDragRaw(from, to, duration),
|
|
2687
|
+
pullUp: (startPoint, distance, duration)=>this.pullUpRaw(startPoint, distance, duration),
|
|
2688
|
+
hideKeyboard: (options, timeoutMs)=>this.hideKeyboardRaw(options, timeoutMs)
|
|
2689
|
+
}, async ()=>{
|
|
2690
|
+
await this.scrcpyAdapter?.markActionBarrier();
|
|
2691
|
+
}));
|
|
2179
2692
|
device_define_property(this, "inputPrimitives", {
|
|
2180
2693
|
pointer: {
|
|
2181
|
-
tap:
|
|
2182
|
-
doubleClick:
|
|
2183
|
-
longPress:
|
|
2184
|
-
dragAndDrop:
|
|
2694
|
+
tap: this.visualActions.tap,
|
|
2695
|
+
doubleClick: this.visualActions.doubleClick,
|
|
2696
|
+
longPress: this.visualActions.longPress,
|
|
2697
|
+
dragAndDrop: this.visualActions.dragAndDrop
|
|
2185
2698
|
},
|
|
2186
2699
|
keyboard: {
|
|
2187
|
-
keyboardPress:
|
|
2188
|
-
typeText:
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
else if (target) await this.tapPoint({
|
|
2192
|
-
x: target.center[0],
|
|
2193
|
-
y: target.center[1]
|
|
2194
|
-
});
|
|
2195
|
-
if (opts?.focusOnly) return;
|
|
2196
|
-
await this.typeText(value, opts);
|
|
2197
|
-
},
|
|
2198
|
-
clearInput: (target)=>this.clearInput(target),
|
|
2199
|
-
cursorMove: async (direction, times = 1)=>{
|
|
2200
|
-
const arrowKey = 'left' === direction ? 'ArrowLeft' : 'ArrowRight';
|
|
2201
|
-
for(let i = 0; i < times; i++)await this.pressKey(arrowKey);
|
|
2202
|
-
}
|
|
2700
|
+
keyboardPress: this.visualActions.keyboardPress,
|
|
2701
|
+
typeText: this.visualActions.typeText,
|
|
2702
|
+
clearInput: this.visualActions.clearInput,
|
|
2703
|
+
cursorMove: this.visualActions.cursorMove
|
|
2203
2704
|
},
|
|
2204
2705
|
touch: {
|
|
2205
|
-
swipe:
|
|
2206
|
-
|
|
2207
|
-
const repeatCount = opts?.repeat ?? 1;
|
|
2208
|
-
for(let i = 0; i < repeatCount; i++)await this.dragPoint(start, end, duration);
|
|
2209
|
-
},
|
|
2210
|
-
pinch: async (center, opts)=>{
|
|
2211
|
-
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.`);
|
|
2212
|
-
const { x: adjCenterX, y: adjCenterY } = await this.adjustCoordinates(Math.round(center.x), Math.round(center.y));
|
|
2213
|
-
const ratio = 0 !== adjCenterX && 0 !== center.x ? adjCenterX / center.x : 1;
|
|
2214
|
-
const adjStartDist = Math.round(opts.startDistance * ratio);
|
|
2215
|
-
const adjEndDist = Math.round(opts.endDistance * ratio);
|
|
2216
|
-
await this.ensureYadb();
|
|
2217
|
-
const adb = await this.getAdb();
|
|
2218
|
-
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}`);
|
|
2219
|
-
}
|
|
2706
|
+
swipe: this.visualActions.swipe,
|
|
2707
|
+
pinch: this.visualActions.pinch
|
|
2220
2708
|
},
|
|
2221
2709
|
scroll: {
|
|
2222
|
-
scroll:
|
|
2710
|
+
scroll: this.visualActions.actionScroll
|
|
2223
2711
|
},
|
|
2224
2712
|
system: {
|
|
2225
|
-
backButton:
|
|
2226
|
-
homeButton:
|
|
2227
|
-
recentAppsButton:
|
|
2713
|
+
backButton: this.visualActions.back,
|
|
2714
|
+
homeButton: this.visualActions.home,
|
|
2715
|
+
recentAppsButton: this.visualActions.recentApps
|
|
2228
2716
|
}
|
|
2229
2717
|
});
|
|
2230
2718
|
node_assert(deviceId, 'deviceId is required for AndroidDevice');
|
|
@@ -2246,7 +2734,7 @@ const launchParamSchema = z.object({
|
|
|
2246
2734
|
const terminateParamSchema = z.object({
|
|
2247
2735
|
uri: z.string().describe('Package name or app name to terminate. Use the exact package name, e.g. com.android.settings.')
|
|
2248
2736
|
});
|
|
2249
|
-
const createPlatformActions = (
|
|
2737
|
+
const createPlatformActions = (visualActions)=>({
|
|
2250
2738
|
RunAdbShell: defineAction({
|
|
2251
2739
|
name: 'RunAdbShell',
|
|
2252
2740
|
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.',
|
|
@@ -2257,16 +2745,7 @@ const createPlatformActions = (device)=>({
|
|
|
2257
2745
|
},
|
|
2258
2746
|
call: async (param, context)=>{
|
|
2259
2747
|
if (!param.command || '' === param.command.trim()) throw new Error('RunAdbShell requires a non-empty command parameter');
|
|
2260
|
-
|
|
2261
|
-
const stdout = await runAdbShellStdoutOrThrow(adb, param.command, void 0 === param.timeout ? void 0 : {
|
|
2262
|
-
timeout: param.timeout
|
|
2263
|
-
});
|
|
2264
|
-
const planningFeedback = buildRunAdbShellPlanningFeedback({
|
|
2265
|
-
command: param.command,
|
|
2266
|
-
stdout
|
|
2267
|
-
});
|
|
2268
|
-
if (planningFeedback && context?.task) context.task.planningFeedback = planningFeedback;
|
|
2269
|
-
return stdout;
|
|
2748
|
+
return visualActions.runAdbShell(param, context);
|
|
2270
2749
|
}
|
|
2271
2750
|
}),
|
|
2272
2751
|
Launch: defineAction({
|
|
@@ -2279,7 +2758,7 @@ const createPlatformActions = (device)=>({
|
|
|
2279
2758
|
},
|
|
2280
2759
|
call: async (param)=>{
|
|
2281
2760
|
if (!param.uri || '' === param.uri.trim()) throw new Error('Launch requires a non-empty uri parameter');
|
|
2282
|
-
await
|
|
2761
|
+
await visualActions.launch(param.uri);
|
|
2283
2762
|
}
|
|
2284
2763
|
}),
|
|
2285
2764
|
Terminate: defineAction({
|
|
@@ -2289,7 +2768,7 @@ const createPlatformActions = (device)=>({
|
|
|
2289
2768
|
paramSchema: terminateParamSchema,
|
|
2290
2769
|
call: async (param)=>{
|
|
2291
2770
|
if (!param.uri || '' === param.uri.trim()) throw new Error('Terminate requires a non-empty uri parameter');
|
|
2292
|
-
await
|
|
2771
|
+
await visualActions.terminate(param.uri);
|
|
2293
2772
|
}
|
|
2294
2773
|
})
|
|
2295
2774
|
});
|