@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/lib/index.js
CHANGED
|
@@ -5,8 +5,10 @@ const __rslib_import_meta_url__ = /*#__PURE__*/ function() {
|
|
|
5
5
|
var __webpack_modules__ = {
|
|
6
6
|
"./src/scrcpy-manager.ts" (__unused_rspack_module, __webpack_exports__, __webpack_require__) {
|
|
7
7
|
__webpack_require__.d(__webpack_exports__, {
|
|
8
|
+
GJ: ()=>isScrcpyFreshFrameUnavailableError,
|
|
8
9
|
ScrcpyScreenshotManager: ()=>ScrcpyScreenshotManager,
|
|
9
|
-
|
|
10
|
+
Xr: ()=>SCRCPY_VIDEO_BIT_RATE_NETWORK_HINT,
|
|
11
|
+
ov: ()=>DEFAULT_SCRCPY_CONFIG
|
|
10
12
|
});
|
|
11
13
|
var node_fs__rspack_import_0 = __webpack_require__("node:fs");
|
|
12
14
|
var node_module__rspack_import_1 = __webpack_require__("node:module");
|
|
@@ -42,6 +44,14 @@ var __webpack_modules__ = {
|
|
|
42
44
|
const CONNECTION_WAIT_MS = 1000;
|
|
43
45
|
const MAX_SERVER_OUTPUT_LINES = 100;
|
|
44
46
|
const SERVER_OUTPUT_DRAIN_TIMEOUT_MS = 500;
|
|
47
|
+
const MAX_FRAME_AGE_US = 500000n;
|
|
48
|
+
const FRAME_FRESHNESS_WARN_INTERVAL_MS = 5000;
|
|
49
|
+
const TRANSPORT_BACKLOG_WARN_INTERVAL_MS = 5000;
|
|
50
|
+
const DEVICE_UPTIME_COMMAND = [
|
|
51
|
+
'dumpsys',
|
|
52
|
+
'power'
|
|
53
|
+
];
|
|
54
|
+
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.';
|
|
45
55
|
const BUSY_LOOP_WINDOW_MS = 1000;
|
|
46
56
|
const BUSY_LOOP_MAX_READS = 500;
|
|
47
57
|
const BUSY_LOOP_COOLDOWN_MS = 50;
|
|
@@ -52,6 +62,24 @@ var __webpack_modules__ = {
|
|
|
52
62
|
idleTimeoutMs: DEFAULT_IDLE_TIMEOUT_MS,
|
|
53
63
|
videoBitRate: DEFAULT_VIDEO_BIT_RATE
|
|
54
64
|
};
|
|
65
|
+
const SCRCPY_FRESH_FRAME_UNAVAILABLE_ERROR_CODE = 'ERR_SCRCPY_FRESH_FRAME_UNAVAILABLE';
|
|
66
|
+
class ScrcpyFreshFrameUnavailableError extends Error {
|
|
67
|
+
constructor(message, options){
|
|
68
|
+
super(message, options), _define_property(this, "code", SCRCPY_FRESH_FRAME_UNAVAILABLE_ERROR_CODE);
|
|
69
|
+
this.name = 'ScrcpyFreshFrameUnavailableError';
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function isScrcpyFreshFrameUnavailableError(error) {
|
|
73
|
+
return 'object' == typeof error && null !== error && 'code' in error && error.code === SCRCPY_FRESH_FRAME_UNAVAILABLE_ERROR_CODE;
|
|
74
|
+
}
|
|
75
|
+
function parseDeviceUptimeMs(output) {
|
|
76
|
+
const match = output.match(/mLastWakeTime=(\d+)\s+\((?:(\d+) ms ago|in (\d+) ms|now)\)/);
|
|
77
|
+
if (!match) throw new Error(`Unable to read Android device uptime from dumpsys power: ${output.trim() || '<empty output>'}`);
|
|
78
|
+
const referenceMs = BigInt(match[1]);
|
|
79
|
+
if (match[2]) return referenceMs + BigInt(match[2]);
|
|
80
|
+
if (match[3]) return referenceMs - BigInt(match[3]);
|
|
81
|
+
return referenceMs;
|
|
82
|
+
}
|
|
55
83
|
function isKeyFrameNalType(nalUnitType) {
|
|
56
84
|
return nalUnitType === NAL_TYPE_IDR || nalUnitType === NAL_TYPE_SPS || nalUnitType === NAL_TYPE_PPS;
|
|
57
85
|
}
|
|
@@ -234,24 +262,25 @@ var __webpack_modules__ = {
|
|
|
234
262
|
debugScrcpy(`Received SPS/PPS configuration: ${this.spsHeader.length}B`);
|
|
235
263
|
return;
|
|
236
264
|
}
|
|
265
|
+
const receivedAtUs = this.monotonicTimeUs();
|
|
266
|
+
if (!this.isFrameFresh(packet.pts)) return;
|
|
237
267
|
const frameBuffer = Buffer.from(packet.data);
|
|
238
268
|
const isKeyFrame = detectH264KeyFrame(frameBuffer);
|
|
239
269
|
if (isKeyFrame && this.spsHeader) {
|
|
270
|
+
const timing = this.estimateFrameTiming(packet.pts, receivedAtUs);
|
|
240
271
|
this.lastRawKeyframe = frameBuffer;
|
|
241
|
-
this.lastRawKeyframeAt =
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
capturedAt: this.lastRawKeyframeAt
|
|
254
|
-
};
|
|
272
|
+
this.lastRawKeyframeAt = timing.capturedAt;
|
|
273
|
+
this.lastRawKeyframePtsUs = packet.pts;
|
|
274
|
+
this.lastRawKeyframeEstimatedAgeMs = timing.estimatedAgeMs;
|
|
275
|
+
const frame = {
|
|
276
|
+
data: frameBuffer,
|
|
277
|
+
header: this.spsHeader,
|
|
278
|
+
ptsUs: packet.pts,
|
|
279
|
+
estimatedAgeMs: timing.estimatedAgeMs,
|
|
280
|
+
capturedAt: this.lastRawKeyframeAt
|
|
281
|
+
};
|
|
282
|
+
if (this.keyframeResolvers.length > 0) this.notifyKeyframeWaiters(frame);
|
|
283
|
+
if (this.keyframeListeners.size > 0 && this.isFrameAgeAcceptable(packet.pts, receivedAtUs)) {
|
|
255
284
|
for (const listener of this.keyframeListeners)try {
|
|
256
285
|
listener(frame);
|
|
257
286
|
} catch (error) {
|
|
@@ -261,6 +290,159 @@ var __webpack_modules__ = {
|
|
|
261
290
|
}
|
|
262
291
|
}
|
|
263
292
|
}
|
|
293
|
+
async readDeviceClockCalibration() {
|
|
294
|
+
const startedAtUs = this.monotonicTimeUs();
|
|
295
|
+
const startedAtWallMs = Date.now();
|
|
296
|
+
const shellProtocol = this.adb.subprocess.shellProtocol;
|
|
297
|
+
let output;
|
|
298
|
+
if (shellProtocol) {
|
|
299
|
+
const result = await shellProtocol.spawnWaitText(DEVICE_UPTIME_COMMAND);
|
|
300
|
+
if (0 !== result.exitCode) throw new Error(`Unable to read Android device uptime (exit ${result.exitCode}): ${result.stderr.trim() || result.stdout.trim()}`);
|
|
301
|
+
output = result.stdout;
|
|
302
|
+
} else output = await this.adb.subprocess.noneProtocol.spawnWaitText(DEVICE_UPTIME_COMMAND);
|
|
303
|
+
const finishedAtUs = this.monotonicTimeUs();
|
|
304
|
+
const finishedAtWallMs = Date.now();
|
|
305
|
+
return {
|
|
306
|
+
deviceUptimeUs: 1000n * parseDeviceUptimeMs(output),
|
|
307
|
+
hostMonotonicUs: startedAtUs + (finishedAtUs - startedAtUs) / 2n,
|
|
308
|
+
hostWallTimeMs: startedAtWallMs + (finishedAtWallMs - startedAtWallMs) / 2,
|
|
309
|
+
roundTripUs: finishedAtUs - startedAtUs
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
async ensureFrameClockCalibration() {
|
|
313
|
+
if (this.deviceClockCalibration) return;
|
|
314
|
+
this.deviceClockCalibration = await this.readDeviceClockCalibration();
|
|
315
|
+
debugScrcpy(`Calibrated scrcpy frame clock (RTT=${Number(this.deviceClockCalibration.roundTripUs / 1000n)}ms)`);
|
|
316
|
+
}
|
|
317
|
+
async setFreshnessBarrier(reason) {
|
|
318
|
+
const generation = ++this.frameFreshnessBarrierGeneration;
|
|
319
|
+
this.frameFreshnessBarrierPending = true;
|
|
320
|
+
this.clearFrameCache();
|
|
321
|
+
try {
|
|
322
|
+
const calibration = await this.readDeviceClockCalibration();
|
|
323
|
+
const barrierPtsUs = (calibration.deviceUptimeUs / 1000n + 1n) * 1000n;
|
|
324
|
+
if (generation !== this.frameFreshnessBarrierGeneration) return this.frameFreshnessBarrierPtsUs ?? barrierPtsUs;
|
|
325
|
+
this.deviceClockCalibration = calibration;
|
|
326
|
+
this.frameFreshnessBarrierPtsUs = null === this.frameFreshnessBarrierPtsUs || barrierPtsUs > this.frameFreshnessBarrierPtsUs ? barrierPtsUs : this.frameFreshnessBarrierPtsUs;
|
|
327
|
+
this.frameFreshnessBarrierReason = reason;
|
|
328
|
+
this.frameFreshnessBarrierPending = false;
|
|
329
|
+
this.frameFreshnessError = null;
|
|
330
|
+
this.lastFramePtsUs = null;
|
|
331
|
+
this.clearFrameCache();
|
|
332
|
+
debugScrcpy(`Armed frame freshness barrier at PTS ${this.frameFreshnessBarrierPtsUs}µs (${reason}, clock RTT=${Number(calibration.roundTripUs / 1000n)}ms)`);
|
|
333
|
+
return this.frameFreshnessBarrierPtsUs;
|
|
334
|
+
} catch (error) {
|
|
335
|
+
if (generation === this.frameFreshnessBarrierGeneration) {
|
|
336
|
+
this.frameFreshnessBarrierPending = false;
|
|
337
|
+
this.frameFreshnessError = new Error(`Unable to establish scrcpy frame freshness barrier (${reason}): ${error instanceof Error ? error.message : String(error)}`, {
|
|
338
|
+
cause: error
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
throw this.frameFreshnessError ?? error;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
isFrameFresh(packetPtsUs) {
|
|
345
|
+
if (this.frameFreshnessBarrierPending) return false;
|
|
346
|
+
if (void 0 !== packetPtsUs) {
|
|
347
|
+
if (null !== this.lastFramePtsUs && packetPtsUs < this.lastFramePtsUs) {
|
|
348
|
+
this.frameFreshnessError = new Error('Scrcpy frame PTS moved backwards; refusing frames until the device clock barrier is refreshed');
|
|
349
|
+
this.frameFreshnessBarrierPending = true;
|
|
350
|
+
this.clearFrameCache();
|
|
351
|
+
this.warnFrameFreshness();
|
|
352
|
+
return false;
|
|
353
|
+
}
|
|
354
|
+
this.lastFramePtsUs = packetPtsUs;
|
|
355
|
+
}
|
|
356
|
+
if (null === this.frameFreshnessBarrierPtsUs) return true;
|
|
357
|
+
if (void 0 === packetPtsUs) {
|
|
358
|
+
this.frameFreshnessError = new Error('Scrcpy frame has no PTS metadata; cannot prove that it is newer than the freshness barrier');
|
|
359
|
+
this.warnFrameFreshness();
|
|
360
|
+
return false;
|
|
361
|
+
}
|
|
362
|
+
if (packetPtsUs >= this.frameFreshnessBarrierPtsUs) {
|
|
363
|
+
if (this.frameFreshnessError) debugScrcpy(`Scrcpy video crossed the ${this.frameFreshnessBarrierReason ?? 'active'} freshness barrier at PTS ${packetPtsUs}µs`);
|
|
364
|
+
this.frameFreshnessError = null;
|
|
365
|
+
return true;
|
|
366
|
+
}
|
|
367
|
+
const behindBarrierUs = this.frameFreshnessBarrierPtsUs - packetPtsUs;
|
|
368
|
+
this.frameFreshnessError = new Error(`Scrcpy frame predates the ${this.frameFreshnessBarrierReason ?? 'active'} freshness barrier by ${Number(behindBarrierUs) / 1000}ms; refusing to use it`);
|
|
369
|
+
this.clearFrameCache();
|
|
370
|
+
this.warnFrameFreshness();
|
|
371
|
+
return false;
|
|
372
|
+
}
|
|
373
|
+
estimateFrameAgeUs(packetPtsUs, hostMonotonicUs = this.monotonicTimeUs()) {
|
|
374
|
+
const calibration = this.deviceClockCalibration;
|
|
375
|
+
if (void 0 === packetPtsUs || !calibration) return null;
|
|
376
|
+
const estimatedDeviceNowUs = calibration.deviceUptimeUs + (hostMonotonicUs - calibration.hostMonotonicUs);
|
|
377
|
+
return estimatedDeviceNowUs > packetPtsUs ? estimatedDeviceNowUs - packetPtsUs : 0n;
|
|
378
|
+
}
|
|
379
|
+
isFrameAgeAcceptable(packetPtsUs, hostMonotonicUs = this.monotonicTimeUs()) {
|
|
380
|
+
const ageUs = this.estimateFrameAgeUs(packetPtsUs, hostMonotonicUs);
|
|
381
|
+
if (null === ageUs) {
|
|
382
|
+
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');
|
|
383
|
+
this.warnFrameFreshness();
|
|
384
|
+
return false;
|
|
385
|
+
}
|
|
386
|
+
if (ageUs <= MAX_FRAME_AGE_US) {
|
|
387
|
+
if (this.frameFreshnessError) {
|
|
388
|
+
debugScrcpy(`Scrcpy frame age recovered (${Number(ageUs / 1000n)}ms)`);
|
|
389
|
+
this.frameFreshnessError = null;
|
|
390
|
+
}
|
|
391
|
+
return true;
|
|
392
|
+
}
|
|
393
|
+
this.frameFreshnessError = new Error(`Scrcpy frame absolute age is ${Number(ageUs / 1000n)}ms, exceeding the ${Number(MAX_FRAME_AGE_US / 1000n)}ms limit`);
|
|
394
|
+
this.warnFrameFreshness();
|
|
395
|
+
return false;
|
|
396
|
+
}
|
|
397
|
+
warnFrameFreshness() {
|
|
398
|
+
if (!this.frameFreshnessError) return;
|
|
399
|
+
const now = Date.now();
|
|
400
|
+
if (now - this.lastFrameFreshnessWarningAt >= FRAME_FRESHNESS_WARN_INTERVAL_MS) {
|
|
401
|
+
warnScrcpy(this.frameFreshnessError.message);
|
|
402
|
+
this.lastFrameFreshnessWarningAt = now;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
warnTransportBacklog(error) {
|
|
406
|
+
const now = Date.now();
|
|
407
|
+
if (now - this.lastTransportBacklogWarningAt < TRANSPORT_BACKLOG_WARN_INTERVAL_MS) return;
|
|
408
|
+
const cause = this.frameFreshnessError ?? error;
|
|
409
|
+
const causeMessage = cause instanceof Error ? cause.message : String(cause);
|
|
410
|
+
const currentBitRateMbps = this.options.videoBitRate / 1000000;
|
|
411
|
+
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}`);
|
|
412
|
+
this.lastTransportBacklogWarningAt = now;
|
|
413
|
+
}
|
|
414
|
+
estimateFrameTiming(packetPtsUs, receivedAtUs) {
|
|
415
|
+
const calibration = this.deviceClockCalibration;
|
|
416
|
+
if (void 0 === packetPtsUs || !calibration) return {
|
|
417
|
+
capturedAt: Date.now()
|
|
418
|
+
};
|
|
419
|
+
const estimatedAgeUs = this.estimateFrameAgeUs(packetPtsUs, receivedAtUs) ?? 0n;
|
|
420
|
+
const estimatedAgeMs = Number(estimatedAgeUs) / 1000;
|
|
421
|
+
const receivedAtWallTimeMs = calibration.hostWallTimeMs + Number(receivedAtUs - calibration.hostMonotonicUs) / 1000;
|
|
422
|
+
return {
|
|
423
|
+
capturedAt: receivedAtWallTimeMs - estimatedAgeMs,
|
|
424
|
+
estimatedAgeMs
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
clearFrameCache() {
|
|
428
|
+
this.lastRawKeyframe = null;
|
|
429
|
+
this.lastRawKeyframeAt = 0;
|
|
430
|
+
this.lastRawKeyframePtsUs = void 0;
|
|
431
|
+
this.lastRawKeyframeEstimatedAgeMs = void 0;
|
|
432
|
+
}
|
|
433
|
+
monotonicTimeUs() {
|
|
434
|
+
return process.hrtime.bigint() / 1000n;
|
|
435
|
+
}
|
|
436
|
+
resetFrameFreshnessState() {
|
|
437
|
+
this.frameFreshnessBarrierPtsUs = null;
|
|
438
|
+
this.frameFreshnessBarrierReason = null;
|
|
439
|
+
this.frameFreshnessBarrierPending = false;
|
|
440
|
+
this.frameFreshnessBarrierGeneration = 0;
|
|
441
|
+
this.deviceClockCalibration = null;
|
|
442
|
+
this.lastFramePtsUs = null;
|
|
443
|
+
this.frameFreshnessError = null;
|
|
444
|
+
this.lastFrameFreshnessWarningAt = 0;
|
|
445
|
+
}
|
|
264
446
|
subscribeKeyframes(listener) {
|
|
265
447
|
this.keyframeListeners.add(listener);
|
|
266
448
|
this.resetIdleTimer();
|
|
@@ -270,10 +452,17 @@ var __webpack_modules__ = {
|
|
|
270
452
|
};
|
|
271
453
|
}
|
|
272
454
|
getLatestRawKeyframe() {
|
|
455
|
+
const frame = this.getCachedKeyframeCandidate();
|
|
456
|
+
if (!frame || !this.isFrameAgeAcceptable(frame.ptsUs)) return null;
|
|
457
|
+
return frame;
|
|
458
|
+
}
|
|
459
|
+
getCachedKeyframeCandidate() {
|
|
273
460
|
if (!this.lastRawKeyframe || !this.spsHeader) return null;
|
|
274
461
|
return {
|
|
275
462
|
data: this.lastRawKeyframe,
|
|
276
463
|
header: this.spsHeader,
|
|
464
|
+
ptsUs: this.lastRawKeyframePtsUs,
|
|
465
|
+
estimatedAgeMs: this.lastRawKeyframeEstimatedAgeMs,
|
|
277
466
|
capturedAt: this.lastRawKeyframeAt
|
|
278
467
|
};
|
|
279
468
|
}
|
|
@@ -283,55 +472,94 @@ var __webpack_modules__ = {
|
|
|
283
472
|
frame.data
|
|
284
473
|
]));
|
|
285
474
|
}
|
|
475
|
+
async waitForUsableKeyframe(timeoutMs) {
|
|
476
|
+
const deadline = Date.now() + timeoutMs;
|
|
477
|
+
let candidate = this.getCachedKeyframeCandidate();
|
|
478
|
+
while(true){
|
|
479
|
+
if (candidate && this.isFrameAgeAcceptable(candidate.ptsUs)) return candidate;
|
|
480
|
+
const remainingMs = deadline - Date.now();
|
|
481
|
+
if (remainingMs <= 0) throw new Error(`No fresh keyframe received within ${timeoutMs}ms`);
|
|
482
|
+
candidate = await this.waitForNextKeyframe(remainingMs);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
async prepareFreshFrame() {
|
|
486
|
+
await this.ensureConnected();
|
|
487
|
+
await this.ensureFrameClockCalibration();
|
|
488
|
+
await this.waitForKeyframe();
|
|
489
|
+
await this.waitForUsableKeyframe(MAX_KEYFRAME_WAIT_MS);
|
|
490
|
+
}
|
|
491
|
+
async waitForPlanningFrame() {
|
|
492
|
+
let planningBarrierArmed = false;
|
|
493
|
+
let deadline = Date.now() + FRESH_FRAME_TIMEOUT_MS;
|
|
494
|
+
let candidate = this.getCachedKeyframeCandidate();
|
|
495
|
+
while(true){
|
|
496
|
+
if (candidate) {
|
|
497
|
+
if (void 0 === candidate.ptsUs) throw new Error('Scrcpy frame has no PTS metadata; cannot prove planning freshness');
|
|
498
|
+
const ageUs = this.estimateFrameAgeUs(candidate.ptsUs);
|
|
499
|
+
if (null === ageUs) throw new Error('Scrcpy frame clock is not calibrated; cannot prove planning freshness');
|
|
500
|
+
if (ageUs <= MAX_FRAME_AGE_US) return candidate;
|
|
501
|
+
if (planningBarrierArmed) this.clearFrameCache();
|
|
502
|
+
else {
|
|
503
|
+
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`);
|
|
504
|
+
await this.setFreshnessBarrier('stale planning frame');
|
|
505
|
+
planningBarrierArmed = true;
|
|
506
|
+
deadline = Date.now() + FRESH_FRAME_TIMEOUT_MS;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
const remainingMs = deadline - Date.now();
|
|
510
|
+
if (remainingMs <= 0) throw new Error(`No scrcpy frame crossed the active freshness target within ${FRESH_FRAME_TIMEOUT_MS}ms`);
|
|
511
|
+
candidate = await this.waitForNextKeyframe(remainingMs);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
async closeStaleStreamAndCreateFallbackError(error) {
|
|
515
|
+
this.warnTransportBacklog(error);
|
|
516
|
+
const causeMessage = error instanceof Error ? error.message : String(error);
|
|
517
|
+
await this.disconnect();
|
|
518
|
+
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}`, {
|
|
519
|
+
cause: error
|
|
520
|
+
});
|
|
521
|
+
}
|
|
286
522
|
async getScreenshotJpeg() {
|
|
287
523
|
const perfStart = Date.now();
|
|
288
524
|
const t1 = Date.now();
|
|
289
525
|
await this.ensureConnected();
|
|
290
526
|
const connectTime = Date.now() - t1;
|
|
291
527
|
const t2 = Date.now();
|
|
292
|
-
|
|
293
|
-
const spsWaitTime = Date.now() - t2;
|
|
294
|
-
const t3 = Date.now();
|
|
295
|
-
let keyframeBuffer;
|
|
296
|
-
let frameSource;
|
|
528
|
+
let frame;
|
|
297
529
|
try {
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
this.spsHeader,
|
|
304
|
-
this.lastRawKeyframe
|
|
305
|
-
]);
|
|
306
|
-
frameSource = 'cached';
|
|
307
|
-
} else {
|
|
308
|
-
keyframeBuffer = await this.waitForNextKeyframe(MAX_KEYFRAME_WAIT_MS);
|
|
309
|
-
frameSource = 'fresh-retry';
|
|
310
|
-
}
|
|
530
|
+
await this.ensureFrameClockCalibration();
|
|
531
|
+
await this.waitForKeyframe();
|
|
532
|
+
frame = await this.waitForPlanningFrame();
|
|
533
|
+
} catch (error) {
|
|
534
|
+
throw await this.closeStaleStreamAndCreateFallbackError(error);
|
|
311
535
|
}
|
|
312
|
-
const frameWaitTime = Date.now() -
|
|
536
|
+
const frameWaitTime = Date.now() - t2;
|
|
537
|
+
const keyframeBuffer = Buffer.concat([
|
|
538
|
+
frame.header,
|
|
539
|
+
frame.data
|
|
540
|
+
]);
|
|
313
541
|
this.resetIdleTimer();
|
|
314
|
-
debugScrcpy(`Decoding H.264 stream: ${keyframeBuffer.length} bytes (
|
|
315
|
-
const
|
|
542
|
+
debugScrcpy(`Decoding H.264 stream: ${keyframeBuffer.length} bytes (post-barrier)`);
|
|
543
|
+
const t5 = Date.now();
|
|
316
544
|
const result = await this.decodeH264ToJpeg(keyframeBuffer);
|
|
317
|
-
const decodeTime = Date.now() -
|
|
545
|
+
const decodeTime = Date.now() - t5;
|
|
318
546
|
const totalTime = Date.now() - perfStart;
|
|
319
|
-
debugScrcpy(`Performance: total=${totalTime}ms (connect=${connectTime}ms,
|
|
547
|
+
debugScrcpy(`Performance: total=${totalTime}ms (connect=${connectTime}ms, frameWait=${frameWaitTime}ms, decode=${decodeTime}ms)`);
|
|
320
548
|
return result;
|
|
321
549
|
}
|
|
322
550
|
getResolution() {
|
|
323
551
|
return this.videoResolution;
|
|
324
552
|
}
|
|
325
|
-
notifyKeyframeWaiters(
|
|
553
|
+
notifyKeyframeWaiters(frame) {
|
|
326
554
|
const resolvers = this.keyframeResolvers;
|
|
327
555
|
this.keyframeResolvers = [];
|
|
328
|
-
for (const resolve of resolvers)resolve(
|
|
556
|
+
for (const resolve of resolvers)resolve(frame);
|
|
329
557
|
}
|
|
330
558
|
waitForNextKeyframe(timeoutMs) {
|
|
331
559
|
return new Promise((resolve, reject)=>{
|
|
332
|
-
const wrappedResolve = (
|
|
560
|
+
const wrappedResolve = (frame)=>{
|
|
333
561
|
clearTimeout(timer);
|
|
334
|
-
resolve(
|
|
562
|
+
resolve(frame);
|
|
335
563
|
};
|
|
336
564
|
const timer = setTimeout(()=>{
|
|
337
565
|
this.keyframeResolvers = this.keyframeResolvers.filter((r)=>r !== wrappedResolve);
|
|
@@ -454,11 +682,11 @@ var __webpack_modules__ = {
|
|
|
454
682
|
this.videoStream = null;
|
|
455
683
|
this.streamReader = null;
|
|
456
684
|
this.spsHeader = null;
|
|
457
|
-
this.
|
|
458
|
-
this.lastRawKeyframeAt = 0;
|
|
685
|
+
this.clearFrameCache();
|
|
459
686
|
this.isInitialized = false;
|
|
460
687
|
this.keyframeResolvers = [];
|
|
461
688
|
this.keyframeListeners.clear();
|
|
689
|
+
this.resetFrameFreshnessState();
|
|
462
690
|
if (reader) try {
|
|
463
691
|
await reader.cancel();
|
|
464
692
|
} catch (error) {
|
|
@@ -488,8 +716,19 @@ var __webpack_modules__ = {
|
|
|
488
716
|
_define_property(this, "keyframeListeners", new Set());
|
|
489
717
|
_define_property(this, "lastRawKeyframe", null);
|
|
490
718
|
_define_property(this, "lastRawKeyframeAt", 0);
|
|
719
|
+
_define_property(this, "lastRawKeyframePtsUs", void 0);
|
|
720
|
+
_define_property(this, "lastRawKeyframeEstimatedAgeMs", void 0);
|
|
491
721
|
_define_property(this, "videoResolution", null);
|
|
492
722
|
_define_property(this, "streamReader", null);
|
|
723
|
+
_define_property(this, "frameFreshnessBarrierPtsUs", null);
|
|
724
|
+
_define_property(this, "frameFreshnessBarrierReason", null);
|
|
725
|
+
_define_property(this, "frameFreshnessBarrierPending", false);
|
|
726
|
+
_define_property(this, "frameFreshnessBarrierGeneration", 0);
|
|
727
|
+
_define_property(this, "deviceClockCalibration", null);
|
|
728
|
+
_define_property(this, "lastFramePtsUs", null);
|
|
729
|
+
_define_property(this, "frameFreshnessError", null);
|
|
730
|
+
_define_property(this, "lastFrameFreshnessWarningAt", 0);
|
|
731
|
+
_define_property(this, "lastTransportBacklogWarningAt", 0);
|
|
493
732
|
this.adb = adb;
|
|
494
733
|
const requestedBitRate = options.videoBitRate ?? DEFAULT_VIDEO_BIT_RATE;
|
|
495
734
|
const clampedBitRate = Math.min(requestedBitRate, MAX_VIDEO_BIT_RATE);
|
|
@@ -672,6 +911,7 @@ ${stdout ? truncateAdbShellStream(stdout, 'stdout') : EMPTY_ADB_SHELL_STDOUT}`;
|
|
|
672
911
|
class ScrcpyDeviceAdapter {
|
|
673
912
|
isEnabled() {
|
|
674
913
|
if (!this.isConfigured()) return false;
|
|
914
|
+
if (this.freshnessRecoveryPending || this.recoveryPromise) return false;
|
|
675
915
|
return null === this.retryAfter || Date.now() >= this.retryAfter;
|
|
676
916
|
}
|
|
677
917
|
getStatus() {
|
|
@@ -683,12 +923,18 @@ ${stdout ? truncateAdbShellStream(stdout, 'stdout') : EMPTY_ADB_SHELL_STDOUT}`;
|
|
|
683
923
|
};
|
|
684
924
|
}
|
|
685
925
|
isConfigured() {
|
|
686
|
-
return this.scrcpyConfig?.enabled ?? scrcpy_manager.
|
|
926
|
+
return this.scrcpyConfig?.enabled ?? scrcpy_manager.ov.enabled;
|
|
687
927
|
}
|
|
688
928
|
async initialize(deviceInfo) {
|
|
929
|
+
if (this.recoveryPromise) {
|
|
930
|
+
await this.recoveryPromise;
|
|
931
|
+
if (this.manager?.isConnected()) return;
|
|
932
|
+
}
|
|
933
|
+
this.freshnessRecoveryPending = false;
|
|
689
934
|
try {
|
|
690
935
|
const manager = await this.ensureManager(deviceInfo);
|
|
691
936
|
await manager.ensureConnected();
|
|
937
|
+
await this.applyPendingActionBarrier(manager);
|
|
692
938
|
this.clearFailure();
|
|
693
939
|
} catch (error) {
|
|
694
940
|
this.recordFailure(error);
|
|
@@ -698,24 +944,27 @@ ${stdout ? truncateAdbShellStream(stdout, 'stdout') : EMPTY_ADB_SHELL_STDOUT}`;
|
|
|
698
944
|
recordFailure(error) {
|
|
699
945
|
this.lastError = error instanceof Error ? error.message : String(error);
|
|
700
946
|
this.retryAfter = Date.now() + SCRCPY_RETRY_COOLDOWN_MS;
|
|
947
|
+
this.freshnessRecoveryPending = false;
|
|
701
948
|
}
|
|
702
949
|
clearFailure() {
|
|
703
950
|
this.lastError = null;
|
|
704
951
|
this.retryAfter = null;
|
|
952
|
+
this.freshnessRecoveryPending = false;
|
|
705
953
|
}
|
|
706
954
|
ensureRetryReady() {
|
|
955
|
+
if (this.freshnessRecoveryPending || this.recoveryPromise) throw new Error('scrcpy freshness recovery is in progress');
|
|
707
956
|
if (null === this.retryAfter || Date.now() >= this.retryAfter) return;
|
|
708
957
|
throw new Error(`scrcpy retry is cooling down until ${new Date(this.retryAfter).toISOString()}. Last error: ${this.lastError}`);
|
|
709
958
|
}
|
|
710
959
|
resolveConfig(deviceInfo) {
|
|
711
960
|
if (this.resolvedConfig) return this.resolvedConfig;
|
|
712
961
|
const config = this.scrcpyConfig;
|
|
713
|
-
const maxSize = config?.maxSize ?? scrcpy_manager.
|
|
714
|
-
const videoBitRate = config?.videoBitRate ?? scrcpy_manager.
|
|
962
|
+
const maxSize = config?.maxSize ?? scrcpy_manager.ov.maxSize;
|
|
963
|
+
const videoBitRate = config?.videoBitRate ?? scrcpy_manager.ov.videoBitRate;
|
|
715
964
|
this.resolvedConfig = {
|
|
716
965
|
enabled: this.isConfigured(),
|
|
717
966
|
maxSize,
|
|
718
|
-
idleTimeoutMs: config?.idleTimeoutMs ?? scrcpy_manager.
|
|
967
|
+
idleTimeoutMs: config?.idleTimeoutMs ?? scrcpy_manager.ov.idleTimeoutMs,
|
|
719
968
|
videoBitRate
|
|
720
969
|
};
|
|
721
970
|
return this.resolvedConfig;
|
|
@@ -749,24 +998,40 @@ ${stdout ? truncateAdbShellStream(stdout, 'stdout') : EMPTY_ADB_SHELL_STDOUT}`;
|
|
|
749
998
|
}
|
|
750
999
|
async screenshotBase64(deviceInfo) {
|
|
751
1000
|
this.ensureRetryReady();
|
|
1001
|
+
let manager = null;
|
|
752
1002
|
try {
|
|
753
|
-
|
|
1003
|
+
manager = await this.ensureManager(deviceInfo);
|
|
1004
|
+
await manager.ensureConnected();
|
|
1005
|
+
await this.applyPendingActionBarrier(manager);
|
|
754
1006
|
const screenshotBuffer = await manager.getScreenshotJpeg();
|
|
755
1007
|
this.clearFailure();
|
|
756
1008
|
return (0, img_namespaceObject.createImgBase64ByFormat)('jpeg', screenshotBuffer.toString('base64'));
|
|
757
1009
|
} catch (error) {
|
|
1010
|
+
if ((0, scrcpy_manager.GJ)(error)) {
|
|
1011
|
+
this.markFreshnessRecoveryPending(manager, error);
|
|
1012
|
+
throw error;
|
|
1013
|
+
}
|
|
758
1014
|
this.recordFailure(error);
|
|
759
1015
|
throw error;
|
|
760
1016
|
}
|
|
761
1017
|
}
|
|
762
1018
|
async subscribeKeyframes(deviceInfo, listener) {
|
|
763
1019
|
this.ensureRetryReady();
|
|
1020
|
+
this.keyframeListeners.add(listener);
|
|
764
1021
|
try {
|
|
765
1022
|
const manager = await this.ensureManager(deviceInfo);
|
|
766
1023
|
await manager.ensureConnected();
|
|
1024
|
+
await this.applyPendingActionBarrier(manager);
|
|
1025
|
+
await manager.ensureFrameClockCalibration();
|
|
767
1026
|
this.clearFailure();
|
|
768
|
-
|
|
1027
|
+
this.attachKeyframeListener(manager, listener);
|
|
1028
|
+
return ()=>{
|
|
1029
|
+
this.keyframeListeners.delete(listener);
|
|
1030
|
+
this.keyframeUnsubscribers.get(listener)?.();
|
|
1031
|
+
this.keyframeUnsubscribers.delete(listener);
|
|
1032
|
+
};
|
|
769
1033
|
} catch (error) {
|
|
1034
|
+
this.keyframeListeners.delete(listener);
|
|
770
1035
|
this.recordFailure(error);
|
|
771
1036
|
throw error;
|
|
772
1037
|
}
|
|
@@ -774,6 +1039,83 @@ ${stdout ? truncateAdbShellStream(stdout, 'stdout') : EMPTY_ADB_SHELL_STDOUT}`;
|
|
|
774
1039
|
getLatestRawKeyframe() {
|
|
775
1040
|
return this.manager?.getLatestRawKeyframe() ?? null;
|
|
776
1041
|
}
|
|
1042
|
+
attachKeyframeListener(manager, listener) {
|
|
1043
|
+
this.keyframeUnsubscribers.get(listener)?.();
|
|
1044
|
+
this.keyframeUnsubscribers.set(listener, manager.subscribeKeyframes(listener));
|
|
1045
|
+
}
|
|
1046
|
+
attachKeyframeListeners(manager) {
|
|
1047
|
+
this.keyframeUnsubscribers.clear();
|
|
1048
|
+
for (const listener of this.keyframeListeners)this.attachKeyframeListener(manager, listener);
|
|
1049
|
+
}
|
|
1050
|
+
markFreshnessRecoveryPending(manager, error) {
|
|
1051
|
+
this.lastError = error.message;
|
|
1052
|
+
this.retryAfter = null;
|
|
1053
|
+
this.freshnessRecoveryPending = true;
|
|
1054
|
+
this.keyframeUnsubscribers.clear();
|
|
1055
|
+
if (manager && this.manager === manager) this.manager = null;
|
|
1056
|
+
}
|
|
1057
|
+
async applyPendingActionBarrier(manager) {
|
|
1058
|
+
if (!this.pendingActionBarrier) return;
|
|
1059
|
+
await manager.setFreshnessBarrier('completed input action while scrcpy was unavailable');
|
|
1060
|
+
this.pendingActionBarrier = false;
|
|
1061
|
+
}
|
|
1062
|
+
recoverAfterAdbScreenshot(deviceInfo) {
|
|
1063
|
+
if (!this.freshnessRecoveryPending || this.recoveryPromise) return;
|
|
1064
|
+
const generation = this.lifecycleGeneration;
|
|
1065
|
+
const recovery = (async ()=>{
|
|
1066
|
+
let manager = null;
|
|
1067
|
+
try {
|
|
1068
|
+
manager = await this.ensureManager(deviceInfo);
|
|
1069
|
+
await manager.ensureConnected();
|
|
1070
|
+
await this.applyPendingActionBarrier(manager);
|
|
1071
|
+
await manager.prepareFreshFrame();
|
|
1072
|
+
if (generation !== this.lifecycleGeneration) {
|
|
1073
|
+
await manager.disconnect();
|
|
1074
|
+
if (this.manager === manager) this.manager = null;
|
|
1075
|
+
return;
|
|
1076
|
+
}
|
|
1077
|
+
this.attachKeyframeListeners(manager);
|
|
1078
|
+
this.clearFailure();
|
|
1079
|
+
debugAdapter('Scrcpy freshness recovery completed in background');
|
|
1080
|
+
} catch (error) {
|
|
1081
|
+
if (manager) {
|
|
1082
|
+
await manager.disconnect();
|
|
1083
|
+
if (this.manager === manager) this.manager = null;
|
|
1084
|
+
}
|
|
1085
|
+
this.freshnessRecoveryPending = false;
|
|
1086
|
+
this.recordFailure(error);
|
|
1087
|
+
debugAdapter(`Scrcpy background freshness recovery failed: ${error}`);
|
|
1088
|
+
throw error;
|
|
1089
|
+
}
|
|
1090
|
+
})();
|
|
1091
|
+
this.recoveryPromise = recovery;
|
|
1092
|
+
recovery.catch(()=>{}).finally(()=>{
|
|
1093
|
+
if (this.recoveryPromise === recovery) this.recoveryPromise = null;
|
|
1094
|
+
});
|
|
1095
|
+
}
|
|
1096
|
+
async markActionBarrier() {
|
|
1097
|
+
const manager = this.manager;
|
|
1098
|
+
if (!manager?.isConnected()) {
|
|
1099
|
+
this.pendingActionBarrier = true;
|
|
1100
|
+
return;
|
|
1101
|
+
}
|
|
1102
|
+
try {
|
|
1103
|
+
await manager.setFreshnessBarrier('completed input action');
|
|
1104
|
+
this.pendingActionBarrier = false;
|
|
1105
|
+
this.clearFailure();
|
|
1106
|
+
} catch (error) {
|
|
1107
|
+
this.pendingActionBarrier = true;
|
|
1108
|
+
this.recordFailure(error);
|
|
1109
|
+
debugAdapter(`Unable to mark scrcpy action barrier; disabling this stream: ${error}`);
|
|
1110
|
+
try {
|
|
1111
|
+
await manager.disconnect();
|
|
1112
|
+
} catch (disconnectError) {
|
|
1113
|
+
debugAdapter(`Error disconnecting scrcpy after barrier failure: ${disconnectError}`);
|
|
1114
|
+
} finally{
|
|
1115
|
+
if (this.manager === manager) this.manager = null;
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
777
1119
|
async decodeRawKeyframeToJpegBase64(frame) {
|
|
778
1120
|
if (!this.manager) throw new Error('scrcpy manager is not initialized');
|
|
779
1121
|
const jpegBuffer = await this.manager.decodeRawKeyframeToJpeg(frame);
|
|
@@ -797,6 +1139,13 @@ ${stdout ? truncateAdbShellStream(stdout, 'stdout') : EMPTY_ADB_SHELL_STDOUT}`;
|
|
|
797
1139
|
return resolution.width / physicalWidth;
|
|
798
1140
|
}
|
|
799
1141
|
async disconnect() {
|
|
1142
|
+
this.lifecycleGeneration += 1;
|
|
1143
|
+
this.freshnessRecoveryPending = false;
|
|
1144
|
+
this.pendingActionBarrier = false;
|
|
1145
|
+
for (const unsubscribe of this.keyframeUnsubscribers.values())unsubscribe();
|
|
1146
|
+
this.keyframeUnsubscribers.clear();
|
|
1147
|
+
this.keyframeListeners.clear();
|
|
1148
|
+
if (this.recoveryPromise) await this.recoveryPromise.catch(()=>{});
|
|
800
1149
|
if (this.manager) {
|
|
801
1150
|
try {
|
|
802
1151
|
await this.manager.disconnect();
|
|
@@ -816,6 +1165,12 @@ ${stdout ? truncateAdbShellStream(stdout, 'stdout') : EMPTY_ADB_SHELL_STDOUT}`;
|
|
|
816
1165
|
_define_property(this, "resolvedConfig", void 0);
|
|
817
1166
|
_define_property(this, "lastError", void 0);
|
|
818
1167
|
_define_property(this, "retryAfter", void 0);
|
|
1168
|
+
_define_property(this, "freshnessRecoveryPending", void 0);
|
|
1169
|
+
_define_property(this, "recoveryPromise", void 0);
|
|
1170
|
+
_define_property(this, "lifecycleGeneration", void 0);
|
|
1171
|
+
_define_property(this, "pendingActionBarrier", void 0);
|
|
1172
|
+
_define_property(this, "keyframeListeners", void 0);
|
|
1173
|
+
_define_property(this, "keyframeUnsubscribers", void 0);
|
|
819
1174
|
this.deviceId = deviceId;
|
|
820
1175
|
this.scrcpyConfig = scrcpyConfig;
|
|
821
1176
|
this.resolveAdbServerEndpoint = resolveAdbServerEndpoint;
|
|
@@ -823,6 +1178,12 @@ ${stdout ? truncateAdbShellStream(stdout, 'stdout') : EMPTY_ADB_SHELL_STDOUT}`;
|
|
|
823
1178
|
this.resolvedConfig = null;
|
|
824
1179
|
this.lastError = null;
|
|
825
1180
|
this.retryAfter = null;
|
|
1181
|
+
this.freshnessRecoveryPending = false;
|
|
1182
|
+
this.recoveryPromise = null;
|
|
1183
|
+
this.lifecycleGeneration = 0;
|
|
1184
|
+
this.pendingActionBarrier = false;
|
|
1185
|
+
this.keyframeListeners = new Set();
|
|
1186
|
+
this.keyframeUnsubscribers = new Map();
|
|
826
1187
|
}
|
|
827
1188
|
}
|
|
828
1189
|
const TAG_RE = /<(\/?)([A-Za-z_][A-Za-z0-9_.\-:]*)([^>]*?)(\/?)>/g;
|
|
@@ -1049,6 +1410,20 @@ ${stdout ? truncateAdbShellStream(stdout, 'stdout') : EMPTY_ADB_SHELL_STDOUT}`;
|
|
|
1049
1410
|
}
|
|
1050
1411
|
throw new Error(`Unable to capture Android UI tree after ${failures.length} attempt(s): ${failures.join('; ')}`);
|
|
1051
1412
|
}
|
|
1413
|
+
function createVisualActionRegistry(definitions, onActionSettled) {
|
|
1414
|
+
const registeredActions = {};
|
|
1415
|
+
for (const actionName of Object.keys(definitions)){
|
|
1416
|
+
const dispatch = definitions[actionName];
|
|
1417
|
+
registeredActions[actionName] = async (...args)=>{
|
|
1418
|
+
try {
|
|
1419
|
+
return await dispatch(...args);
|
|
1420
|
+
} finally{
|
|
1421
|
+
await onActionSettled(actionName);
|
|
1422
|
+
}
|
|
1423
|
+
};
|
|
1424
|
+
}
|
|
1425
|
+
return registeredActions;
|
|
1426
|
+
}
|
|
1052
1427
|
function device_define_property(obj, key, value) {
|
|
1053
1428
|
if (key in obj) Object.defineProperty(obj, key, {
|
|
1054
1429
|
value: value,
|
|
@@ -1149,19 +1524,12 @@ ${stdout ? truncateAdbShellStream(stdout, 'stdout') : EMPTY_ADB_SHELL_STDOUT}`;
|
|
|
1149
1524
|
}
|
|
1150
1525
|
},
|
|
1151
1526
|
call: async (param)=>{
|
|
1152
|
-
const element = param.locate;
|
|
1153
|
-
const startPoint = element ? {
|
|
1154
|
-
left: element.center[0],
|
|
1155
|
-
top: element.center[1]
|
|
1156
|
-
} : void 0;
|
|
1157
1527
|
if (!param || !param.direction) throw new Error('PullGesture requires a direction parameter');
|
|
1158
|
-
|
|
1159
|
-
else if ('up' === param.direction) await this.pullUp(startPoint, param.distance, param.duration);
|
|
1160
|
-
else throw new Error(`Unknown pull direction: ${param.direction}`);
|
|
1528
|
+
await this.visualActions.pullGesture(param);
|
|
1161
1529
|
}
|
|
1162
1530
|
})
|
|
1163
1531
|
];
|
|
1164
|
-
const platformActions = createPlatformActions(this);
|
|
1532
|
+
const platformActions = createPlatformActions(this.visualActions);
|
|
1165
1533
|
let platformSpecificActions = Object.values(platformActions);
|
|
1166
1534
|
if (this.options?.exposeRunAdbShellAction === false) platformSpecificActions = platformSpecificActions.filter((action)=>action !== platformActions.RunAdbShell);
|
|
1167
1535
|
const customActions = this.customActions || [];
|
|
@@ -1171,6 +1539,16 @@ ${stdout ? truncateAdbShellStream(stdout, 'stdout') : EMPTY_ADB_SHELL_STDOUT}`;
|
|
|
1171
1539
|
...customActions
|
|
1172
1540
|
];
|
|
1173
1541
|
}
|
|
1542
|
+
async performPullGesture(param) {
|
|
1543
|
+
const element = param.locate;
|
|
1544
|
+
const startPoint = element ? {
|
|
1545
|
+
left: element.center[0],
|
|
1546
|
+
top: element.center[1]
|
|
1547
|
+
} : void 0;
|
|
1548
|
+
if ('down' === param.direction) await this.pullDownRaw(startPoint, param.distance, param.duration);
|
|
1549
|
+
else if ('up' === param.direction) await this.pullUpRaw(startPoint, param.distance, param.duration);
|
|
1550
|
+
else throw new Error(`Unknown pull direction: ${param.direction}`);
|
|
1551
|
+
}
|
|
1174
1552
|
async performActionScroll(param) {
|
|
1175
1553
|
const element = param.locate;
|
|
1176
1554
|
const startingPoint = element ? {
|
|
@@ -1178,20 +1556,32 @@ ${stdout ? truncateAdbShellStream(stdout, 'stdout') : EMPTY_ADB_SHELL_STDOUT}`;
|
|
|
1178
1556
|
top: element.center[1]
|
|
1179
1557
|
} : void 0;
|
|
1180
1558
|
const scrollToEventName = param?.scrollType;
|
|
1181
|
-
if ('scrollToTop' === scrollToEventName) await this.
|
|
1182
|
-
else if ('scrollToBottom' === scrollToEventName) await this.
|
|
1183
|
-
else if ('scrollToRight' === scrollToEventName) await this.
|
|
1184
|
-
else if ('scrollToLeft' === scrollToEventName) await this.
|
|
1559
|
+
if ('scrollToTop' === scrollToEventName) await this.scrollUntilTopRaw(startingPoint);
|
|
1560
|
+
else if ('scrollToBottom' === scrollToEventName) await this.scrollUntilBottomRaw(startingPoint);
|
|
1561
|
+
else if ('scrollToRight' === scrollToEventName) await this.scrollUntilRightRaw(startingPoint);
|
|
1562
|
+
else if ('scrollToLeft' === scrollToEventName) await this.scrollUntilLeftRaw(startingPoint);
|
|
1185
1563
|
else if ('singleAction' !== scrollToEventName && scrollToEventName) throw new Error(`Unknown scroll event type: ${scrollToEventName}, param: ${JSON.stringify(param)}`);
|
|
1186
1564
|
else {
|
|
1187
|
-
if (param?.direction !== 'down' && param && param.direction) if ('up' === param.direction) await this.
|
|
1188
|
-
else if ('left' === param.direction) await this.
|
|
1189
|
-
else if ('right' === param.direction) await this.
|
|
1565
|
+
if (param?.direction !== 'down' && param && param.direction) if ('up' === param.direction) await this.scrollUpRaw(param.distance || void 0, startingPoint);
|
|
1566
|
+
else if ('left' === param.direction) await this.scrollLeftRaw(param.distance || void 0, startingPoint);
|
|
1567
|
+
else if ('right' === param.direction) await this.scrollRightRaw(param.distance || void 0, startingPoint);
|
|
1190
1568
|
else throw new Error(`Unknown scroll direction: ${param.direction}`);
|
|
1191
|
-
else await this.
|
|
1569
|
+
else await this.scrollDownRaw(param?.distance || void 0, startingPoint);
|
|
1192
1570
|
await (0, utils_namespaceObject.sleep)(500);
|
|
1193
1571
|
}
|
|
1194
1572
|
}
|
|
1573
|
+
async runAdbShellRaw(param, context) {
|
|
1574
|
+
const adb = await this.getAdb();
|
|
1575
|
+
const stdout = await runAdbShellStdoutOrThrow(adb, param.command, void 0 === param.timeout ? void 0 : {
|
|
1576
|
+
timeout: param.timeout
|
|
1577
|
+
});
|
|
1578
|
+
const planningFeedback = buildRunAdbShellPlanningFeedback({
|
|
1579
|
+
command: param.command,
|
|
1580
|
+
stdout
|
|
1581
|
+
});
|
|
1582
|
+
if (planningFeedback && context?.task) context.task.planningFeedback = planningFeedback;
|
|
1583
|
+
return stdout;
|
|
1584
|
+
}
|
|
1195
1585
|
describe() {
|
|
1196
1586
|
return this.description || `DeviceId: ${this.deviceId}`;
|
|
1197
1587
|
}
|
|
@@ -1288,15 +1678,15 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1288
1678
|
const adapter = this.getScrcpyAdapter();
|
|
1289
1679
|
if (!adapter.isEnabled()) throw new Error('scrcpy is not available for frame observation');
|
|
1290
1680
|
const deviceInfo = await this.getDevicePhysicalInfo();
|
|
1291
|
-
|
|
1292
|
-
const unsubscribe = await adapter.subscribeKeyframes(deviceInfo, (frame)=>{
|
|
1293
|
-
latest = frame;
|
|
1294
|
-
});
|
|
1681
|
+
const unsubscribe = await adapter.subscribeKeyframes(deviceInfo, ()=>{});
|
|
1295
1682
|
return {
|
|
1296
|
-
latest: ()=>
|
|
1683
|
+
latest: ()=>{
|
|
1684
|
+
const latest = adapter.getLatestRawKeyframe();
|
|
1685
|
+
return latest ? {
|
|
1297
1686
|
ref: latest,
|
|
1298
1687
|
capturedAt: latest.capturedAt
|
|
1299
|
-
} : null
|
|
1688
|
+
} : null;
|
|
1689
|
+
},
|
|
1300
1690
|
decode: async (refs)=>{
|
|
1301
1691
|
const images = [];
|
|
1302
1692
|
for (const frameRef of refs)images.push(await adapter.decodeRawKeyframeToJpegBase64(frameRef.ref));
|
|
@@ -1330,6 +1720,9 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1330
1720
|
return this.appNameMapping[normalizedAppName];
|
|
1331
1721
|
}
|
|
1332
1722
|
async launch(uri) {
|
|
1723
|
+
return this.visualActions.launch(uri);
|
|
1724
|
+
}
|
|
1725
|
+
async launchRaw(uri) {
|
|
1333
1726
|
const adb = await this.getAdb();
|
|
1334
1727
|
this.uri = uri;
|
|
1335
1728
|
try {
|
|
@@ -1355,6 +1748,9 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1355
1748
|
return this;
|
|
1356
1749
|
}
|
|
1357
1750
|
async terminate(uri) {
|
|
1751
|
+
await this.visualActions.terminate(uri);
|
|
1752
|
+
}
|
|
1753
|
+
async terminateRaw(uri) {
|
|
1358
1754
|
const packagePart = uri.includes('/') ? uri.split('/')[0] : uri;
|
|
1359
1755
|
const resolved = this.resolvePackageName(packagePart) ?? packagePart;
|
|
1360
1756
|
const adb = await this.getAdb();
|
|
@@ -1370,6 +1766,9 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1370
1766
|
}
|
|
1371
1767
|
}
|
|
1372
1768
|
async execYadb(keyboardContent) {
|
|
1769
|
+
await this.visualActions.execYadb(keyboardContent);
|
|
1770
|
+
}
|
|
1771
|
+
async execYadbRaw(keyboardContent) {
|
|
1373
1772
|
this.warnYadbOnNonDefaultDisplay('keyboard input');
|
|
1374
1773
|
await this.ensureYadb();
|
|
1375
1774
|
const adb = await this.getAdb();
|
|
@@ -1626,14 +2025,15 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1626
2025
|
async screenshotBase64() {
|
|
1627
2026
|
debugDevice('screenshotBase64 begin');
|
|
1628
2027
|
const adapter = this.getScrcpyAdapter();
|
|
2028
|
+
let scrcpyDeviceInfo = null;
|
|
1629
2029
|
if (adapter.isEnabled()) try {
|
|
1630
2030
|
debugDevice('Attempting scrcpy screenshot...');
|
|
1631
|
-
|
|
1632
|
-
const result = await adapter.screenshotBase64(
|
|
2031
|
+
scrcpyDeviceInfo = await this.getDevicePhysicalInfo();
|
|
2032
|
+
const result = await adapter.screenshotBase64(scrcpyDeviceInfo);
|
|
1633
2033
|
debugDevice('screenshotBase64 end (scrcpy mode)');
|
|
1634
2034
|
return result;
|
|
1635
2035
|
} catch (error) {
|
|
1636
|
-
warnDevice(`Scrcpy screenshot failed, falling back to standard ADB method
|
|
2036
|
+
warnDevice(`Scrcpy screenshot failed, falling back to standard ADB method. This may be caused by transport backlog. ${scrcpy_manager.Xr}\nError: ${error}`);
|
|
1637
2037
|
}
|
|
1638
2038
|
const adb = await this.getAdb();
|
|
1639
2039
|
let screenshotBuffer;
|
|
@@ -1711,9 +2111,13 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1711
2111
|
});
|
|
1712
2112
|
}
|
|
1713
2113
|
debugDevice('screenshotBase64 end');
|
|
2114
|
+
if (scrcpyDeviceInfo) adapter.recoverAfterAdbScreenshot(scrcpyDeviceInfo);
|
|
1714
2115
|
return result;
|
|
1715
2116
|
}
|
|
1716
2117
|
async clearInput(element) {
|
|
2118
|
+
await this.visualActions.clearInput(element);
|
|
2119
|
+
}
|
|
2120
|
+
async clearInputRaw(element) {
|
|
1717
2121
|
if (element) await this.tapPoint({
|
|
1718
2122
|
x: element.center[0],
|
|
1719
2123
|
y: element.center[1]
|
|
@@ -1747,6 +2151,9 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1747
2151
|
return '';
|
|
1748
2152
|
}
|
|
1749
2153
|
async scrollUntilTop(startPoint) {
|
|
2154
|
+
await this.visualActions.scrollUntilTop(startPoint);
|
|
2155
|
+
}
|
|
2156
|
+
async scrollUntilTopRaw(startPoint) {
|
|
1750
2157
|
if (startPoint) {
|
|
1751
2158
|
const { height } = await this.size();
|
|
1752
2159
|
const start = {
|
|
@@ -1761,10 +2168,13 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1761
2168
|
await (0, utils_namespaceObject.sleep)(1000);
|
|
1762
2169
|
return;
|
|
1763
2170
|
}
|
|
1764
|
-
await (0, shared_utils_namespaceObject.repeat)(defaultScrollUntilTimes, ()=>this.
|
|
2171
|
+
await (0, shared_utils_namespaceObject.repeat)(defaultScrollUntilTimes, ()=>this.scrollRaw(0, -9999999, defaultFastScrollDuration));
|
|
1765
2172
|
await (0, utils_namespaceObject.sleep)(1000);
|
|
1766
2173
|
}
|
|
1767
2174
|
async scrollUntilBottom(startPoint) {
|
|
2175
|
+
await this.visualActions.scrollUntilBottom(startPoint);
|
|
2176
|
+
}
|
|
2177
|
+
async scrollUntilBottomRaw(startPoint) {
|
|
1768
2178
|
if (startPoint) {
|
|
1769
2179
|
const start = {
|
|
1770
2180
|
x: Math.round(startPoint.left),
|
|
@@ -1778,10 +2188,13 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1778
2188
|
await (0, utils_namespaceObject.sleep)(1000);
|
|
1779
2189
|
return;
|
|
1780
2190
|
}
|
|
1781
|
-
await (0, shared_utils_namespaceObject.repeat)(defaultScrollUntilTimes, ()=>this.
|
|
2191
|
+
await (0, shared_utils_namespaceObject.repeat)(defaultScrollUntilTimes, ()=>this.scrollRaw(0, 9999999, defaultFastScrollDuration));
|
|
1782
2192
|
await (0, utils_namespaceObject.sleep)(1000);
|
|
1783
2193
|
}
|
|
1784
2194
|
async scrollUntilLeft(startPoint) {
|
|
2195
|
+
await this.visualActions.scrollUntilLeft(startPoint);
|
|
2196
|
+
}
|
|
2197
|
+
async scrollUntilLeftRaw(startPoint) {
|
|
1785
2198
|
if (startPoint) {
|
|
1786
2199
|
const { width } = await this.size();
|
|
1787
2200
|
const start = {
|
|
@@ -1796,10 +2209,13 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1796
2209
|
await (0, utils_namespaceObject.sleep)(1000);
|
|
1797
2210
|
return;
|
|
1798
2211
|
}
|
|
1799
|
-
await (0, shared_utils_namespaceObject.repeat)(defaultScrollUntilTimes, ()=>this.
|
|
2212
|
+
await (0, shared_utils_namespaceObject.repeat)(defaultScrollUntilTimes, ()=>this.scrollRaw(-9999999, 0, defaultFastScrollDuration));
|
|
1800
2213
|
await (0, utils_namespaceObject.sleep)(1000);
|
|
1801
2214
|
}
|
|
1802
2215
|
async scrollUntilRight(startPoint) {
|
|
2216
|
+
await this.visualActions.scrollUntilRight(startPoint);
|
|
2217
|
+
}
|
|
2218
|
+
async scrollUntilRightRaw(startPoint) {
|
|
1803
2219
|
if (startPoint) {
|
|
1804
2220
|
const start = {
|
|
1805
2221
|
x: Math.round(startPoint.left),
|
|
@@ -1813,10 +2229,13 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1813
2229
|
await (0, utils_namespaceObject.sleep)(1000);
|
|
1814
2230
|
return;
|
|
1815
2231
|
}
|
|
1816
|
-
await (0, shared_utils_namespaceObject.repeat)(defaultScrollUntilTimes, ()=>this.
|
|
2232
|
+
await (0, shared_utils_namespaceObject.repeat)(defaultScrollUntilTimes, ()=>this.scrollRaw(9999999, 0, defaultFastScrollDuration));
|
|
1817
2233
|
await (0, utils_namespaceObject.sleep)(1000);
|
|
1818
2234
|
}
|
|
1819
2235
|
async scrollUp(distance, startPoint) {
|
|
2236
|
+
await this.visualActions.scrollUp(distance, startPoint);
|
|
2237
|
+
}
|
|
2238
|
+
async scrollUpRaw(distance, startPoint) {
|
|
1820
2239
|
const { height } = await this.size();
|
|
1821
2240
|
const scrollDistance = Math.round(distance || height);
|
|
1822
2241
|
const hasExplicitDistance = void 0 !== distance;
|
|
@@ -1830,9 +2249,12 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1830
2249
|
await this.dragPoint(start, end);
|
|
1831
2250
|
return;
|
|
1832
2251
|
}
|
|
1833
|
-
await this.
|
|
2252
|
+
await this.scrollRaw(0, -scrollDistance, void 0, hasExplicitDistance, 'up');
|
|
1834
2253
|
}
|
|
1835
2254
|
async scrollDown(distance, startPoint) {
|
|
2255
|
+
await this.visualActions.scrollDown(distance, startPoint);
|
|
2256
|
+
}
|
|
2257
|
+
async scrollDownRaw(distance, startPoint) {
|
|
1836
2258
|
const { height } = await this.size();
|
|
1837
2259
|
const scrollDistance = Math.round(distance || height);
|
|
1838
2260
|
const hasExplicitDistance = void 0 !== distance;
|
|
@@ -1846,9 +2268,12 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1846
2268
|
await this.dragPoint(start, end);
|
|
1847
2269
|
return;
|
|
1848
2270
|
}
|
|
1849
|
-
await this.
|
|
2271
|
+
await this.scrollRaw(0, scrollDistance, void 0, hasExplicitDistance, 'down');
|
|
1850
2272
|
}
|
|
1851
2273
|
async scrollLeft(distance, startPoint) {
|
|
2274
|
+
await this.visualActions.scrollLeft(distance, startPoint);
|
|
2275
|
+
}
|
|
2276
|
+
async scrollLeftRaw(distance, startPoint) {
|
|
1852
2277
|
const { width } = await this.size();
|
|
1853
2278
|
const scrollDistance = Math.round(distance || width);
|
|
1854
2279
|
const hasExplicitDistance = void 0 !== distance;
|
|
@@ -1862,9 +2287,12 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1862
2287
|
await this.dragPoint(start, end);
|
|
1863
2288
|
return;
|
|
1864
2289
|
}
|
|
1865
|
-
await this.
|
|
2290
|
+
await this.scrollRaw(-scrollDistance, 0, void 0, hasExplicitDistance, 'left');
|
|
1866
2291
|
}
|
|
1867
2292
|
async scrollRight(distance, startPoint) {
|
|
2293
|
+
await this.visualActions.scrollRight(distance, startPoint);
|
|
2294
|
+
}
|
|
2295
|
+
async scrollRightRaw(distance, startPoint) {
|
|
1868
2296
|
const { width } = await this.size();
|
|
1869
2297
|
const scrollDistance = Math.round(distance || width);
|
|
1870
2298
|
const hasExplicitDistance = void 0 !== distance;
|
|
@@ -1878,7 +2306,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1878
2306
|
await this.dragPoint(start, end);
|
|
1879
2307
|
return;
|
|
1880
2308
|
}
|
|
1881
|
-
await this.
|
|
2309
|
+
await this.scrollRaw(scrollDistance, 0, void 0, hasExplicitDistance, 'right');
|
|
1882
2310
|
}
|
|
1883
2311
|
async ensureYadb() {
|
|
1884
2312
|
if (!this.yadbPushed) {
|
|
@@ -1907,11 +2335,11 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1907
2335
|
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)';
|
|
1908
2336
|
throw new Error(`${reason}, but yadb (app_process) cannot target non-default displayId=${this.options?.displayId}. Use displayId=0 or a different imeStrategy.`);
|
|
1909
2337
|
}
|
|
1910
|
-
if (needsYadb) await this.
|
|
2338
|
+
if (needsYadb) await this.execYadbRaw(escapeForShell(text));
|
|
1911
2339
|
else await this.shellInputText(text, {
|
|
1912
2340
|
keyboardTypeDelay: typeDelay
|
|
1913
2341
|
});
|
|
1914
|
-
if (true === shouldAutoDismissKeyboard) await this.
|
|
2342
|
+
if (true === shouldAutoDismissKeyboard) await this.hideKeyboardRaw(options);
|
|
1915
2343
|
}
|
|
1916
2344
|
normalizeKeyName(key) {
|
|
1917
2345
|
const keyMap = {
|
|
@@ -1981,6 +2409,9 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1981
2409
|
await adb.shell(`input${this.getDisplayArg()} swipe ${fromX} ${fromY} ${toX} ${toY} ${duration}`);
|
|
1982
2410
|
}
|
|
1983
2411
|
async scroll(deltaX, deltaY, duration, warnOnClamp = false, direction) {
|
|
2412
|
+
await this.visualActions.scroll(deltaX, deltaY, duration, warnOnClamp, direction);
|
|
2413
|
+
}
|
|
2414
|
+
async scrollRaw(deltaX, deltaY, duration, warnOnClamp = false, direction) {
|
|
1984
2415
|
if (0 === deltaX && 0 === deltaY) throw new Error('Scroll distance cannot be zero in both directions');
|
|
1985
2416
|
const { width, height } = await this.size();
|
|
1986
2417
|
const n = 4;
|
|
@@ -2045,12 +2476,21 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
2045
2476
|
}
|
|
2046
2477
|
}
|
|
2047
2478
|
async back() {
|
|
2479
|
+
await this.visualActions.back();
|
|
2480
|
+
}
|
|
2481
|
+
async backRaw() {
|
|
2048
2482
|
await this.shellInputKeyevent(4);
|
|
2049
2483
|
}
|
|
2050
2484
|
async home() {
|
|
2485
|
+
await this.visualActions.home();
|
|
2486
|
+
}
|
|
2487
|
+
async homeRaw() {
|
|
2051
2488
|
await this.shellInputKeyevent(3);
|
|
2052
2489
|
}
|
|
2053
2490
|
async recentApps() {
|
|
2491
|
+
await this.visualActions.recentApps();
|
|
2492
|
+
}
|
|
2493
|
+
async recentAppsRaw() {
|
|
2054
2494
|
await this.shellInputKeyevent(187);
|
|
2055
2495
|
}
|
|
2056
2496
|
async longPressPoint(point, duration = 2000) {
|
|
@@ -2059,6 +2499,9 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
2059
2499
|
await adb.shell(`input${this.getDisplayArg()} swipe ${adjustedX} ${adjustedY} ${adjustedX} ${adjustedY} ${duration}`);
|
|
2060
2500
|
}
|
|
2061
2501
|
async pullDown(startPoint, distance, duration = 800) {
|
|
2502
|
+
await this.visualActions.pullDown(startPoint, distance, duration);
|
|
2503
|
+
}
|
|
2504
|
+
async pullDownRaw(startPoint, distance, duration = 800) {
|
|
2062
2505
|
const { width, height } = await this.size();
|
|
2063
2506
|
const start = startPoint ? {
|
|
2064
2507
|
x: Math.round(startPoint.left),
|
|
@@ -2072,13 +2515,19 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
2072
2515
|
x: start.x,
|
|
2073
2516
|
y: start.y + pullDistance
|
|
2074
2517
|
};
|
|
2075
|
-
await this.
|
|
2518
|
+
await this.pullDragRaw(start, end, duration);
|
|
2076
2519
|
await (0, utils_namespaceObject.sleep)(200);
|
|
2077
2520
|
}
|
|
2078
2521
|
async pullDrag(from, to, duration) {
|
|
2522
|
+
await this.visualActions.pullDrag(from, to, duration);
|
|
2523
|
+
}
|
|
2524
|
+
async pullDragRaw(from, to, duration) {
|
|
2079
2525
|
await this.swipePoint(from, to, duration);
|
|
2080
2526
|
}
|
|
2081
2527
|
async pullUp(startPoint, distance, duration = 600) {
|
|
2528
|
+
await this.visualActions.pullUp(startPoint, distance, duration);
|
|
2529
|
+
}
|
|
2530
|
+
async pullUpRaw(startPoint, distance, duration = 600) {
|
|
2082
2531
|
const { width, height } = await this.size();
|
|
2083
2532
|
const start = startPoint ? {
|
|
2084
2533
|
x: Math.round(startPoint.left),
|
|
@@ -2092,7 +2541,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
2092
2541
|
x: start.x,
|
|
2093
2542
|
y: start.y - pullDistance
|
|
2094
2543
|
};
|
|
2095
|
-
await this.
|
|
2544
|
+
await this.pullDragRaw(start, end, duration);
|
|
2096
2545
|
await (0, utils_namespaceObject.sleep)(100);
|
|
2097
2546
|
}
|
|
2098
2547
|
getDisplayArg() {
|
|
@@ -2155,6 +2604,9 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
2155
2604
|
}
|
|
2156
2605
|
}
|
|
2157
2606
|
async hideKeyboard(options, timeoutMs = 1000) {
|
|
2607
|
+
return this.visualActions.hideKeyboard(options, timeoutMs);
|
|
2608
|
+
}
|
|
2609
|
+
async hideKeyboardRaw(options, timeoutMs = 1000) {
|
|
2158
2610
|
const adb = await this.getAdb();
|
|
2159
2611
|
const keyboardDismissStrategy = options?.keyboardDismissStrategy ?? this.options?.keyboardDismissStrategy ?? 'esc-first';
|
|
2160
2612
|
const keyboardStatus = await adb.isSoftKeyboardPresent();
|
|
@@ -2209,55 +2661,91 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
2209
2661
|
device_define_property(this, "interfaceType", 'android');
|
|
2210
2662
|
device_define_property(this, "uri", void 0);
|
|
2211
2663
|
device_define_property(this, "options", void 0);
|
|
2664
|
+
device_define_property(this, "visualActions", createVisualActionRegistry({
|
|
2665
|
+
tap: (point)=>this.tapPoint(point),
|
|
2666
|
+
doubleClick: (point)=>this.doubleTapPoint(point),
|
|
2667
|
+
longPress: (point, opts)=>this.longPressPoint(point, opts?.duration),
|
|
2668
|
+
dragAndDrop: (from, to)=>this.dragPoint(from, to),
|
|
2669
|
+
keyboardPress: (keyName)=>this.pressKey(keyName),
|
|
2670
|
+
typeText: async (value, opts)=>{
|
|
2671
|
+
const target = opts?.target;
|
|
2672
|
+
if (target && opts?.replace !== false) await this.clearInputRaw(target);
|
|
2673
|
+
else if (target) await this.tapPoint({
|
|
2674
|
+
x: target.center[0],
|
|
2675
|
+
y: target.center[1]
|
|
2676
|
+
});
|
|
2677
|
+
if (opts?.focusOnly) return;
|
|
2678
|
+
await this.typeText(value, opts);
|
|
2679
|
+
},
|
|
2680
|
+
clearInput: (target)=>this.clearInputRaw(target),
|
|
2681
|
+
cursorMove: async (direction, times = 1)=>{
|
|
2682
|
+
const arrowKey = 'left' === direction ? 'ArrowLeft' : 'ArrowRight';
|
|
2683
|
+
for(let index = 0; index < times; index++)await this.pressKey(arrowKey);
|
|
2684
|
+
},
|
|
2685
|
+
swipe: async (start, end, opts)=>{
|
|
2686
|
+
const duration = opts?.duration ?? 300;
|
|
2687
|
+
const repeatCount = opts?.repeat ?? 1;
|
|
2688
|
+
for(let index = 0; index < repeatCount; index++)await this.dragPoint(start, end, duration);
|
|
2689
|
+
},
|
|
2690
|
+
pinch: async (center, opts)=>{
|
|
2691
|
+
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.`);
|
|
2692
|
+
const { x: adjCenterX, y: adjCenterY } = await this.adjustCoordinates(Math.round(center.x), Math.round(center.y));
|
|
2693
|
+
const ratio = 0 !== adjCenterX && 0 !== center.x ? adjCenterX / center.x : 1;
|
|
2694
|
+
const adjStartDist = Math.round(opts.startDistance * ratio);
|
|
2695
|
+
const adjEndDist = Math.round(opts.endDistance * ratio);
|
|
2696
|
+
await this.ensureYadb();
|
|
2697
|
+
const adb = await this.getAdb();
|
|
2698
|
+
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}`);
|
|
2699
|
+
},
|
|
2700
|
+
actionScroll: (param)=>this.performActionScroll(param),
|
|
2701
|
+
back: ()=>this.backRaw(),
|
|
2702
|
+
home: ()=>this.homeRaw(),
|
|
2703
|
+
recentApps: ()=>this.recentAppsRaw(),
|
|
2704
|
+
pullGesture: (param)=>this.performPullGesture(param),
|
|
2705
|
+
launch: (uri)=>this.launchRaw(uri),
|
|
2706
|
+
terminate: (uri)=>this.terminateRaw(uri),
|
|
2707
|
+
runAdbShell: (param, context)=>this.runAdbShellRaw(param, context),
|
|
2708
|
+
execYadb: (keyboardContent)=>this.execYadbRaw(keyboardContent),
|
|
2709
|
+
scrollUntilTop: (startPoint)=>this.scrollUntilTopRaw(startPoint),
|
|
2710
|
+
scrollUntilBottom: (startPoint)=>this.scrollUntilBottomRaw(startPoint),
|
|
2711
|
+
scrollUntilLeft: (startPoint)=>this.scrollUntilLeftRaw(startPoint),
|
|
2712
|
+
scrollUntilRight: (startPoint)=>this.scrollUntilRightRaw(startPoint),
|
|
2713
|
+
scrollUp: (distance, startPoint)=>this.scrollUpRaw(distance, startPoint),
|
|
2714
|
+
scrollDown: (distance, startPoint)=>this.scrollDownRaw(distance, startPoint),
|
|
2715
|
+
scrollLeft: (distance, startPoint)=>this.scrollLeftRaw(distance, startPoint),
|
|
2716
|
+
scrollRight: (distance, startPoint)=>this.scrollRightRaw(distance, startPoint),
|
|
2717
|
+
scroll: (deltaX, deltaY, duration, warnOnClamp, direction)=>this.scrollRaw(deltaX, deltaY, duration, warnOnClamp, direction),
|
|
2718
|
+
pullDown: (startPoint, distance, duration)=>this.pullDownRaw(startPoint, distance, duration),
|
|
2719
|
+
pullDrag: (from, to, duration)=>this.pullDragRaw(from, to, duration),
|
|
2720
|
+
pullUp: (startPoint, distance, duration)=>this.pullUpRaw(startPoint, distance, duration),
|
|
2721
|
+
hideKeyboard: (options, timeoutMs)=>this.hideKeyboardRaw(options, timeoutMs)
|
|
2722
|
+
}, async ()=>{
|
|
2723
|
+
await this.scrcpyAdapter?.markActionBarrier();
|
|
2724
|
+
}));
|
|
2212
2725
|
device_define_property(this, "inputPrimitives", {
|
|
2213
2726
|
pointer: {
|
|
2214
|
-
tap:
|
|
2215
|
-
doubleClick:
|
|
2216
|
-
longPress:
|
|
2217
|
-
dragAndDrop:
|
|
2727
|
+
tap: this.visualActions.tap,
|
|
2728
|
+
doubleClick: this.visualActions.doubleClick,
|
|
2729
|
+
longPress: this.visualActions.longPress,
|
|
2730
|
+
dragAndDrop: this.visualActions.dragAndDrop
|
|
2218
2731
|
},
|
|
2219
2732
|
keyboard: {
|
|
2220
|
-
keyboardPress:
|
|
2221
|
-
typeText:
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
else if (target) await this.tapPoint({
|
|
2225
|
-
x: target.center[0],
|
|
2226
|
-
y: target.center[1]
|
|
2227
|
-
});
|
|
2228
|
-
if (opts?.focusOnly) return;
|
|
2229
|
-
await this.typeText(value, opts);
|
|
2230
|
-
},
|
|
2231
|
-
clearInput: (target)=>this.clearInput(target),
|
|
2232
|
-
cursorMove: async (direction, times = 1)=>{
|
|
2233
|
-
const arrowKey = 'left' === direction ? 'ArrowLeft' : 'ArrowRight';
|
|
2234
|
-
for(let i = 0; i < times; i++)await this.pressKey(arrowKey);
|
|
2235
|
-
}
|
|
2733
|
+
keyboardPress: this.visualActions.keyboardPress,
|
|
2734
|
+
typeText: this.visualActions.typeText,
|
|
2735
|
+
clearInput: this.visualActions.clearInput,
|
|
2736
|
+
cursorMove: this.visualActions.cursorMove
|
|
2236
2737
|
},
|
|
2237
2738
|
touch: {
|
|
2238
|
-
swipe:
|
|
2239
|
-
|
|
2240
|
-
const repeatCount = opts?.repeat ?? 1;
|
|
2241
|
-
for(let i = 0; i < repeatCount; i++)await this.dragPoint(start, end, duration);
|
|
2242
|
-
},
|
|
2243
|
-
pinch: async (center, opts)=>{
|
|
2244
|
-
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.`);
|
|
2245
|
-
const { x: adjCenterX, y: adjCenterY } = await this.adjustCoordinates(Math.round(center.x), Math.round(center.y));
|
|
2246
|
-
const ratio = 0 !== adjCenterX && 0 !== center.x ? adjCenterX / center.x : 1;
|
|
2247
|
-
const adjStartDist = Math.round(opts.startDistance * ratio);
|
|
2248
|
-
const adjEndDist = Math.round(opts.endDistance * ratio);
|
|
2249
|
-
await this.ensureYadb();
|
|
2250
|
-
const adb = await this.getAdb();
|
|
2251
|
-
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}`);
|
|
2252
|
-
}
|
|
2739
|
+
swipe: this.visualActions.swipe,
|
|
2740
|
+
pinch: this.visualActions.pinch
|
|
2253
2741
|
},
|
|
2254
2742
|
scroll: {
|
|
2255
|
-
scroll:
|
|
2743
|
+
scroll: this.visualActions.actionScroll
|
|
2256
2744
|
},
|
|
2257
2745
|
system: {
|
|
2258
|
-
backButton:
|
|
2259
|
-
homeButton:
|
|
2260
|
-
recentAppsButton:
|
|
2746
|
+
backButton: this.visualActions.back,
|
|
2747
|
+
homeButton: this.visualActions.home,
|
|
2748
|
+
recentAppsButton: this.visualActions.recentApps
|
|
2261
2749
|
}
|
|
2262
2750
|
});
|
|
2263
2751
|
external_node_assert_default()(deviceId, 'deviceId is required for AndroidDevice');
|
|
@@ -2279,7 +2767,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
2279
2767
|
const terminateParamSchema = core_namespaceObject.z.object({
|
|
2280
2768
|
uri: core_namespaceObject.z.string().describe('Package name or app name to terminate. Use the exact package name, e.g. com.android.settings.')
|
|
2281
2769
|
});
|
|
2282
|
-
const createPlatformActions = (
|
|
2770
|
+
const createPlatformActions = (visualActions)=>({
|
|
2283
2771
|
RunAdbShell: (0, device_namespaceObject.defineAction)({
|
|
2284
2772
|
name: 'RunAdbShell',
|
|
2285
2773
|
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.',
|
|
@@ -2290,16 +2778,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
2290
2778
|
},
|
|
2291
2779
|
call: async (param, context)=>{
|
|
2292
2780
|
if (!param.command || '' === param.command.trim()) throw new Error('RunAdbShell requires a non-empty command parameter');
|
|
2293
|
-
|
|
2294
|
-
const stdout = await runAdbShellStdoutOrThrow(adb, param.command, void 0 === param.timeout ? void 0 : {
|
|
2295
|
-
timeout: param.timeout
|
|
2296
|
-
});
|
|
2297
|
-
const planningFeedback = buildRunAdbShellPlanningFeedback({
|
|
2298
|
-
command: param.command,
|
|
2299
|
-
stdout
|
|
2300
|
-
});
|
|
2301
|
-
if (planningFeedback && context?.task) context.task.planningFeedback = planningFeedback;
|
|
2302
|
-
return stdout;
|
|
2781
|
+
return visualActions.runAdbShell(param, context);
|
|
2303
2782
|
}
|
|
2304
2783
|
}),
|
|
2305
2784
|
Launch: (0, device_namespaceObject.defineAction)({
|
|
@@ -2312,7 +2791,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
2312
2791
|
},
|
|
2313
2792
|
call: async (param)=>{
|
|
2314
2793
|
if (!param.uri || '' === param.uri.trim()) throw new Error('Launch requires a non-empty uri parameter');
|
|
2315
|
-
await
|
|
2794
|
+
await visualActions.launch(param.uri);
|
|
2316
2795
|
}
|
|
2317
2796
|
}),
|
|
2318
2797
|
Terminate: (0, device_namespaceObject.defineAction)({
|
|
@@ -2322,7 +2801,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
2322
2801
|
paramSchema: terminateParamSchema,
|
|
2323
2802
|
call: async (param)=>{
|
|
2324
2803
|
if (!param.uri || '' === param.uri.trim()) throw new Error('Terminate requires a non-empty uri parameter');
|
|
2325
|
-
await
|
|
2804
|
+
await visualActions.terminate(param.uri);
|
|
2326
2805
|
}
|
|
2327
2806
|
})
|
|
2328
2807
|
});
|