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