@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/cli.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);
|
|
@@ -751,6 +990,7 @@ ${stdout ? truncateAdbShellStream(stdout, 'stdout') : EMPTY_ADB_SHELL_STDOUT}`;
|
|
|
751
990
|
class ScrcpyDeviceAdapter {
|
|
752
991
|
isEnabled() {
|
|
753
992
|
if (!this.isConfigured()) return false;
|
|
993
|
+
if (this.freshnessRecoveryPending || this.recoveryPromise) return false;
|
|
754
994
|
return null === this.retryAfter || Date.now() >= this.retryAfter;
|
|
755
995
|
}
|
|
756
996
|
getStatus() {
|
|
@@ -762,12 +1002,18 @@ ${stdout ? truncateAdbShellStream(stdout, 'stdout') : EMPTY_ADB_SHELL_STDOUT}`;
|
|
|
762
1002
|
};
|
|
763
1003
|
}
|
|
764
1004
|
isConfigured() {
|
|
765
|
-
return this.scrcpyConfig?.enabled ?? scrcpy_manager.
|
|
1005
|
+
return this.scrcpyConfig?.enabled ?? scrcpy_manager.ov.enabled;
|
|
766
1006
|
}
|
|
767
1007
|
async initialize(deviceInfo) {
|
|
1008
|
+
if (this.recoveryPromise) {
|
|
1009
|
+
await this.recoveryPromise;
|
|
1010
|
+
if (this.manager?.isConnected()) return;
|
|
1011
|
+
}
|
|
1012
|
+
this.freshnessRecoveryPending = false;
|
|
768
1013
|
try {
|
|
769
1014
|
const manager = await this.ensureManager(deviceInfo);
|
|
770
1015
|
await manager.ensureConnected();
|
|
1016
|
+
await this.applyPendingActionBarrier(manager);
|
|
771
1017
|
this.clearFailure();
|
|
772
1018
|
} catch (error) {
|
|
773
1019
|
this.recordFailure(error);
|
|
@@ -777,24 +1023,27 @@ ${stdout ? truncateAdbShellStream(stdout, 'stdout') : EMPTY_ADB_SHELL_STDOUT}`;
|
|
|
777
1023
|
recordFailure(error) {
|
|
778
1024
|
this.lastError = error instanceof Error ? error.message : String(error);
|
|
779
1025
|
this.retryAfter = Date.now() + SCRCPY_RETRY_COOLDOWN_MS;
|
|
1026
|
+
this.freshnessRecoveryPending = false;
|
|
780
1027
|
}
|
|
781
1028
|
clearFailure() {
|
|
782
1029
|
this.lastError = null;
|
|
783
1030
|
this.retryAfter = null;
|
|
1031
|
+
this.freshnessRecoveryPending = false;
|
|
784
1032
|
}
|
|
785
1033
|
ensureRetryReady() {
|
|
1034
|
+
if (this.freshnessRecoveryPending || this.recoveryPromise) throw new Error('scrcpy freshness recovery is in progress');
|
|
786
1035
|
if (null === this.retryAfter || Date.now() >= this.retryAfter) return;
|
|
787
1036
|
throw new Error(`scrcpy retry is cooling down until ${new Date(this.retryAfter).toISOString()}. Last error: ${this.lastError}`);
|
|
788
1037
|
}
|
|
789
1038
|
resolveConfig(deviceInfo) {
|
|
790
1039
|
if (this.resolvedConfig) return this.resolvedConfig;
|
|
791
1040
|
const config = this.scrcpyConfig;
|
|
792
|
-
const maxSize = config?.maxSize ?? scrcpy_manager.
|
|
793
|
-
const videoBitRate = config?.videoBitRate ?? scrcpy_manager.
|
|
1041
|
+
const maxSize = config?.maxSize ?? scrcpy_manager.ov.maxSize;
|
|
1042
|
+
const videoBitRate = config?.videoBitRate ?? scrcpy_manager.ov.videoBitRate;
|
|
794
1043
|
this.resolvedConfig = {
|
|
795
1044
|
enabled: this.isConfigured(),
|
|
796
1045
|
maxSize,
|
|
797
|
-
idleTimeoutMs: config?.idleTimeoutMs ?? scrcpy_manager.
|
|
1046
|
+
idleTimeoutMs: config?.idleTimeoutMs ?? scrcpy_manager.ov.idleTimeoutMs,
|
|
798
1047
|
videoBitRate
|
|
799
1048
|
};
|
|
800
1049
|
return this.resolvedConfig;
|
|
@@ -828,24 +1077,40 @@ ${stdout ? truncateAdbShellStream(stdout, 'stdout') : EMPTY_ADB_SHELL_STDOUT}`;
|
|
|
828
1077
|
}
|
|
829
1078
|
async screenshotBase64(deviceInfo) {
|
|
830
1079
|
this.ensureRetryReady();
|
|
1080
|
+
let manager = null;
|
|
831
1081
|
try {
|
|
832
|
-
|
|
1082
|
+
manager = await this.ensureManager(deviceInfo);
|
|
1083
|
+
await manager.ensureConnected();
|
|
1084
|
+
await this.applyPendingActionBarrier(manager);
|
|
833
1085
|
const screenshotBuffer = await manager.getScreenshotJpeg();
|
|
834
1086
|
this.clearFailure();
|
|
835
1087
|
return (0, img_namespaceObject.createImgBase64ByFormat)('jpeg', screenshotBuffer.toString('base64'));
|
|
836
1088
|
} catch (error) {
|
|
1089
|
+
if ((0, scrcpy_manager.GJ)(error)) {
|
|
1090
|
+
this.markFreshnessRecoveryPending(manager, error);
|
|
1091
|
+
throw error;
|
|
1092
|
+
}
|
|
837
1093
|
this.recordFailure(error);
|
|
838
1094
|
throw error;
|
|
839
1095
|
}
|
|
840
1096
|
}
|
|
841
1097
|
async subscribeKeyframes(deviceInfo, listener) {
|
|
842
1098
|
this.ensureRetryReady();
|
|
1099
|
+
this.keyframeListeners.add(listener);
|
|
843
1100
|
try {
|
|
844
1101
|
const manager = await this.ensureManager(deviceInfo);
|
|
845
1102
|
await manager.ensureConnected();
|
|
1103
|
+
await this.applyPendingActionBarrier(manager);
|
|
1104
|
+
await manager.ensureFrameClockCalibration();
|
|
846
1105
|
this.clearFailure();
|
|
847
|
-
|
|
1106
|
+
this.attachKeyframeListener(manager, listener);
|
|
1107
|
+
return ()=>{
|
|
1108
|
+
this.keyframeListeners.delete(listener);
|
|
1109
|
+
this.keyframeUnsubscribers.get(listener)?.();
|
|
1110
|
+
this.keyframeUnsubscribers.delete(listener);
|
|
1111
|
+
};
|
|
848
1112
|
} catch (error) {
|
|
1113
|
+
this.keyframeListeners.delete(listener);
|
|
849
1114
|
this.recordFailure(error);
|
|
850
1115
|
throw error;
|
|
851
1116
|
}
|
|
@@ -853,6 +1118,83 @@ ${stdout ? truncateAdbShellStream(stdout, 'stdout') : EMPTY_ADB_SHELL_STDOUT}`;
|
|
|
853
1118
|
getLatestRawKeyframe() {
|
|
854
1119
|
return this.manager?.getLatestRawKeyframe() ?? null;
|
|
855
1120
|
}
|
|
1121
|
+
attachKeyframeListener(manager, listener) {
|
|
1122
|
+
this.keyframeUnsubscribers.get(listener)?.();
|
|
1123
|
+
this.keyframeUnsubscribers.set(listener, manager.subscribeKeyframes(listener));
|
|
1124
|
+
}
|
|
1125
|
+
attachKeyframeListeners(manager) {
|
|
1126
|
+
this.keyframeUnsubscribers.clear();
|
|
1127
|
+
for (const listener of this.keyframeListeners)this.attachKeyframeListener(manager, listener);
|
|
1128
|
+
}
|
|
1129
|
+
markFreshnessRecoveryPending(manager, error) {
|
|
1130
|
+
this.lastError = error.message;
|
|
1131
|
+
this.retryAfter = null;
|
|
1132
|
+
this.freshnessRecoveryPending = true;
|
|
1133
|
+
this.keyframeUnsubscribers.clear();
|
|
1134
|
+
if (manager && this.manager === manager) this.manager = null;
|
|
1135
|
+
}
|
|
1136
|
+
async applyPendingActionBarrier(manager) {
|
|
1137
|
+
if (!this.pendingActionBarrier) return;
|
|
1138
|
+
await manager.setFreshnessBarrier('completed input action while scrcpy was unavailable');
|
|
1139
|
+
this.pendingActionBarrier = false;
|
|
1140
|
+
}
|
|
1141
|
+
recoverAfterAdbScreenshot(deviceInfo) {
|
|
1142
|
+
if (!this.freshnessRecoveryPending || this.recoveryPromise) return;
|
|
1143
|
+
const generation = this.lifecycleGeneration;
|
|
1144
|
+
const recovery = (async ()=>{
|
|
1145
|
+
let manager = null;
|
|
1146
|
+
try {
|
|
1147
|
+
manager = await this.ensureManager(deviceInfo);
|
|
1148
|
+
await manager.ensureConnected();
|
|
1149
|
+
await this.applyPendingActionBarrier(manager);
|
|
1150
|
+
await manager.prepareFreshFrame();
|
|
1151
|
+
if (generation !== this.lifecycleGeneration) {
|
|
1152
|
+
await manager.disconnect();
|
|
1153
|
+
if (this.manager === manager) this.manager = null;
|
|
1154
|
+
return;
|
|
1155
|
+
}
|
|
1156
|
+
this.attachKeyframeListeners(manager);
|
|
1157
|
+
this.clearFailure();
|
|
1158
|
+
debugAdapter('Scrcpy freshness recovery completed in background');
|
|
1159
|
+
} catch (error) {
|
|
1160
|
+
if (manager) {
|
|
1161
|
+
await manager.disconnect();
|
|
1162
|
+
if (this.manager === manager) this.manager = null;
|
|
1163
|
+
}
|
|
1164
|
+
this.freshnessRecoveryPending = false;
|
|
1165
|
+
this.recordFailure(error);
|
|
1166
|
+
debugAdapter(`Scrcpy background freshness recovery failed: ${error}`);
|
|
1167
|
+
throw error;
|
|
1168
|
+
}
|
|
1169
|
+
})();
|
|
1170
|
+
this.recoveryPromise = recovery;
|
|
1171
|
+
recovery.catch(()=>{}).finally(()=>{
|
|
1172
|
+
if (this.recoveryPromise === recovery) this.recoveryPromise = null;
|
|
1173
|
+
});
|
|
1174
|
+
}
|
|
1175
|
+
async markActionBarrier() {
|
|
1176
|
+
const manager = this.manager;
|
|
1177
|
+
if (!manager?.isConnected()) {
|
|
1178
|
+
this.pendingActionBarrier = true;
|
|
1179
|
+
return;
|
|
1180
|
+
}
|
|
1181
|
+
try {
|
|
1182
|
+
await manager.setFreshnessBarrier('completed input action');
|
|
1183
|
+
this.pendingActionBarrier = false;
|
|
1184
|
+
this.clearFailure();
|
|
1185
|
+
} catch (error) {
|
|
1186
|
+
this.pendingActionBarrier = true;
|
|
1187
|
+
this.recordFailure(error);
|
|
1188
|
+
debugAdapter(`Unable to mark scrcpy action barrier; disabling this stream: ${error}`);
|
|
1189
|
+
try {
|
|
1190
|
+
await manager.disconnect();
|
|
1191
|
+
} catch (disconnectError) {
|
|
1192
|
+
debugAdapter(`Error disconnecting scrcpy after barrier failure: ${disconnectError}`);
|
|
1193
|
+
} finally{
|
|
1194
|
+
if (this.manager === manager) this.manager = null;
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
856
1198
|
async decodeRawKeyframeToJpegBase64(frame) {
|
|
857
1199
|
if (!this.manager) throw new Error('scrcpy manager is not initialized');
|
|
858
1200
|
const jpegBuffer = await this.manager.decodeRawKeyframeToJpeg(frame);
|
|
@@ -876,6 +1218,13 @@ ${stdout ? truncateAdbShellStream(stdout, 'stdout') : EMPTY_ADB_SHELL_STDOUT}`;
|
|
|
876
1218
|
return resolution.width / physicalWidth;
|
|
877
1219
|
}
|
|
878
1220
|
async disconnect() {
|
|
1221
|
+
this.lifecycleGeneration += 1;
|
|
1222
|
+
this.freshnessRecoveryPending = false;
|
|
1223
|
+
this.pendingActionBarrier = false;
|
|
1224
|
+
for (const unsubscribe of this.keyframeUnsubscribers.values())unsubscribe();
|
|
1225
|
+
this.keyframeUnsubscribers.clear();
|
|
1226
|
+
this.keyframeListeners.clear();
|
|
1227
|
+
if (this.recoveryPromise) await this.recoveryPromise.catch(()=>{});
|
|
879
1228
|
if (this.manager) {
|
|
880
1229
|
try {
|
|
881
1230
|
await this.manager.disconnect();
|
|
@@ -895,6 +1244,12 @@ ${stdout ? truncateAdbShellStream(stdout, 'stdout') : EMPTY_ADB_SHELL_STDOUT}`;
|
|
|
895
1244
|
_define_property(this, "resolvedConfig", void 0);
|
|
896
1245
|
_define_property(this, "lastError", void 0);
|
|
897
1246
|
_define_property(this, "retryAfter", void 0);
|
|
1247
|
+
_define_property(this, "freshnessRecoveryPending", void 0);
|
|
1248
|
+
_define_property(this, "recoveryPromise", void 0);
|
|
1249
|
+
_define_property(this, "lifecycleGeneration", void 0);
|
|
1250
|
+
_define_property(this, "pendingActionBarrier", void 0);
|
|
1251
|
+
_define_property(this, "keyframeListeners", void 0);
|
|
1252
|
+
_define_property(this, "keyframeUnsubscribers", void 0);
|
|
898
1253
|
this.deviceId = deviceId;
|
|
899
1254
|
this.scrcpyConfig = scrcpyConfig;
|
|
900
1255
|
this.resolveAdbServerEndpoint = resolveAdbServerEndpoint;
|
|
@@ -902,6 +1257,12 @@ ${stdout ? truncateAdbShellStream(stdout, 'stdout') : EMPTY_ADB_SHELL_STDOUT}`;
|
|
|
902
1257
|
this.resolvedConfig = null;
|
|
903
1258
|
this.lastError = null;
|
|
904
1259
|
this.retryAfter = null;
|
|
1260
|
+
this.freshnessRecoveryPending = false;
|
|
1261
|
+
this.recoveryPromise = null;
|
|
1262
|
+
this.lifecycleGeneration = 0;
|
|
1263
|
+
this.pendingActionBarrier = false;
|
|
1264
|
+
this.keyframeListeners = new Set();
|
|
1265
|
+
this.keyframeUnsubscribers = new Map();
|
|
905
1266
|
}
|
|
906
1267
|
}
|
|
907
1268
|
const TAG_RE = /<(\/?)([A-Za-z_][A-Za-z0-9_.\-:]*)([^>]*?)(\/?)>/g;
|
|
@@ -1128,6 +1489,20 @@ ${stdout ? truncateAdbShellStream(stdout, 'stdout') : EMPTY_ADB_SHELL_STDOUT}`;
|
|
|
1128
1489
|
}
|
|
1129
1490
|
throw new Error(`Unable to capture Android UI tree after ${failures.length} attempt(s): ${failures.join('; ')}`);
|
|
1130
1491
|
}
|
|
1492
|
+
function createVisualActionRegistry(definitions, onActionSettled) {
|
|
1493
|
+
const registeredActions = {};
|
|
1494
|
+
for (const actionName of Object.keys(definitions)){
|
|
1495
|
+
const dispatch = definitions[actionName];
|
|
1496
|
+
registeredActions[actionName] = async (...args)=>{
|
|
1497
|
+
try {
|
|
1498
|
+
return await dispatch(...args);
|
|
1499
|
+
} finally{
|
|
1500
|
+
await onActionSettled(actionName);
|
|
1501
|
+
}
|
|
1502
|
+
};
|
|
1503
|
+
}
|
|
1504
|
+
return registeredActions;
|
|
1505
|
+
}
|
|
1131
1506
|
function device_define_property(obj, key, value) {
|
|
1132
1507
|
if (key in obj) Object.defineProperty(obj, key, {
|
|
1133
1508
|
value: value,
|
|
@@ -1228,19 +1603,12 @@ ${stdout ? truncateAdbShellStream(stdout, 'stdout') : EMPTY_ADB_SHELL_STDOUT}`;
|
|
|
1228
1603
|
}
|
|
1229
1604
|
},
|
|
1230
1605
|
call: async (param)=>{
|
|
1231
|
-
const element = param.locate;
|
|
1232
|
-
const startPoint = element ? {
|
|
1233
|
-
left: element.center[0],
|
|
1234
|
-
top: element.center[1]
|
|
1235
|
-
} : void 0;
|
|
1236
1606
|
if (!param || !param.direction) throw new Error('PullGesture requires a direction parameter');
|
|
1237
|
-
|
|
1238
|
-
else if ('up' === param.direction) await this.pullUp(startPoint, param.distance, param.duration);
|
|
1239
|
-
else throw new Error(`Unknown pull direction: ${param.direction}`);
|
|
1607
|
+
await this.visualActions.pullGesture(param);
|
|
1240
1608
|
}
|
|
1241
1609
|
})
|
|
1242
1610
|
];
|
|
1243
|
-
const platformActions = createPlatformActions(this);
|
|
1611
|
+
const platformActions = createPlatformActions(this.visualActions);
|
|
1244
1612
|
let platformSpecificActions = Object.values(platformActions);
|
|
1245
1613
|
if (this.options?.exposeRunAdbShellAction === false) platformSpecificActions = platformSpecificActions.filter((action)=>action !== platformActions.RunAdbShell);
|
|
1246
1614
|
const customActions = this.customActions || [];
|
|
@@ -1250,6 +1618,16 @@ ${stdout ? truncateAdbShellStream(stdout, 'stdout') : EMPTY_ADB_SHELL_STDOUT}`;
|
|
|
1250
1618
|
...customActions
|
|
1251
1619
|
];
|
|
1252
1620
|
}
|
|
1621
|
+
async performPullGesture(param) {
|
|
1622
|
+
const element = param.locate;
|
|
1623
|
+
const startPoint = element ? {
|
|
1624
|
+
left: element.center[0],
|
|
1625
|
+
top: element.center[1]
|
|
1626
|
+
} : void 0;
|
|
1627
|
+
if ('down' === param.direction) await this.pullDownRaw(startPoint, param.distance, param.duration);
|
|
1628
|
+
else if ('up' === param.direction) await this.pullUpRaw(startPoint, param.distance, param.duration);
|
|
1629
|
+
else throw new Error(`Unknown pull direction: ${param.direction}`);
|
|
1630
|
+
}
|
|
1253
1631
|
async performActionScroll(param) {
|
|
1254
1632
|
const element = param.locate;
|
|
1255
1633
|
const startingPoint = element ? {
|
|
@@ -1257,20 +1635,32 @@ ${stdout ? truncateAdbShellStream(stdout, 'stdout') : EMPTY_ADB_SHELL_STDOUT}`;
|
|
|
1257
1635
|
top: element.center[1]
|
|
1258
1636
|
} : void 0;
|
|
1259
1637
|
const scrollToEventName = param?.scrollType;
|
|
1260
|
-
if ('scrollToTop' === scrollToEventName) await this.
|
|
1261
|
-
else if ('scrollToBottom' === scrollToEventName) await this.
|
|
1262
|
-
else if ('scrollToRight' === scrollToEventName) await this.
|
|
1263
|
-
else if ('scrollToLeft' === scrollToEventName) await this.
|
|
1638
|
+
if ('scrollToTop' === scrollToEventName) await this.scrollUntilTopRaw(startingPoint);
|
|
1639
|
+
else if ('scrollToBottom' === scrollToEventName) await this.scrollUntilBottomRaw(startingPoint);
|
|
1640
|
+
else if ('scrollToRight' === scrollToEventName) await this.scrollUntilRightRaw(startingPoint);
|
|
1641
|
+
else if ('scrollToLeft' === scrollToEventName) await this.scrollUntilLeftRaw(startingPoint);
|
|
1264
1642
|
else if ('singleAction' !== scrollToEventName && scrollToEventName) throw new Error(`Unknown scroll event type: ${scrollToEventName}, param: ${JSON.stringify(param)}`);
|
|
1265
1643
|
else {
|
|
1266
|
-
if (param?.direction !== 'down' && param && param.direction) if ('up' === param.direction) await this.
|
|
1267
|
-
else if ('left' === param.direction) await this.
|
|
1268
|
-
else if ('right' === param.direction) await this.
|
|
1644
|
+
if (param?.direction !== 'down' && param && param.direction) if ('up' === param.direction) await this.scrollUpRaw(param.distance || void 0, startingPoint);
|
|
1645
|
+
else if ('left' === param.direction) await this.scrollLeftRaw(param.distance || void 0, startingPoint);
|
|
1646
|
+
else if ('right' === param.direction) await this.scrollRightRaw(param.distance || void 0, startingPoint);
|
|
1269
1647
|
else throw new Error(`Unknown scroll direction: ${param.direction}`);
|
|
1270
|
-
else await this.
|
|
1648
|
+
else await this.scrollDownRaw(param?.distance || void 0, startingPoint);
|
|
1271
1649
|
await (0, core_utils_namespaceObject.sleep)(500);
|
|
1272
1650
|
}
|
|
1273
1651
|
}
|
|
1652
|
+
async runAdbShellRaw(param, context) {
|
|
1653
|
+
const adb = await this.getAdb();
|
|
1654
|
+
const stdout = await runAdbShellStdoutOrThrow(adb, param.command, void 0 === param.timeout ? void 0 : {
|
|
1655
|
+
timeout: param.timeout
|
|
1656
|
+
});
|
|
1657
|
+
const planningFeedback = buildRunAdbShellPlanningFeedback({
|
|
1658
|
+
command: param.command,
|
|
1659
|
+
stdout
|
|
1660
|
+
});
|
|
1661
|
+
if (planningFeedback && context?.task) context.task.planningFeedback = planningFeedback;
|
|
1662
|
+
return stdout;
|
|
1663
|
+
}
|
|
1274
1664
|
describe() {
|
|
1275
1665
|
return this.description || `DeviceId: ${this.deviceId}`;
|
|
1276
1666
|
}
|
|
@@ -1367,15 +1757,15 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1367
1757
|
const adapter = this.getScrcpyAdapter();
|
|
1368
1758
|
if (!adapter.isEnabled()) throw new Error('scrcpy is not available for frame observation');
|
|
1369
1759
|
const deviceInfo = await this.getDevicePhysicalInfo();
|
|
1370
|
-
|
|
1371
|
-
const unsubscribe = await adapter.subscribeKeyframes(deviceInfo, (frame)=>{
|
|
1372
|
-
latest = frame;
|
|
1373
|
-
});
|
|
1760
|
+
const unsubscribe = await adapter.subscribeKeyframes(deviceInfo, ()=>{});
|
|
1374
1761
|
return {
|
|
1375
|
-
latest: ()=>
|
|
1762
|
+
latest: ()=>{
|
|
1763
|
+
const latest = adapter.getLatestRawKeyframe();
|
|
1764
|
+
return latest ? {
|
|
1376
1765
|
ref: latest,
|
|
1377
1766
|
capturedAt: latest.capturedAt
|
|
1378
|
-
} : null
|
|
1767
|
+
} : null;
|
|
1768
|
+
},
|
|
1379
1769
|
decode: async (refs)=>{
|
|
1380
1770
|
const images = [];
|
|
1381
1771
|
for (const frameRef of refs)images.push(await adapter.decodeRawKeyframeToJpegBase64(frameRef.ref));
|
|
@@ -1409,6 +1799,9 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1409
1799
|
return this.appNameMapping[normalizedAppName];
|
|
1410
1800
|
}
|
|
1411
1801
|
async launch(uri) {
|
|
1802
|
+
return this.visualActions.launch(uri);
|
|
1803
|
+
}
|
|
1804
|
+
async launchRaw(uri) {
|
|
1412
1805
|
const adb = await this.getAdb();
|
|
1413
1806
|
this.uri = uri;
|
|
1414
1807
|
try {
|
|
@@ -1434,6 +1827,9 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1434
1827
|
return this;
|
|
1435
1828
|
}
|
|
1436
1829
|
async terminate(uri) {
|
|
1830
|
+
await this.visualActions.terminate(uri);
|
|
1831
|
+
}
|
|
1832
|
+
async terminateRaw(uri) {
|
|
1437
1833
|
const packagePart = uri.includes('/') ? uri.split('/')[0] : uri;
|
|
1438
1834
|
const resolved = this.resolvePackageName(packagePart) ?? packagePart;
|
|
1439
1835
|
const adb = await this.getAdb();
|
|
@@ -1449,6 +1845,9 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1449
1845
|
}
|
|
1450
1846
|
}
|
|
1451
1847
|
async execYadb(keyboardContent) {
|
|
1848
|
+
await this.visualActions.execYadb(keyboardContent);
|
|
1849
|
+
}
|
|
1850
|
+
async execYadbRaw(keyboardContent) {
|
|
1452
1851
|
this.warnYadbOnNonDefaultDisplay('keyboard input');
|
|
1453
1852
|
await this.ensureYadb();
|
|
1454
1853
|
const adb = await this.getAdb();
|
|
@@ -1705,14 +2104,15 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1705
2104
|
async screenshotBase64() {
|
|
1706
2105
|
debugDevice('screenshotBase64 begin');
|
|
1707
2106
|
const adapter = this.getScrcpyAdapter();
|
|
2107
|
+
let scrcpyDeviceInfo = null;
|
|
1708
2108
|
if (adapter.isEnabled()) try {
|
|
1709
2109
|
debugDevice('Attempting scrcpy screenshot...');
|
|
1710
|
-
|
|
1711
|
-
const result = await adapter.screenshotBase64(
|
|
2110
|
+
scrcpyDeviceInfo = await this.getDevicePhysicalInfo();
|
|
2111
|
+
const result = await adapter.screenshotBase64(scrcpyDeviceInfo);
|
|
1712
2112
|
debugDevice('screenshotBase64 end (scrcpy mode)');
|
|
1713
2113
|
return result;
|
|
1714
2114
|
} catch (error) {
|
|
1715
|
-
warnDevice(`Scrcpy screenshot failed, falling back to standard ADB method
|
|
2115
|
+
warnDevice(`Scrcpy screenshot failed, falling back to standard ADB method. This may be caused by transport backlog. ${scrcpy_manager.Xr}\nError: ${error}`);
|
|
1716
2116
|
}
|
|
1717
2117
|
const adb = await this.getAdb();
|
|
1718
2118
|
let screenshotBuffer;
|
|
@@ -1790,9 +2190,13 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1790
2190
|
});
|
|
1791
2191
|
}
|
|
1792
2192
|
debugDevice('screenshotBase64 end');
|
|
2193
|
+
if (scrcpyDeviceInfo) adapter.recoverAfterAdbScreenshot(scrcpyDeviceInfo);
|
|
1793
2194
|
return result;
|
|
1794
2195
|
}
|
|
1795
2196
|
async clearInput(element) {
|
|
2197
|
+
await this.visualActions.clearInput(element);
|
|
2198
|
+
}
|
|
2199
|
+
async clearInputRaw(element) {
|
|
1796
2200
|
if (element) await this.tapPoint({
|
|
1797
2201
|
x: element.center[0],
|
|
1798
2202
|
y: element.center[1]
|
|
@@ -1826,6 +2230,9 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1826
2230
|
return '';
|
|
1827
2231
|
}
|
|
1828
2232
|
async scrollUntilTop(startPoint) {
|
|
2233
|
+
await this.visualActions.scrollUntilTop(startPoint);
|
|
2234
|
+
}
|
|
2235
|
+
async scrollUntilTopRaw(startPoint) {
|
|
1829
2236
|
if (startPoint) {
|
|
1830
2237
|
const { height } = await this.size();
|
|
1831
2238
|
const start = {
|
|
@@ -1840,10 +2247,13 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1840
2247
|
await (0, core_utils_namespaceObject.sleep)(1000);
|
|
1841
2248
|
return;
|
|
1842
2249
|
}
|
|
1843
|
-
await (0, utils_namespaceObject.repeat)(defaultScrollUntilTimes, ()=>this.
|
|
2250
|
+
await (0, utils_namespaceObject.repeat)(defaultScrollUntilTimes, ()=>this.scrollRaw(0, -9999999, defaultFastScrollDuration));
|
|
1844
2251
|
await (0, core_utils_namespaceObject.sleep)(1000);
|
|
1845
2252
|
}
|
|
1846
2253
|
async scrollUntilBottom(startPoint) {
|
|
2254
|
+
await this.visualActions.scrollUntilBottom(startPoint);
|
|
2255
|
+
}
|
|
2256
|
+
async scrollUntilBottomRaw(startPoint) {
|
|
1847
2257
|
if (startPoint) {
|
|
1848
2258
|
const start = {
|
|
1849
2259
|
x: Math.round(startPoint.left),
|
|
@@ -1857,10 +2267,13 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1857
2267
|
await (0, core_utils_namespaceObject.sleep)(1000);
|
|
1858
2268
|
return;
|
|
1859
2269
|
}
|
|
1860
|
-
await (0, utils_namespaceObject.repeat)(defaultScrollUntilTimes, ()=>this.
|
|
2270
|
+
await (0, utils_namespaceObject.repeat)(defaultScrollUntilTimes, ()=>this.scrollRaw(0, 9999999, defaultFastScrollDuration));
|
|
1861
2271
|
await (0, core_utils_namespaceObject.sleep)(1000);
|
|
1862
2272
|
}
|
|
1863
2273
|
async scrollUntilLeft(startPoint) {
|
|
2274
|
+
await this.visualActions.scrollUntilLeft(startPoint);
|
|
2275
|
+
}
|
|
2276
|
+
async scrollUntilLeftRaw(startPoint) {
|
|
1864
2277
|
if (startPoint) {
|
|
1865
2278
|
const { width } = await this.size();
|
|
1866
2279
|
const start = {
|
|
@@ -1875,10 +2288,13 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1875
2288
|
await (0, core_utils_namespaceObject.sleep)(1000);
|
|
1876
2289
|
return;
|
|
1877
2290
|
}
|
|
1878
|
-
await (0, utils_namespaceObject.repeat)(defaultScrollUntilTimes, ()=>this.
|
|
2291
|
+
await (0, utils_namespaceObject.repeat)(defaultScrollUntilTimes, ()=>this.scrollRaw(-9999999, 0, defaultFastScrollDuration));
|
|
1879
2292
|
await (0, core_utils_namespaceObject.sleep)(1000);
|
|
1880
2293
|
}
|
|
1881
2294
|
async scrollUntilRight(startPoint) {
|
|
2295
|
+
await this.visualActions.scrollUntilRight(startPoint);
|
|
2296
|
+
}
|
|
2297
|
+
async scrollUntilRightRaw(startPoint) {
|
|
1882
2298
|
if (startPoint) {
|
|
1883
2299
|
const start = {
|
|
1884
2300
|
x: Math.round(startPoint.left),
|
|
@@ -1892,10 +2308,13 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1892
2308
|
await (0, core_utils_namespaceObject.sleep)(1000);
|
|
1893
2309
|
return;
|
|
1894
2310
|
}
|
|
1895
|
-
await (0, utils_namespaceObject.repeat)(defaultScrollUntilTimes, ()=>this.
|
|
2311
|
+
await (0, utils_namespaceObject.repeat)(defaultScrollUntilTimes, ()=>this.scrollRaw(9999999, 0, defaultFastScrollDuration));
|
|
1896
2312
|
await (0, core_utils_namespaceObject.sleep)(1000);
|
|
1897
2313
|
}
|
|
1898
2314
|
async scrollUp(distance, startPoint) {
|
|
2315
|
+
await this.visualActions.scrollUp(distance, startPoint);
|
|
2316
|
+
}
|
|
2317
|
+
async scrollUpRaw(distance, startPoint) {
|
|
1899
2318
|
const { height } = await this.size();
|
|
1900
2319
|
const scrollDistance = Math.round(distance || height);
|
|
1901
2320
|
const hasExplicitDistance = void 0 !== distance;
|
|
@@ -1909,9 +2328,12 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1909
2328
|
await this.dragPoint(start, end);
|
|
1910
2329
|
return;
|
|
1911
2330
|
}
|
|
1912
|
-
await this.
|
|
2331
|
+
await this.scrollRaw(0, -scrollDistance, void 0, hasExplicitDistance, 'up');
|
|
1913
2332
|
}
|
|
1914
2333
|
async scrollDown(distance, startPoint) {
|
|
2334
|
+
await this.visualActions.scrollDown(distance, startPoint);
|
|
2335
|
+
}
|
|
2336
|
+
async scrollDownRaw(distance, startPoint) {
|
|
1915
2337
|
const { height } = await this.size();
|
|
1916
2338
|
const scrollDistance = Math.round(distance || height);
|
|
1917
2339
|
const hasExplicitDistance = void 0 !== distance;
|
|
@@ -1925,9 +2347,12 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1925
2347
|
await this.dragPoint(start, end);
|
|
1926
2348
|
return;
|
|
1927
2349
|
}
|
|
1928
|
-
await this.
|
|
2350
|
+
await this.scrollRaw(0, scrollDistance, void 0, hasExplicitDistance, 'down');
|
|
1929
2351
|
}
|
|
1930
2352
|
async scrollLeft(distance, startPoint) {
|
|
2353
|
+
await this.visualActions.scrollLeft(distance, startPoint);
|
|
2354
|
+
}
|
|
2355
|
+
async scrollLeftRaw(distance, startPoint) {
|
|
1931
2356
|
const { width } = await this.size();
|
|
1932
2357
|
const scrollDistance = Math.round(distance || width);
|
|
1933
2358
|
const hasExplicitDistance = void 0 !== distance;
|
|
@@ -1941,9 +2366,12 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1941
2366
|
await this.dragPoint(start, end);
|
|
1942
2367
|
return;
|
|
1943
2368
|
}
|
|
1944
|
-
await this.
|
|
2369
|
+
await this.scrollRaw(-scrollDistance, 0, void 0, hasExplicitDistance, 'left');
|
|
1945
2370
|
}
|
|
1946
2371
|
async scrollRight(distance, startPoint) {
|
|
2372
|
+
await this.visualActions.scrollRight(distance, startPoint);
|
|
2373
|
+
}
|
|
2374
|
+
async scrollRightRaw(distance, startPoint) {
|
|
1947
2375
|
const { width } = await this.size();
|
|
1948
2376
|
const scrollDistance = Math.round(distance || width);
|
|
1949
2377
|
const hasExplicitDistance = void 0 !== distance;
|
|
@@ -1957,7 +2385,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1957
2385
|
await this.dragPoint(start, end);
|
|
1958
2386
|
return;
|
|
1959
2387
|
}
|
|
1960
|
-
await this.
|
|
2388
|
+
await this.scrollRaw(scrollDistance, 0, void 0, hasExplicitDistance, 'right');
|
|
1961
2389
|
}
|
|
1962
2390
|
async ensureYadb() {
|
|
1963
2391
|
if (!this.yadbPushed) {
|
|
@@ -1986,11 +2414,11 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1986
2414
|
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)';
|
|
1987
2415
|
throw new Error(`${reason}, but yadb (app_process) cannot target non-default displayId=${this.options?.displayId}. Use displayId=0 or a different imeStrategy.`);
|
|
1988
2416
|
}
|
|
1989
|
-
if (needsYadb) await this.
|
|
2417
|
+
if (needsYadb) await this.execYadbRaw(escapeForShell(text));
|
|
1990
2418
|
else await this.shellInputText(text, {
|
|
1991
2419
|
keyboardTypeDelay: typeDelay
|
|
1992
2420
|
});
|
|
1993
|
-
if (true === shouldAutoDismissKeyboard) await this.
|
|
2421
|
+
if (true === shouldAutoDismissKeyboard) await this.hideKeyboardRaw(options);
|
|
1994
2422
|
}
|
|
1995
2423
|
normalizeKeyName(key) {
|
|
1996
2424
|
const keyMap = {
|
|
@@ -2060,6 +2488,9 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
2060
2488
|
await adb.shell(`input${this.getDisplayArg()} swipe ${fromX} ${fromY} ${toX} ${toY} ${duration}`);
|
|
2061
2489
|
}
|
|
2062
2490
|
async scroll(deltaX, deltaY, duration, warnOnClamp = false, direction) {
|
|
2491
|
+
await this.visualActions.scroll(deltaX, deltaY, duration, warnOnClamp, direction);
|
|
2492
|
+
}
|
|
2493
|
+
async scrollRaw(deltaX, deltaY, duration, warnOnClamp = false, direction) {
|
|
2063
2494
|
if (0 === deltaX && 0 === deltaY) throw new Error('Scroll distance cannot be zero in both directions');
|
|
2064
2495
|
const { width, height } = await this.size();
|
|
2065
2496
|
const n = 4;
|
|
@@ -2124,12 +2555,21 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
2124
2555
|
}
|
|
2125
2556
|
}
|
|
2126
2557
|
async back() {
|
|
2558
|
+
await this.visualActions.back();
|
|
2559
|
+
}
|
|
2560
|
+
async backRaw() {
|
|
2127
2561
|
await this.shellInputKeyevent(4);
|
|
2128
2562
|
}
|
|
2129
2563
|
async home() {
|
|
2564
|
+
await this.visualActions.home();
|
|
2565
|
+
}
|
|
2566
|
+
async homeRaw() {
|
|
2130
2567
|
await this.shellInputKeyevent(3);
|
|
2131
2568
|
}
|
|
2132
2569
|
async recentApps() {
|
|
2570
|
+
await this.visualActions.recentApps();
|
|
2571
|
+
}
|
|
2572
|
+
async recentAppsRaw() {
|
|
2133
2573
|
await this.shellInputKeyevent(187);
|
|
2134
2574
|
}
|
|
2135
2575
|
async longPressPoint(point, duration = 2000) {
|
|
@@ -2138,6 +2578,9 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
2138
2578
|
await adb.shell(`input${this.getDisplayArg()} swipe ${adjustedX} ${adjustedY} ${adjustedX} ${adjustedY} ${duration}`);
|
|
2139
2579
|
}
|
|
2140
2580
|
async pullDown(startPoint, distance, duration = 800) {
|
|
2581
|
+
await this.visualActions.pullDown(startPoint, distance, duration);
|
|
2582
|
+
}
|
|
2583
|
+
async pullDownRaw(startPoint, distance, duration = 800) {
|
|
2141
2584
|
const { width, height } = await this.size();
|
|
2142
2585
|
const start = startPoint ? {
|
|
2143
2586
|
x: Math.round(startPoint.left),
|
|
@@ -2151,13 +2594,19 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
2151
2594
|
x: start.x,
|
|
2152
2595
|
y: start.y + pullDistance
|
|
2153
2596
|
};
|
|
2154
|
-
await this.
|
|
2597
|
+
await this.pullDragRaw(start, end, duration);
|
|
2155
2598
|
await (0, core_utils_namespaceObject.sleep)(200);
|
|
2156
2599
|
}
|
|
2157
2600
|
async pullDrag(from, to, duration) {
|
|
2601
|
+
await this.visualActions.pullDrag(from, to, duration);
|
|
2602
|
+
}
|
|
2603
|
+
async pullDragRaw(from, to, duration) {
|
|
2158
2604
|
await this.swipePoint(from, to, duration);
|
|
2159
2605
|
}
|
|
2160
2606
|
async pullUp(startPoint, distance, duration = 600) {
|
|
2607
|
+
await this.visualActions.pullUp(startPoint, distance, duration);
|
|
2608
|
+
}
|
|
2609
|
+
async pullUpRaw(startPoint, distance, duration = 600) {
|
|
2161
2610
|
const { width, height } = await this.size();
|
|
2162
2611
|
const start = startPoint ? {
|
|
2163
2612
|
x: Math.round(startPoint.left),
|
|
@@ -2171,7 +2620,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
2171
2620
|
x: start.x,
|
|
2172
2621
|
y: start.y - pullDistance
|
|
2173
2622
|
};
|
|
2174
|
-
await this.
|
|
2623
|
+
await this.pullDragRaw(start, end, duration);
|
|
2175
2624
|
await (0, core_utils_namespaceObject.sleep)(100);
|
|
2176
2625
|
}
|
|
2177
2626
|
getDisplayArg() {
|
|
@@ -2234,6 +2683,9 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
2234
2683
|
}
|
|
2235
2684
|
}
|
|
2236
2685
|
async hideKeyboard(options, timeoutMs = 1000) {
|
|
2686
|
+
return this.visualActions.hideKeyboard(options, timeoutMs);
|
|
2687
|
+
}
|
|
2688
|
+
async hideKeyboardRaw(options, timeoutMs = 1000) {
|
|
2237
2689
|
const adb = await this.getAdb();
|
|
2238
2690
|
const keyboardDismissStrategy = options?.keyboardDismissStrategy ?? this.options?.keyboardDismissStrategy ?? 'esc-first';
|
|
2239
2691
|
const keyboardStatus = await adb.isSoftKeyboardPresent();
|
|
@@ -2288,55 +2740,91 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
2288
2740
|
device_define_property(this, "interfaceType", 'android');
|
|
2289
2741
|
device_define_property(this, "uri", void 0);
|
|
2290
2742
|
device_define_property(this, "options", void 0);
|
|
2743
|
+
device_define_property(this, "visualActions", createVisualActionRegistry({
|
|
2744
|
+
tap: (point)=>this.tapPoint(point),
|
|
2745
|
+
doubleClick: (point)=>this.doubleTapPoint(point),
|
|
2746
|
+
longPress: (point, opts)=>this.longPressPoint(point, opts?.duration),
|
|
2747
|
+
dragAndDrop: (from, to)=>this.dragPoint(from, to),
|
|
2748
|
+
keyboardPress: (keyName)=>this.pressKey(keyName),
|
|
2749
|
+
typeText: async (value, opts)=>{
|
|
2750
|
+
const target = opts?.target;
|
|
2751
|
+
if (target && opts?.replace !== false) await this.clearInputRaw(target);
|
|
2752
|
+
else if (target) await this.tapPoint({
|
|
2753
|
+
x: target.center[0],
|
|
2754
|
+
y: target.center[1]
|
|
2755
|
+
});
|
|
2756
|
+
if (opts?.focusOnly) return;
|
|
2757
|
+
await this.typeText(value, opts);
|
|
2758
|
+
},
|
|
2759
|
+
clearInput: (target)=>this.clearInputRaw(target),
|
|
2760
|
+
cursorMove: async (direction, times = 1)=>{
|
|
2761
|
+
const arrowKey = 'left' === direction ? 'ArrowLeft' : 'ArrowRight';
|
|
2762
|
+
for(let index = 0; index < times; index++)await this.pressKey(arrowKey);
|
|
2763
|
+
},
|
|
2764
|
+
swipe: async (start, end, opts)=>{
|
|
2765
|
+
const duration = opts?.duration ?? 300;
|
|
2766
|
+
const repeatCount = opts?.repeat ?? 1;
|
|
2767
|
+
for(let index = 0; index < repeatCount; index++)await this.dragPoint(start, end, duration);
|
|
2768
|
+
},
|
|
2769
|
+
pinch: async (center, opts)=>{
|
|
2770
|
+
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.`);
|
|
2771
|
+
const { x: adjCenterX, y: adjCenterY } = await this.adjustCoordinates(Math.round(center.x), Math.round(center.y));
|
|
2772
|
+
const ratio = 0 !== adjCenterX && 0 !== center.x ? adjCenterX / center.x : 1;
|
|
2773
|
+
const adjStartDist = Math.round(opts.startDistance * ratio);
|
|
2774
|
+
const adjEndDist = Math.round(opts.endDistance * ratio);
|
|
2775
|
+
await this.ensureYadb();
|
|
2776
|
+
const adb = await this.getAdb();
|
|
2777
|
+
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}`);
|
|
2778
|
+
},
|
|
2779
|
+
actionScroll: (param)=>this.performActionScroll(param),
|
|
2780
|
+
back: ()=>this.backRaw(),
|
|
2781
|
+
home: ()=>this.homeRaw(),
|
|
2782
|
+
recentApps: ()=>this.recentAppsRaw(),
|
|
2783
|
+
pullGesture: (param)=>this.performPullGesture(param),
|
|
2784
|
+
launch: (uri)=>this.launchRaw(uri),
|
|
2785
|
+
terminate: (uri)=>this.terminateRaw(uri),
|
|
2786
|
+
runAdbShell: (param, context)=>this.runAdbShellRaw(param, context),
|
|
2787
|
+
execYadb: (keyboardContent)=>this.execYadbRaw(keyboardContent),
|
|
2788
|
+
scrollUntilTop: (startPoint)=>this.scrollUntilTopRaw(startPoint),
|
|
2789
|
+
scrollUntilBottom: (startPoint)=>this.scrollUntilBottomRaw(startPoint),
|
|
2790
|
+
scrollUntilLeft: (startPoint)=>this.scrollUntilLeftRaw(startPoint),
|
|
2791
|
+
scrollUntilRight: (startPoint)=>this.scrollUntilRightRaw(startPoint),
|
|
2792
|
+
scrollUp: (distance, startPoint)=>this.scrollUpRaw(distance, startPoint),
|
|
2793
|
+
scrollDown: (distance, startPoint)=>this.scrollDownRaw(distance, startPoint),
|
|
2794
|
+
scrollLeft: (distance, startPoint)=>this.scrollLeftRaw(distance, startPoint),
|
|
2795
|
+
scrollRight: (distance, startPoint)=>this.scrollRightRaw(distance, startPoint),
|
|
2796
|
+
scroll: (deltaX, deltaY, duration, warnOnClamp, direction)=>this.scrollRaw(deltaX, deltaY, duration, warnOnClamp, direction),
|
|
2797
|
+
pullDown: (startPoint, distance, duration)=>this.pullDownRaw(startPoint, distance, duration),
|
|
2798
|
+
pullDrag: (from, to, duration)=>this.pullDragRaw(from, to, duration),
|
|
2799
|
+
pullUp: (startPoint, distance, duration)=>this.pullUpRaw(startPoint, distance, duration),
|
|
2800
|
+
hideKeyboard: (options, timeoutMs)=>this.hideKeyboardRaw(options, timeoutMs)
|
|
2801
|
+
}, async ()=>{
|
|
2802
|
+
await this.scrcpyAdapter?.markActionBarrier();
|
|
2803
|
+
}));
|
|
2291
2804
|
device_define_property(this, "inputPrimitives", {
|
|
2292
2805
|
pointer: {
|
|
2293
|
-
tap:
|
|
2294
|
-
doubleClick:
|
|
2295
|
-
longPress:
|
|
2296
|
-
dragAndDrop:
|
|
2806
|
+
tap: this.visualActions.tap,
|
|
2807
|
+
doubleClick: this.visualActions.doubleClick,
|
|
2808
|
+
longPress: this.visualActions.longPress,
|
|
2809
|
+
dragAndDrop: this.visualActions.dragAndDrop
|
|
2297
2810
|
},
|
|
2298
2811
|
keyboard: {
|
|
2299
|
-
keyboardPress:
|
|
2300
|
-
typeText:
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
else if (target) await this.tapPoint({
|
|
2304
|
-
x: target.center[0],
|
|
2305
|
-
y: target.center[1]
|
|
2306
|
-
});
|
|
2307
|
-
if (opts?.focusOnly) return;
|
|
2308
|
-
await this.typeText(value, opts);
|
|
2309
|
-
},
|
|
2310
|
-
clearInput: (target)=>this.clearInput(target),
|
|
2311
|
-
cursorMove: async (direction, times = 1)=>{
|
|
2312
|
-
const arrowKey = 'left' === direction ? 'ArrowLeft' : 'ArrowRight';
|
|
2313
|
-
for(let i = 0; i < times; i++)await this.pressKey(arrowKey);
|
|
2314
|
-
}
|
|
2812
|
+
keyboardPress: this.visualActions.keyboardPress,
|
|
2813
|
+
typeText: this.visualActions.typeText,
|
|
2814
|
+
clearInput: this.visualActions.clearInput,
|
|
2815
|
+
cursorMove: this.visualActions.cursorMove
|
|
2315
2816
|
},
|
|
2316
2817
|
touch: {
|
|
2317
|
-
swipe:
|
|
2318
|
-
|
|
2319
|
-
const repeatCount = opts?.repeat ?? 1;
|
|
2320
|
-
for(let i = 0; i < repeatCount; i++)await this.dragPoint(start, end, duration);
|
|
2321
|
-
},
|
|
2322
|
-
pinch: async (center, opts)=>{
|
|
2323
|
-
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.`);
|
|
2324
|
-
const { x: adjCenterX, y: adjCenterY } = await this.adjustCoordinates(Math.round(center.x), Math.round(center.y));
|
|
2325
|
-
const ratio = 0 !== adjCenterX && 0 !== center.x ? adjCenterX / center.x : 1;
|
|
2326
|
-
const adjStartDist = Math.round(opts.startDistance * ratio);
|
|
2327
|
-
const adjEndDist = Math.round(opts.endDistance * ratio);
|
|
2328
|
-
await this.ensureYadb();
|
|
2329
|
-
const adb = await this.getAdb();
|
|
2330
|
-
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}`);
|
|
2331
|
-
}
|
|
2818
|
+
swipe: this.visualActions.swipe,
|
|
2819
|
+
pinch: this.visualActions.pinch
|
|
2332
2820
|
},
|
|
2333
2821
|
scroll: {
|
|
2334
|
-
scroll:
|
|
2822
|
+
scroll: this.visualActions.actionScroll
|
|
2335
2823
|
},
|
|
2336
2824
|
system: {
|
|
2337
|
-
backButton:
|
|
2338
|
-
homeButton:
|
|
2339
|
-
recentAppsButton:
|
|
2825
|
+
backButton: this.visualActions.back,
|
|
2826
|
+
homeButton: this.visualActions.home,
|
|
2827
|
+
recentAppsButton: this.visualActions.recentApps
|
|
2340
2828
|
}
|
|
2341
2829
|
});
|
|
2342
2830
|
external_node_assert_default()(deviceId, 'deviceId is required for AndroidDevice');
|
|
@@ -2358,7 +2846,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
2358
2846
|
const terminateParamSchema = core_namespaceObject.z.object({
|
|
2359
2847
|
uri: core_namespaceObject.z.string().describe('Package name or app name to terminate. Use the exact package name, e.g. com.android.settings.')
|
|
2360
2848
|
});
|
|
2361
|
-
const createPlatformActions = (
|
|
2849
|
+
const createPlatformActions = (visualActions)=>({
|
|
2362
2850
|
RunAdbShell: (0, device_namespaceObject.defineAction)({
|
|
2363
2851
|
name: 'RunAdbShell',
|
|
2364
2852
|
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.',
|
|
@@ -2369,16 +2857,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
2369
2857
|
},
|
|
2370
2858
|
call: async (param, context)=>{
|
|
2371
2859
|
if (!param.command || '' === param.command.trim()) throw new Error('RunAdbShell requires a non-empty command parameter');
|
|
2372
|
-
|
|
2373
|
-
const stdout = await runAdbShellStdoutOrThrow(adb, param.command, void 0 === param.timeout ? void 0 : {
|
|
2374
|
-
timeout: param.timeout
|
|
2375
|
-
});
|
|
2376
|
-
const planningFeedback = buildRunAdbShellPlanningFeedback({
|
|
2377
|
-
command: param.command,
|
|
2378
|
-
stdout
|
|
2379
|
-
});
|
|
2380
|
-
if (planningFeedback && context?.task) context.task.planningFeedback = planningFeedback;
|
|
2381
|
-
return stdout;
|
|
2860
|
+
return visualActions.runAdbShell(param, context);
|
|
2382
2861
|
}
|
|
2383
2862
|
}),
|
|
2384
2863
|
Launch: (0, device_namespaceObject.defineAction)({
|
|
@@ -2391,7 +2870,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
2391
2870
|
},
|
|
2392
2871
|
call: async (param)=>{
|
|
2393
2872
|
if (!param.uri || '' === param.uri.trim()) throw new Error('Launch requires a non-empty uri parameter');
|
|
2394
|
-
await
|
|
2873
|
+
await visualActions.launch(param.uri);
|
|
2395
2874
|
}
|
|
2396
2875
|
}),
|
|
2397
2876
|
Terminate: (0, device_namespaceObject.defineAction)({
|
|
@@ -2401,7 +2880,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
2401
2880
|
paramSchema: terminateParamSchema,
|
|
2402
2881
|
call: async (param)=>{
|
|
2403
2882
|
if (!param.uri || '' === param.uri.trim()) throw new Error('Terminate requires a non-empty uri parameter');
|
|
2404
|
-
await
|
|
2883
|
+
await visualActions.terminate(param.uri);
|
|
2405
2884
|
}
|
|
2406
2885
|
})
|
|
2407
2886
|
});
|
|
@@ -2600,7 +3079,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
2600
3079
|
const tools = new AndroidMidsceneTools();
|
|
2601
3080
|
(0, cli_namespaceObject.runToolsCLI)(tools, 'midscene-android', {
|
|
2602
3081
|
stripPrefix: 'android_',
|
|
2603
|
-
version: "1.10.
|
|
3082
|
+
version: "1.10.10-beta-20260805080137.0",
|
|
2604
3083
|
extraCommands: (0, core_namespaceObject.createReportCliCommands)()
|
|
2605
3084
|
}).catch((e)=>{
|
|
2606
3085
|
process.exit((0, cli_namespaceObject.reportCLIError)(e));
|