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