@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/lib/cli.js CHANGED
@@ -5,8 +5,10 @@ const __rslib_import_meta_url__ = /*#__PURE__*/ function() {
5
5
  var __webpack_modules__ = {
6
6
  "./src/scrcpy-manager.ts" (__unused_rspack_module, __webpack_exports__, __webpack_require__) {
7
7
  __webpack_require__.d(__webpack_exports__, {
8
+ GJ: ()=>isScrcpyFreshFrameUnavailableError,
8
9
  ScrcpyScreenshotManager: ()=>ScrcpyScreenshotManager,
9
- o: ()=>DEFAULT_SCRCPY_CONFIG
10
+ Xr: ()=>SCRCPY_VIDEO_BIT_RATE_NETWORK_HINT,
11
+ ov: ()=>DEFAULT_SCRCPY_CONFIG
10
12
  });
11
13
  var node_fs__rspack_import_0 = __webpack_require__("node:fs");
12
14
  var node_module__rspack_import_1 = __webpack_require__("node:module");
@@ -42,6 +44,14 @@ var __webpack_modules__ = {
42
44
  const CONNECTION_WAIT_MS = 1000;
43
45
  const MAX_SERVER_OUTPUT_LINES = 100;
44
46
  const SERVER_OUTPUT_DRAIN_TIMEOUT_MS = 500;
47
+ const MAX_FRAME_AGE_US = 500000n;
48
+ const FRAME_FRESHNESS_WARN_INTERVAL_MS = 5000;
49
+ const TRANSPORT_BACKLOG_WARN_INTERVAL_MS = 5000;
50
+ const DEVICE_UPTIME_COMMAND = [
51
+ 'dumpsys',
52
+ 'power'
53
+ ];
54
+ const SCRCPY_VIDEO_BIT_RATE_NETWORK_HINT = 'The appropriate scrcpy video bitrate depends on network conditions. For constrained remote links, consider setting scrcpyConfig.videoBitRate to 4_000_000 (4 Mbps) as a starting point, and lower it further if backlog persists.';
45
55
  const BUSY_LOOP_WINDOW_MS = 1000;
46
56
  const BUSY_LOOP_MAX_READS = 500;
47
57
  const BUSY_LOOP_COOLDOWN_MS = 50;
@@ -52,6 +62,24 @@ var __webpack_modules__ = {
52
62
  idleTimeoutMs: DEFAULT_IDLE_TIMEOUT_MS,
53
63
  videoBitRate: DEFAULT_VIDEO_BIT_RATE
54
64
  };
65
+ const SCRCPY_FRESH_FRAME_UNAVAILABLE_ERROR_CODE = 'ERR_SCRCPY_FRESH_FRAME_UNAVAILABLE';
66
+ class ScrcpyFreshFrameUnavailableError extends Error {
67
+ constructor(message, options){
68
+ super(message, options), _define_property(this, "code", SCRCPY_FRESH_FRAME_UNAVAILABLE_ERROR_CODE);
69
+ this.name = 'ScrcpyFreshFrameUnavailableError';
70
+ }
71
+ }
72
+ function isScrcpyFreshFrameUnavailableError(error) {
73
+ return 'object' == typeof error && null !== error && 'code' in error && error.code === SCRCPY_FRESH_FRAME_UNAVAILABLE_ERROR_CODE;
74
+ }
75
+ function parseDeviceUptimeMs(output) {
76
+ const match = output.match(/mLastWakeTime=(\d+)\s+\((?:(\d+) ms ago|in (\d+) ms|now)\)/);
77
+ if (!match) throw new Error(`Unable to read Android device uptime from dumpsys power: ${output.trim() || '<empty output>'}`);
78
+ const referenceMs = BigInt(match[1]);
79
+ if (match[2]) return referenceMs + BigInt(match[2]);
80
+ if (match[3]) return referenceMs - BigInt(match[3]);
81
+ return referenceMs;
82
+ }
55
83
  function isKeyFrameNalType(nalUnitType) {
56
84
  return nalUnitType === NAL_TYPE_IDR || nalUnitType === NAL_TYPE_SPS || nalUnitType === NAL_TYPE_PPS;
57
85
  }
@@ -234,24 +262,25 @@ var __webpack_modules__ = {
234
262
  debugScrcpy(`Received SPS/PPS configuration: ${this.spsHeader.length}B`);
235
263
  return;
236
264
  }
265
+ const receivedAtUs = this.monotonicTimeUs();
266
+ if (!this.isFrameFresh(packet.pts)) return;
237
267
  const frameBuffer = Buffer.from(packet.data);
238
268
  const isKeyFrame = detectH264KeyFrame(frameBuffer);
239
269
  if (isKeyFrame && this.spsHeader) {
270
+ const timing = this.estimateFrameTiming(packet.pts, receivedAtUs);
240
271
  this.lastRawKeyframe = frameBuffer;
241
- this.lastRawKeyframeAt = Date.now();
242
- if (this.keyframeResolvers.length > 0) {
243
- const combined = Buffer.concat([
244
- this.spsHeader,
245
- frameBuffer
246
- ]);
247
- this.notifyKeyframeWaiters(combined);
248
- }
249
- if (this.keyframeListeners.size > 0) {
250
- const frame = {
251
- data: frameBuffer,
252
- header: this.spsHeader,
253
- capturedAt: this.lastRawKeyframeAt
254
- };
272
+ this.lastRawKeyframeAt = timing.capturedAt;
273
+ this.lastRawKeyframePtsUs = packet.pts;
274
+ this.lastRawKeyframeEstimatedAgeMs = timing.estimatedAgeMs;
275
+ const frame = {
276
+ data: frameBuffer,
277
+ header: this.spsHeader,
278
+ ptsUs: packet.pts,
279
+ estimatedAgeMs: timing.estimatedAgeMs,
280
+ capturedAt: this.lastRawKeyframeAt
281
+ };
282
+ if (this.keyframeResolvers.length > 0) this.notifyKeyframeWaiters(frame);
283
+ if (this.keyframeListeners.size > 0 && this.isFrameAgeAcceptable(packet.pts, receivedAtUs)) {
255
284
  for (const listener of this.keyframeListeners)try {
256
285
  listener(frame);
257
286
  } catch (error) {
@@ -261,6 +290,159 @@ var __webpack_modules__ = {
261
290
  }
262
291
  }
263
292
  }
293
+ async readDeviceClockCalibration() {
294
+ const startedAtUs = this.monotonicTimeUs();
295
+ const startedAtWallMs = Date.now();
296
+ const shellProtocol = this.adb.subprocess.shellProtocol;
297
+ let output;
298
+ if (shellProtocol) {
299
+ const result = await shellProtocol.spawnWaitText(DEVICE_UPTIME_COMMAND);
300
+ if (0 !== result.exitCode) throw new Error(`Unable to read Android device uptime (exit ${result.exitCode}): ${result.stderr.trim() || result.stdout.trim()}`);
301
+ output = result.stdout;
302
+ } else output = await this.adb.subprocess.noneProtocol.spawnWaitText(DEVICE_UPTIME_COMMAND);
303
+ const finishedAtUs = this.monotonicTimeUs();
304
+ const finishedAtWallMs = Date.now();
305
+ return {
306
+ deviceUptimeUs: 1000n * parseDeviceUptimeMs(output),
307
+ hostMonotonicUs: startedAtUs + (finishedAtUs - startedAtUs) / 2n,
308
+ hostWallTimeMs: startedAtWallMs + (finishedAtWallMs - startedAtWallMs) / 2,
309
+ roundTripUs: finishedAtUs - startedAtUs
310
+ };
311
+ }
312
+ async ensureFrameClockCalibration() {
313
+ if (this.deviceClockCalibration) return;
314
+ this.deviceClockCalibration = await this.readDeviceClockCalibration();
315
+ debugScrcpy(`Calibrated scrcpy frame clock (RTT=${Number(this.deviceClockCalibration.roundTripUs / 1000n)}ms)`);
316
+ }
317
+ async setFreshnessBarrier(reason) {
318
+ const generation = ++this.frameFreshnessBarrierGeneration;
319
+ this.frameFreshnessBarrierPending = true;
320
+ this.clearFrameCache();
321
+ try {
322
+ const calibration = await this.readDeviceClockCalibration();
323
+ const barrierPtsUs = (calibration.deviceUptimeUs / 1000n + 1n) * 1000n;
324
+ if (generation !== this.frameFreshnessBarrierGeneration) return this.frameFreshnessBarrierPtsUs ?? barrierPtsUs;
325
+ this.deviceClockCalibration = calibration;
326
+ this.frameFreshnessBarrierPtsUs = null === this.frameFreshnessBarrierPtsUs || barrierPtsUs > this.frameFreshnessBarrierPtsUs ? barrierPtsUs : this.frameFreshnessBarrierPtsUs;
327
+ this.frameFreshnessBarrierReason = reason;
328
+ this.frameFreshnessBarrierPending = false;
329
+ this.frameFreshnessError = null;
330
+ this.lastFramePtsUs = null;
331
+ this.clearFrameCache();
332
+ debugScrcpy(`Armed frame freshness barrier at PTS ${this.frameFreshnessBarrierPtsUs}µs (${reason}, clock RTT=${Number(calibration.roundTripUs / 1000n)}ms)`);
333
+ return this.frameFreshnessBarrierPtsUs;
334
+ } catch (error) {
335
+ if (generation === this.frameFreshnessBarrierGeneration) {
336
+ this.frameFreshnessBarrierPending = false;
337
+ this.frameFreshnessError = new Error(`Unable to establish scrcpy frame freshness barrier (${reason}): ${error instanceof Error ? error.message : String(error)}`, {
338
+ cause: error
339
+ });
340
+ }
341
+ throw this.frameFreshnessError ?? error;
342
+ }
343
+ }
344
+ isFrameFresh(packetPtsUs) {
345
+ if (this.frameFreshnessBarrierPending) return false;
346
+ if (void 0 !== packetPtsUs) {
347
+ if (null !== this.lastFramePtsUs && packetPtsUs < this.lastFramePtsUs) {
348
+ this.frameFreshnessError = new Error('Scrcpy frame PTS moved backwards; refusing frames until the device clock barrier is refreshed');
349
+ this.frameFreshnessBarrierPending = true;
350
+ this.clearFrameCache();
351
+ this.warnFrameFreshness();
352
+ return false;
353
+ }
354
+ this.lastFramePtsUs = packetPtsUs;
355
+ }
356
+ if (null === this.frameFreshnessBarrierPtsUs) return true;
357
+ if (void 0 === packetPtsUs) {
358
+ this.frameFreshnessError = new Error('Scrcpy frame has no PTS metadata; cannot prove that it is newer than the freshness barrier');
359
+ this.warnFrameFreshness();
360
+ return false;
361
+ }
362
+ if (packetPtsUs >= this.frameFreshnessBarrierPtsUs) {
363
+ if (this.frameFreshnessError) debugScrcpy(`Scrcpy video crossed the ${this.frameFreshnessBarrierReason ?? 'active'} freshness barrier at PTS ${packetPtsUs}µs`);
364
+ this.frameFreshnessError = null;
365
+ return true;
366
+ }
367
+ const behindBarrierUs = this.frameFreshnessBarrierPtsUs - packetPtsUs;
368
+ this.frameFreshnessError = new Error(`Scrcpy frame predates the ${this.frameFreshnessBarrierReason ?? 'active'} freshness barrier by ${Number(behindBarrierUs) / 1000}ms; refusing to use it`);
369
+ this.clearFrameCache();
370
+ this.warnFrameFreshness();
371
+ return false;
372
+ }
373
+ estimateFrameAgeUs(packetPtsUs, hostMonotonicUs = this.monotonicTimeUs()) {
374
+ const calibration = this.deviceClockCalibration;
375
+ if (void 0 === packetPtsUs || !calibration) return null;
376
+ const estimatedDeviceNowUs = calibration.deviceUptimeUs + (hostMonotonicUs - calibration.hostMonotonicUs);
377
+ return estimatedDeviceNowUs > packetPtsUs ? estimatedDeviceNowUs - packetPtsUs : 0n;
378
+ }
379
+ isFrameAgeAcceptable(packetPtsUs, hostMonotonicUs = this.monotonicTimeUs()) {
380
+ const ageUs = this.estimateFrameAgeUs(packetPtsUs, hostMonotonicUs);
381
+ if (null === ageUs) {
382
+ this.frameFreshnessError = new Error(void 0 === packetPtsUs ? 'Scrcpy frame has no PTS metadata; cannot prove its absolute age' : 'Scrcpy frame clock is not calibrated; cannot prove its absolute age');
383
+ this.warnFrameFreshness();
384
+ return false;
385
+ }
386
+ if (ageUs <= MAX_FRAME_AGE_US) {
387
+ if (this.frameFreshnessError) {
388
+ debugScrcpy(`Scrcpy frame age recovered (${Number(ageUs / 1000n)}ms)`);
389
+ this.frameFreshnessError = null;
390
+ }
391
+ return true;
392
+ }
393
+ this.frameFreshnessError = new Error(`Scrcpy frame absolute age is ${Number(ageUs / 1000n)}ms, exceeding the ${Number(MAX_FRAME_AGE_US / 1000n)}ms limit`);
394
+ this.warnFrameFreshness();
395
+ return false;
396
+ }
397
+ warnFrameFreshness() {
398
+ if (!this.frameFreshnessError) return;
399
+ const now = Date.now();
400
+ if (now - this.lastFrameFreshnessWarningAt >= FRAME_FRESHNESS_WARN_INTERVAL_MS) {
401
+ warnScrcpy(this.frameFreshnessError.message);
402
+ this.lastFrameFreshnessWarningAt = now;
403
+ }
404
+ }
405
+ warnTransportBacklog(error) {
406
+ const now = Date.now();
407
+ if (now - this.lastTransportBacklogWarningAt < TRANSPORT_BACKLOG_WARN_INTERVAL_MS) return;
408
+ const cause = this.frameFreshnessError ?? error;
409
+ const causeMessage = cause instanceof Error ? cause.message : String(cause);
410
+ const currentBitRateMbps = this.options.videoBitRate / 1000000;
411
+ warnScrcpy(`No usable scrcpy frame crossed the active freshness target within ${FRESH_FRAME_TIMEOUT_MS}ms; closing the stale stream epoch and falling back to ADB screenshot. This may indicate transport backlog or a static screen that emitted no new frame. ${SCRCPY_VIDEO_BIT_RATE_NETWORK_HINT} Current videoBitRate: ${this.options.videoBitRate} bps (${currentBitRateMbps} Mbps).\nError: ${causeMessage}`);
412
+ this.lastTransportBacklogWarningAt = now;
413
+ }
414
+ estimateFrameTiming(packetPtsUs, receivedAtUs) {
415
+ const calibration = this.deviceClockCalibration;
416
+ if (void 0 === packetPtsUs || !calibration) return {
417
+ capturedAt: Date.now()
418
+ };
419
+ const estimatedAgeUs = this.estimateFrameAgeUs(packetPtsUs, receivedAtUs) ?? 0n;
420
+ const estimatedAgeMs = Number(estimatedAgeUs) / 1000;
421
+ const receivedAtWallTimeMs = calibration.hostWallTimeMs + Number(receivedAtUs - calibration.hostMonotonicUs) / 1000;
422
+ return {
423
+ capturedAt: receivedAtWallTimeMs - estimatedAgeMs,
424
+ estimatedAgeMs
425
+ };
426
+ }
427
+ clearFrameCache() {
428
+ this.lastRawKeyframe = null;
429
+ this.lastRawKeyframeAt = 0;
430
+ this.lastRawKeyframePtsUs = void 0;
431
+ this.lastRawKeyframeEstimatedAgeMs = void 0;
432
+ }
433
+ monotonicTimeUs() {
434
+ return process.hrtime.bigint() / 1000n;
435
+ }
436
+ resetFrameFreshnessState() {
437
+ this.frameFreshnessBarrierPtsUs = null;
438
+ this.frameFreshnessBarrierReason = null;
439
+ this.frameFreshnessBarrierPending = false;
440
+ this.frameFreshnessBarrierGeneration = 0;
441
+ this.deviceClockCalibration = null;
442
+ this.lastFramePtsUs = null;
443
+ this.frameFreshnessError = null;
444
+ this.lastFrameFreshnessWarningAt = 0;
445
+ }
264
446
  subscribeKeyframes(listener) {
265
447
  this.keyframeListeners.add(listener);
266
448
  this.resetIdleTimer();
@@ -270,10 +452,17 @@ var __webpack_modules__ = {
270
452
  };
271
453
  }
272
454
  getLatestRawKeyframe() {
455
+ const frame = this.getCachedKeyframeCandidate();
456
+ if (!frame || !this.isFrameAgeAcceptable(frame.ptsUs)) return null;
457
+ return frame;
458
+ }
459
+ getCachedKeyframeCandidate() {
273
460
  if (!this.lastRawKeyframe || !this.spsHeader) return null;
274
461
  return {
275
462
  data: this.lastRawKeyframe,
276
463
  header: this.spsHeader,
464
+ ptsUs: this.lastRawKeyframePtsUs,
465
+ estimatedAgeMs: this.lastRawKeyframeEstimatedAgeMs,
277
466
  capturedAt: this.lastRawKeyframeAt
278
467
  };
279
468
  }
@@ -283,55 +472,94 @@ var __webpack_modules__ = {
283
472
  frame.data
284
473
  ]));
285
474
  }
475
+ async waitForUsableKeyframe(timeoutMs) {
476
+ const deadline = Date.now() + timeoutMs;
477
+ let candidate = this.getCachedKeyframeCandidate();
478
+ while(true){
479
+ if (candidate && this.isFrameAgeAcceptable(candidate.ptsUs)) return candidate;
480
+ const remainingMs = deadline - Date.now();
481
+ if (remainingMs <= 0) throw new Error(`No fresh keyframe received within ${timeoutMs}ms`);
482
+ candidate = await this.waitForNextKeyframe(remainingMs);
483
+ }
484
+ }
485
+ async prepareFreshFrame() {
486
+ await this.ensureConnected();
487
+ await this.ensureFrameClockCalibration();
488
+ await this.waitForKeyframe();
489
+ await this.waitForUsableKeyframe(MAX_KEYFRAME_WAIT_MS);
490
+ }
491
+ async waitForPlanningFrame() {
492
+ let planningBarrierArmed = false;
493
+ let deadline = Date.now() + FRESH_FRAME_TIMEOUT_MS;
494
+ let candidate = this.getCachedKeyframeCandidate();
495
+ while(true){
496
+ if (candidate) {
497
+ if (void 0 === candidate.ptsUs) throw new Error('Scrcpy frame has no PTS metadata; cannot prove planning freshness');
498
+ const ageUs = this.estimateFrameAgeUs(candidate.ptsUs);
499
+ if (null === ageUs) throw new Error('Scrcpy frame clock is not calibrated; cannot prove planning freshness');
500
+ if (ageUs <= MAX_FRAME_AGE_US) return candidate;
501
+ if (planningBarrierArmed) this.clearFrameCache();
502
+ else {
503
+ debugScrcpy(`Planning candidate PTS ${candidate.ptsUs}µs has absolute age ${Number(ageUs) / 1000}ms, exceeding the ${Number(MAX_FRAME_AGE_US / 1000n)}ms limit; arming a planning freshness barrier`);
504
+ await this.setFreshnessBarrier('stale planning frame');
505
+ planningBarrierArmed = true;
506
+ deadline = Date.now() + FRESH_FRAME_TIMEOUT_MS;
507
+ }
508
+ }
509
+ const remainingMs = deadline - Date.now();
510
+ if (remainingMs <= 0) throw new Error(`No scrcpy frame crossed the active freshness target within ${FRESH_FRAME_TIMEOUT_MS}ms`);
511
+ candidate = await this.waitForNextKeyframe(remainingMs);
512
+ }
513
+ }
514
+ async closeStaleStreamAndCreateFallbackError(error) {
515
+ this.warnTransportBacklog(error);
516
+ const causeMessage = error instanceof Error ? error.message : String(error);
517
+ await this.disconnect();
518
+ return new ScrcpyFreshFrameUnavailableError(`Unable to obtain a fresh scrcpy frame; the stale stream epoch was closed so the caller can use ADB screenshot fallback. ${causeMessage}`, {
519
+ cause: error
520
+ });
521
+ }
286
522
  async getScreenshotJpeg() {
287
523
  const perfStart = Date.now();
288
524
  const t1 = Date.now();
289
525
  await this.ensureConnected();
290
526
  const connectTime = Date.now() - t1;
291
527
  const t2 = Date.now();
292
- await this.waitForKeyframe();
293
- const spsWaitTime = Date.now() - t2;
294
- const t3 = Date.now();
295
- let keyframeBuffer;
296
- let frameSource;
528
+ let frame;
297
529
  try {
298
- keyframeBuffer = await this.waitForNextKeyframe(FRESH_FRAME_TIMEOUT_MS);
299
- frameSource = 'fresh';
300
- } catch {
301
- if (this.lastRawKeyframe && this.spsHeader) {
302
- keyframeBuffer = Buffer.concat([
303
- this.spsHeader,
304
- this.lastRawKeyframe
305
- ]);
306
- frameSource = 'cached';
307
- } else {
308
- keyframeBuffer = await this.waitForNextKeyframe(MAX_KEYFRAME_WAIT_MS);
309
- frameSource = 'fresh-retry';
310
- }
530
+ await this.ensureFrameClockCalibration();
531
+ await this.waitForKeyframe();
532
+ frame = await this.waitForPlanningFrame();
533
+ } catch (error) {
534
+ throw await this.closeStaleStreamAndCreateFallbackError(error);
311
535
  }
312
- const frameWaitTime = Date.now() - t3;
536
+ const frameWaitTime = Date.now() - t2;
537
+ const keyframeBuffer = Buffer.concat([
538
+ frame.header,
539
+ frame.data
540
+ ]);
313
541
  this.resetIdleTimer();
314
- debugScrcpy(`Decoding H.264 stream: ${keyframeBuffer.length} bytes (${frameSource})`);
315
- const t4 = Date.now();
542
+ debugScrcpy(`Decoding H.264 stream: ${keyframeBuffer.length} bytes (post-barrier)`);
543
+ const t5 = Date.now();
316
544
  const result = await this.decodeH264ToJpeg(keyframeBuffer);
317
- const decodeTime = Date.now() - t4;
545
+ const decodeTime = Date.now() - t5;
318
546
  const totalTime = Date.now() - perfStart;
319
- debugScrcpy(`Performance: total=${totalTime}ms (connect=${connectTime}ms, spsWait=${spsWaitTime}ms, frameWait=${frameWaitTime}ms[${frameSource}], decode=${decodeTime}ms)`);
547
+ debugScrcpy(`Performance: total=${totalTime}ms (connect=${connectTime}ms, frameWait=${frameWaitTime}ms, decode=${decodeTime}ms)`);
320
548
  return result;
321
549
  }
322
550
  getResolution() {
323
551
  return this.videoResolution;
324
552
  }
325
- notifyKeyframeWaiters(buf) {
553
+ notifyKeyframeWaiters(frame) {
326
554
  const resolvers = this.keyframeResolvers;
327
555
  this.keyframeResolvers = [];
328
- for (const resolve of resolvers)resolve(buf);
556
+ for (const resolve of resolvers)resolve(frame);
329
557
  }
330
558
  waitForNextKeyframe(timeoutMs) {
331
559
  return new Promise((resolve, reject)=>{
332
- const wrappedResolve = (buf)=>{
560
+ const wrappedResolve = (frame)=>{
333
561
  clearTimeout(timer);
334
- resolve(buf);
562
+ resolve(frame);
335
563
  };
336
564
  const timer = setTimeout(()=>{
337
565
  this.keyframeResolvers = this.keyframeResolvers.filter((r)=>r !== wrappedResolve);
@@ -454,11 +682,11 @@ var __webpack_modules__ = {
454
682
  this.videoStream = null;
455
683
  this.streamReader = null;
456
684
  this.spsHeader = null;
457
- this.lastRawKeyframe = null;
458
- this.lastRawKeyframeAt = 0;
685
+ this.clearFrameCache();
459
686
  this.isInitialized = false;
460
687
  this.keyframeResolvers = [];
461
688
  this.keyframeListeners.clear();
689
+ this.resetFrameFreshnessState();
462
690
  if (reader) try {
463
691
  await reader.cancel();
464
692
  } catch (error) {
@@ -488,8 +716,19 @@ var __webpack_modules__ = {
488
716
  _define_property(this, "keyframeListeners", new Set());
489
717
  _define_property(this, "lastRawKeyframe", null);
490
718
  _define_property(this, "lastRawKeyframeAt", 0);
719
+ _define_property(this, "lastRawKeyframePtsUs", void 0);
720
+ _define_property(this, "lastRawKeyframeEstimatedAgeMs", void 0);
491
721
  _define_property(this, "videoResolution", null);
492
722
  _define_property(this, "streamReader", null);
723
+ _define_property(this, "frameFreshnessBarrierPtsUs", null);
724
+ _define_property(this, "frameFreshnessBarrierReason", null);
725
+ _define_property(this, "frameFreshnessBarrierPending", false);
726
+ _define_property(this, "frameFreshnessBarrierGeneration", 0);
727
+ _define_property(this, "deviceClockCalibration", null);
728
+ _define_property(this, "lastFramePtsUs", null);
729
+ _define_property(this, "frameFreshnessError", null);
730
+ _define_property(this, "lastFrameFreshnessWarningAt", 0);
731
+ _define_property(this, "lastTransportBacklogWarningAt", 0);
493
732
  this.adb = adb;
494
733
  const requestedBitRate = options.videoBitRate ?? DEFAULT_VIDEO_BIT_RATE;
495
734
  const clampedBitRate = Math.min(requestedBitRate, MAX_VIDEO_BIT_RATE);
@@ -751,6 +990,7 @@ ${stdout ? truncateAdbShellStream(stdout, 'stdout') : EMPTY_ADB_SHELL_STDOUT}`;
751
990
  class ScrcpyDeviceAdapter {
752
991
  isEnabled() {
753
992
  if (!this.isConfigured()) return false;
993
+ if (this.freshnessRecoveryPending || this.recoveryPromise) return false;
754
994
  return null === this.retryAfter || Date.now() >= this.retryAfter;
755
995
  }
756
996
  getStatus() {
@@ -762,12 +1002,18 @@ ${stdout ? truncateAdbShellStream(stdout, 'stdout') : EMPTY_ADB_SHELL_STDOUT}`;
762
1002
  };
763
1003
  }
764
1004
  isConfigured() {
765
- return this.scrcpyConfig?.enabled ?? scrcpy_manager.o.enabled;
1005
+ return this.scrcpyConfig?.enabled ?? scrcpy_manager.ov.enabled;
766
1006
  }
767
1007
  async initialize(deviceInfo) {
1008
+ if (this.recoveryPromise) {
1009
+ await this.recoveryPromise;
1010
+ if (this.manager?.isConnected()) return;
1011
+ }
1012
+ this.freshnessRecoveryPending = false;
768
1013
  try {
769
1014
  const manager = await this.ensureManager(deviceInfo);
770
1015
  await manager.ensureConnected();
1016
+ await this.applyPendingActionBarrier(manager);
771
1017
  this.clearFailure();
772
1018
  } catch (error) {
773
1019
  this.recordFailure(error);
@@ -777,24 +1023,27 @@ ${stdout ? truncateAdbShellStream(stdout, 'stdout') : EMPTY_ADB_SHELL_STDOUT}`;
777
1023
  recordFailure(error) {
778
1024
  this.lastError = error instanceof Error ? error.message : String(error);
779
1025
  this.retryAfter = Date.now() + SCRCPY_RETRY_COOLDOWN_MS;
1026
+ this.freshnessRecoveryPending = false;
780
1027
  }
781
1028
  clearFailure() {
782
1029
  this.lastError = null;
783
1030
  this.retryAfter = null;
1031
+ this.freshnessRecoveryPending = false;
784
1032
  }
785
1033
  ensureRetryReady() {
1034
+ if (this.freshnessRecoveryPending || this.recoveryPromise) throw new Error('scrcpy freshness recovery is in progress');
786
1035
  if (null === this.retryAfter || Date.now() >= this.retryAfter) return;
787
1036
  throw new Error(`scrcpy retry is cooling down until ${new Date(this.retryAfter).toISOString()}. Last error: ${this.lastError}`);
788
1037
  }
789
1038
  resolveConfig(deviceInfo) {
790
1039
  if (this.resolvedConfig) return this.resolvedConfig;
791
1040
  const config = this.scrcpyConfig;
792
- const maxSize = config?.maxSize ?? scrcpy_manager.o.maxSize;
793
- const videoBitRate = config?.videoBitRate ?? scrcpy_manager.o.videoBitRate;
1041
+ const maxSize = config?.maxSize ?? scrcpy_manager.ov.maxSize;
1042
+ const videoBitRate = config?.videoBitRate ?? scrcpy_manager.ov.videoBitRate;
794
1043
  this.resolvedConfig = {
795
1044
  enabled: this.isConfigured(),
796
1045
  maxSize,
797
- idleTimeoutMs: config?.idleTimeoutMs ?? scrcpy_manager.o.idleTimeoutMs,
1046
+ idleTimeoutMs: config?.idleTimeoutMs ?? scrcpy_manager.ov.idleTimeoutMs,
798
1047
  videoBitRate
799
1048
  };
800
1049
  return this.resolvedConfig;
@@ -828,24 +1077,40 @@ ${stdout ? truncateAdbShellStream(stdout, 'stdout') : EMPTY_ADB_SHELL_STDOUT}`;
828
1077
  }
829
1078
  async screenshotBase64(deviceInfo) {
830
1079
  this.ensureRetryReady();
1080
+ let manager = null;
831
1081
  try {
832
- const manager = await this.ensureManager(deviceInfo);
1082
+ manager = await this.ensureManager(deviceInfo);
1083
+ await manager.ensureConnected();
1084
+ await this.applyPendingActionBarrier(manager);
833
1085
  const screenshotBuffer = await manager.getScreenshotJpeg();
834
1086
  this.clearFailure();
835
1087
  return (0, img_namespaceObject.createImgBase64ByFormat)('jpeg', screenshotBuffer.toString('base64'));
836
1088
  } catch (error) {
1089
+ if ((0, scrcpy_manager.GJ)(error)) {
1090
+ this.markFreshnessRecoveryPending(manager, error);
1091
+ throw error;
1092
+ }
837
1093
  this.recordFailure(error);
838
1094
  throw error;
839
1095
  }
840
1096
  }
841
1097
  async subscribeKeyframes(deviceInfo, listener) {
842
1098
  this.ensureRetryReady();
1099
+ this.keyframeListeners.add(listener);
843
1100
  try {
844
1101
  const manager = await this.ensureManager(deviceInfo);
845
1102
  await manager.ensureConnected();
1103
+ await this.applyPendingActionBarrier(manager);
1104
+ await manager.ensureFrameClockCalibration();
846
1105
  this.clearFailure();
847
- return manager.subscribeKeyframes(listener);
1106
+ this.attachKeyframeListener(manager, listener);
1107
+ return ()=>{
1108
+ this.keyframeListeners.delete(listener);
1109
+ this.keyframeUnsubscribers.get(listener)?.();
1110
+ this.keyframeUnsubscribers.delete(listener);
1111
+ };
848
1112
  } catch (error) {
1113
+ this.keyframeListeners.delete(listener);
849
1114
  this.recordFailure(error);
850
1115
  throw error;
851
1116
  }
@@ -853,6 +1118,83 @@ ${stdout ? truncateAdbShellStream(stdout, 'stdout') : EMPTY_ADB_SHELL_STDOUT}`;
853
1118
  getLatestRawKeyframe() {
854
1119
  return this.manager?.getLatestRawKeyframe() ?? null;
855
1120
  }
1121
+ attachKeyframeListener(manager, listener) {
1122
+ this.keyframeUnsubscribers.get(listener)?.();
1123
+ this.keyframeUnsubscribers.set(listener, manager.subscribeKeyframes(listener));
1124
+ }
1125
+ attachKeyframeListeners(manager) {
1126
+ this.keyframeUnsubscribers.clear();
1127
+ for (const listener of this.keyframeListeners)this.attachKeyframeListener(manager, listener);
1128
+ }
1129
+ markFreshnessRecoveryPending(manager, error) {
1130
+ this.lastError = error.message;
1131
+ this.retryAfter = null;
1132
+ this.freshnessRecoveryPending = true;
1133
+ this.keyframeUnsubscribers.clear();
1134
+ if (manager && this.manager === manager) this.manager = null;
1135
+ }
1136
+ async applyPendingActionBarrier(manager) {
1137
+ if (!this.pendingActionBarrier) return;
1138
+ await manager.setFreshnessBarrier('completed input action while scrcpy was unavailable');
1139
+ this.pendingActionBarrier = false;
1140
+ }
1141
+ recoverAfterAdbScreenshot(deviceInfo) {
1142
+ if (!this.freshnessRecoveryPending || this.recoveryPromise) return;
1143
+ const generation = this.lifecycleGeneration;
1144
+ const recovery = (async ()=>{
1145
+ let manager = null;
1146
+ try {
1147
+ manager = await this.ensureManager(deviceInfo);
1148
+ await manager.ensureConnected();
1149
+ await this.applyPendingActionBarrier(manager);
1150
+ await manager.prepareFreshFrame();
1151
+ if (generation !== this.lifecycleGeneration) {
1152
+ await manager.disconnect();
1153
+ if (this.manager === manager) this.manager = null;
1154
+ return;
1155
+ }
1156
+ this.attachKeyframeListeners(manager);
1157
+ this.clearFailure();
1158
+ debugAdapter('Scrcpy freshness recovery completed in background');
1159
+ } catch (error) {
1160
+ if (manager) {
1161
+ await manager.disconnect();
1162
+ if (this.manager === manager) this.manager = null;
1163
+ }
1164
+ this.freshnessRecoveryPending = false;
1165
+ this.recordFailure(error);
1166
+ debugAdapter(`Scrcpy background freshness recovery failed: ${error}`);
1167
+ throw error;
1168
+ }
1169
+ })();
1170
+ this.recoveryPromise = recovery;
1171
+ recovery.catch(()=>{}).finally(()=>{
1172
+ if (this.recoveryPromise === recovery) this.recoveryPromise = null;
1173
+ });
1174
+ }
1175
+ async markActionBarrier() {
1176
+ const manager = this.manager;
1177
+ if (!manager?.isConnected()) {
1178
+ this.pendingActionBarrier = true;
1179
+ return;
1180
+ }
1181
+ try {
1182
+ await manager.setFreshnessBarrier('completed input action');
1183
+ this.pendingActionBarrier = false;
1184
+ this.clearFailure();
1185
+ } catch (error) {
1186
+ this.pendingActionBarrier = true;
1187
+ this.recordFailure(error);
1188
+ debugAdapter(`Unable to mark scrcpy action barrier; disabling this stream: ${error}`);
1189
+ try {
1190
+ await manager.disconnect();
1191
+ } catch (disconnectError) {
1192
+ debugAdapter(`Error disconnecting scrcpy after barrier failure: ${disconnectError}`);
1193
+ } finally{
1194
+ if (this.manager === manager) this.manager = null;
1195
+ }
1196
+ }
1197
+ }
856
1198
  async decodeRawKeyframeToJpegBase64(frame) {
857
1199
  if (!this.manager) throw new Error('scrcpy manager is not initialized');
858
1200
  const jpegBuffer = await this.manager.decodeRawKeyframeToJpeg(frame);
@@ -876,6 +1218,13 @@ ${stdout ? truncateAdbShellStream(stdout, 'stdout') : EMPTY_ADB_SHELL_STDOUT}`;
876
1218
  return resolution.width / physicalWidth;
877
1219
  }
878
1220
  async disconnect() {
1221
+ this.lifecycleGeneration += 1;
1222
+ this.freshnessRecoveryPending = false;
1223
+ this.pendingActionBarrier = false;
1224
+ for (const unsubscribe of this.keyframeUnsubscribers.values())unsubscribe();
1225
+ this.keyframeUnsubscribers.clear();
1226
+ this.keyframeListeners.clear();
1227
+ if (this.recoveryPromise) await this.recoveryPromise.catch(()=>{});
879
1228
  if (this.manager) {
880
1229
  try {
881
1230
  await this.manager.disconnect();
@@ -895,6 +1244,12 @@ ${stdout ? truncateAdbShellStream(stdout, 'stdout') : EMPTY_ADB_SHELL_STDOUT}`;
895
1244
  _define_property(this, "resolvedConfig", void 0);
896
1245
  _define_property(this, "lastError", void 0);
897
1246
  _define_property(this, "retryAfter", void 0);
1247
+ _define_property(this, "freshnessRecoveryPending", void 0);
1248
+ _define_property(this, "recoveryPromise", void 0);
1249
+ _define_property(this, "lifecycleGeneration", void 0);
1250
+ _define_property(this, "pendingActionBarrier", void 0);
1251
+ _define_property(this, "keyframeListeners", void 0);
1252
+ _define_property(this, "keyframeUnsubscribers", void 0);
898
1253
  this.deviceId = deviceId;
899
1254
  this.scrcpyConfig = scrcpyConfig;
900
1255
  this.resolveAdbServerEndpoint = resolveAdbServerEndpoint;
@@ -902,6 +1257,12 @@ ${stdout ? truncateAdbShellStream(stdout, 'stdout') : EMPTY_ADB_SHELL_STDOUT}`;
902
1257
  this.resolvedConfig = null;
903
1258
  this.lastError = null;
904
1259
  this.retryAfter = null;
1260
+ this.freshnessRecoveryPending = false;
1261
+ this.recoveryPromise = null;
1262
+ this.lifecycleGeneration = 0;
1263
+ this.pendingActionBarrier = false;
1264
+ this.keyframeListeners = new Set();
1265
+ this.keyframeUnsubscribers = new Map();
905
1266
  }
906
1267
  }
907
1268
  const TAG_RE = /<(\/?)([A-Za-z_][A-Za-z0-9_.\-:]*)([^>]*?)(\/?)>/g;
@@ -1128,6 +1489,20 @@ ${stdout ? truncateAdbShellStream(stdout, 'stdout') : EMPTY_ADB_SHELL_STDOUT}`;
1128
1489
  }
1129
1490
  throw new Error(`Unable to capture Android UI tree after ${failures.length} attempt(s): ${failures.join('; ')}`);
1130
1491
  }
1492
+ function createVisualActionRegistry(definitions, onActionSettled) {
1493
+ const registeredActions = {};
1494
+ for (const actionName of Object.keys(definitions)){
1495
+ const dispatch = definitions[actionName];
1496
+ registeredActions[actionName] = async (...args)=>{
1497
+ try {
1498
+ return await dispatch(...args);
1499
+ } finally{
1500
+ await onActionSettled(actionName);
1501
+ }
1502
+ };
1503
+ }
1504
+ return registeredActions;
1505
+ }
1131
1506
  function device_define_property(obj, key, value) {
1132
1507
  if (key in obj) Object.defineProperty(obj, key, {
1133
1508
  value: value,
@@ -1228,19 +1603,12 @@ ${stdout ? truncateAdbShellStream(stdout, 'stdout') : EMPTY_ADB_SHELL_STDOUT}`;
1228
1603
  }
1229
1604
  },
1230
1605
  call: async (param)=>{
1231
- const element = param.locate;
1232
- const startPoint = element ? {
1233
- left: element.center[0],
1234
- top: element.center[1]
1235
- } : void 0;
1236
1606
  if (!param || !param.direction) throw new Error('PullGesture requires a direction parameter');
1237
- if ('down' === param.direction) await this.pullDown(startPoint, param.distance, param.duration);
1238
- else if ('up' === param.direction) await this.pullUp(startPoint, param.distance, param.duration);
1239
- else throw new Error(`Unknown pull direction: ${param.direction}`);
1607
+ await this.visualActions.pullGesture(param);
1240
1608
  }
1241
1609
  })
1242
1610
  ];
1243
- const platformActions = createPlatformActions(this);
1611
+ const platformActions = createPlatformActions(this.visualActions);
1244
1612
  let platformSpecificActions = Object.values(platformActions);
1245
1613
  if (this.options?.exposeRunAdbShellAction === false) platformSpecificActions = platformSpecificActions.filter((action)=>action !== platformActions.RunAdbShell);
1246
1614
  const customActions = this.customActions || [];
@@ -1250,6 +1618,16 @@ ${stdout ? truncateAdbShellStream(stdout, 'stdout') : EMPTY_ADB_SHELL_STDOUT}`;
1250
1618
  ...customActions
1251
1619
  ];
1252
1620
  }
1621
+ async performPullGesture(param) {
1622
+ const element = param.locate;
1623
+ const startPoint = element ? {
1624
+ left: element.center[0],
1625
+ top: element.center[1]
1626
+ } : void 0;
1627
+ if ('down' === param.direction) await this.pullDownRaw(startPoint, param.distance, param.duration);
1628
+ else if ('up' === param.direction) await this.pullUpRaw(startPoint, param.distance, param.duration);
1629
+ else throw new Error(`Unknown pull direction: ${param.direction}`);
1630
+ }
1253
1631
  async performActionScroll(param) {
1254
1632
  const element = param.locate;
1255
1633
  const startingPoint = element ? {
@@ -1257,20 +1635,32 @@ ${stdout ? truncateAdbShellStream(stdout, 'stdout') : EMPTY_ADB_SHELL_STDOUT}`;
1257
1635
  top: element.center[1]
1258
1636
  } : void 0;
1259
1637
  const scrollToEventName = param?.scrollType;
1260
- if ('scrollToTop' === scrollToEventName) await this.scrollUntilTop(startingPoint);
1261
- else if ('scrollToBottom' === scrollToEventName) await this.scrollUntilBottom(startingPoint);
1262
- else if ('scrollToRight' === scrollToEventName) await this.scrollUntilRight(startingPoint);
1263
- else if ('scrollToLeft' === scrollToEventName) await this.scrollUntilLeft(startingPoint);
1638
+ if ('scrollToTop' === scrollToEventName) await this.scrollUntilTopRaw(startingPoint);
1639
+ else if ('scrollToBottom' === scrollToEventName) await this.scrollUntilBottomRaw(startingPoint);
1640
+ else if ('scrollToRight' === scrollToEventName) await this.scrollUntilRightRaw(startingPoint);
1641
+ else if ('scrollToLeft' === scrollToEventName) await this.scrollUntilLeftRaw(startingPoint);
1264
1642
  else if ('singleAction' !== scrollToEventName && scrollToEventName) throw new Error(`Unknown scroll event type: ${scrollToEventName}, param: ${JSON.stringify(param)}`);
1265
1643
  else {
1266
- if (param?.direction !== 'down' && param && param.direction) if ('up' === param.direction) await this.scrollUp(param.distance || void 0, startingPoint);
1267
- else if ('left' === param.direction) await this.scrollLeft(param.distance || void 0, startingPoint);
1268
- else if ('right' === param.direction) await this.scrollRight(param.distance || void 0, startingPoint);
1644
+ if (param?.direction !== 'down' && param && param.direction) if ('up' === param.direction) await this.scrollUpRaw(param.distance || void 0, startingPoint);
1645
+ else if ('left' === param.direction) await this.scrollLeftRaw(param.distance || void 0, startingPoint);
1646
+ else if ('right' === param.direction) await this.scrollRightRaw(param.distance || void 0, startingPoint);
1269
1647
  else throw new Error(`Unknown scroll direction: ${param.direction}`);
1270
- else await this.scrollDown(param?.distance || void 0, startingPoint);
1648
+ else await this.scrollDownRaw(param?.distance || void 0, startingPoint);
1271
1649
  await (0, core_utils_namespaceObject.sleep)(500);
1272
1650
  }
1273
1651
  }
1652
+ async runAdbShellRaw(param, context) {
1653
+ const adb = await this.getAdb();
1654
+ const stdout = await runAdbShellStdoutOrThrow(adb, param.command, void 0 === param.timeout ? void 0 : {
1655
+ timeout: param.timeout
1656
+ });
1657
+ const planningFeedback = buildRunAdbShellPlanningFeedback({
1658
+ command: param.command,
1659
+ stdout
1660
+ });
1661
+ if (planningFeedback && context?.task) context.task.planningFeedback = planningFeedback;
1662
+ return stdout;
1663
+ }
1274
1664
  describe() {
1275
1665
  return this.description || `DeviceId: ${this.deviceId}`;
1276
1666
  }
@@ -1367,15 +1757,15 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1367
1757
  const adapter = this.getScrcpyAdapter();
1368
1758
  if (!adapter.isEnabled()) throw new Error('scrcpy is not available for frame observation');
1369
1759
  const deviceInfo = await this.getDevicePhysicalInfo();
1370
- let latest = adapter.getLatestRawKeyframe();
1371
- const unsubscribe = await adapter.subscribeKeyframes(deviceInfo, (frame)=>{
1372
- latest = frame;
1373
- });
1760
+ const unsubscribe = await adapter.subscribeKeyframes(deviceInfo, ()=>{});
1374
1761
  return {
1375
- latest: ()=>latest ? {
1762
+ latest: ()=>{
1763
+ const latest = adapter.getLatestRawKeyframe();
1764
+ return latest ? {
1376
1765
  ref: latest,
1377
1766
  capturedAt: latest.capturedAt
1378
- } : null,
1767
+ } : null;
1768
+ },
1379
1769
  decode: async (refs)=>{
1380
1770
  const images = [];
1381
1771
  for (const frameRef of refs)images.push(await adapter.decodeRawKeyframeToJpegBase64(frameRef.ref));
@@ -1409,6 +1799,9 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1409
1799
  return this.appNameMapping[normalizedAppName];
1410
1800
  }
1411
1801
  async launch(uri) {
1802
+ return this.visualActions.launch(uri);
1803
+ }
1804
+ async launchRaw(uri) {
1412
1805
  const adb = await this.getAdb();
1413
1806
  this.uri = uri;
1414
1807
  try {
@@ -1434,6 +1827,9 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1434
1827
  return this;
1435
1828
  }
1436
1829
  async terminate(uri) {
1830
+ await this.visualActions.terminate(uri);
1831
+ }
1832
+ async terminateRaw(uri) {
1437
1833
  const packagePart = uri.includes('/') ? uri.split('/')[0] : uri;
1438
1834
  const resolved = this.resolvePackageName(packagePart) ?? packagePart;
1439
1835
  const adb = await this.getAdb();
@@ -1449,6 +1845,9 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1449
1845
  }
1450
1846
  }
1451
1847
  async execYadb(keyboardContent) {
1848
+ await this.visualActions.execYadb(keyboardContent);
1849
+ }
1850
+ async execYadbRaw(keyboardContent) {
1452
1851
  this.warnYadbOnNonDefaultDisplay('keyboard input');
1453
1852
  await this.ensureYadb();
1454
1853
  const adb = await this.getAdb();
@@ -1705,14 +2104,15 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1705
2104
  async screenshotBase64() {
1706
2105
  debugDevice('screenshotBase64 begin');
1707
2106
  const adapter = this.getScrcpyAdapter();
2107
+ let scrcpyDeviceInfo = null;
1708
2108
  if (adapter.isEnabled()) try {
1709
2109
  debugDevice('Attempting scrcpy screenshot...');
1710
- const deviceInfo = await this.getDevicePhysicalInfo();
1711
- const result = await adapter.screenshotBase64(deviceInfo);
2110
+ scrcpyDeviceInfo = await this.getDevicePhysicalInfo();
2111
+ const result = await adapter.screenshotBase64(scrcpyDeviceInfo);
1712
2112
  debugDevice('screenshotBase64 end (scrcpy mode)');
1713
2113
  return result;
1714
2114
  } catch (error) {
1715
- warnDevice(`Scrcpy screenshot failed, falling back to standard ADB method.\nError: ${error}`);
2115
+ warnDevice(`Scrcpy screenshot failed, falling back to standard ADB method. This may be caused by transport backlog. ${scrcpy_manager.Xr}\nError: ${error}`);
1716
2116
  }
1717
2117
  const adb = await this.getAdb();
1718
2118
  let screenshotBuffer;
@@ -1790,9 +2190,13 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1790
2190
  });
1791
2191
  }
1792
2192
  debugDevice('screenshotBase64 end');
2193
+ if (scrcpyDeviceInfo) adapter.recoverAfterAdbScreenshot(scrcpyDeviceInfo);
1793
2194
  return result;
1794
2195
  }
1795
2196
  async clearInput(element) {
2197
+ await this.visualActions.clearInput(element);
2198
+ }
2199
+ async clearInputRaw(element) {
1796
2200
  if (element) await this.tapPoint({
1797
2201
  x: element.center[0],
1798
2202
  y: element.center[1]
@@ -1826,6 +2230,9 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1826
2230
  return '';
1827
2231
  }
1828
2232
  async scrollUntilTop(startPoint) {
2233
+ await this.visualActions.scrollUntilTop(startPoint);
2234
+ }
2235
+ async scrollUntilTopRaw(startPoint) {
1829
2236
  if (startPoint) {
1830
2237
  const { height } = await this.size();
1831
2238
  const start = {
@@ -1840,10 +2247,13 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1840
2247
  await (0, core_utils_namespaceObject.sleep)(1000);
1841
2248
  return;
1842
2249
  }
1843
- await (0, utils_namespaceObject.repeat)(defaultScrollUntilTimes, ()=>this.scroll(0, -9999999, defaultFastScrollDuration));
2250
+ await (0, utils_namespaceObject.repeat)(defaultScrollUntilTimes, ()=>this.scrollRaw(0, -9999999, defaultFastScrollDuration));
1844
2251
  await (0, core_utils_namespaceObject.sleep)(1000);
1845
2252
  }
1846
2253
  async scrollUntilBottom(startPoint) {
2254
+ await this.visualActions.scrollUntilBottom(startPoint);
2255
+ }
2256
+ async scrollUntilBottomRaw(startPoint) {
1847
2257
  if (startPoint) {
1848
2258
  const start = {
1849
2259
  x: Math.round(startPoint.left),
@@ -1857,10 +2267,13 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1857
2267
  await (0, core_utils_namespaceObject.sleep)(1000);
1858
2268
  return;
1859
2269
  }
1860
- await (0, utils_namespaceObject.repeat)(defaultScrollUntilTimes, ()=>this.scroll(0, 9999999, defaultFastScrollDuration));
2270
+ await (0, utils_namespaceObject.repeat)(defaultScrollUntilTimes, ()=>this.scrollRaw(0, 9999999, defaultFastScrollDuration));
1861
2271
  await (0, core_utils_namespaceObject.sleep)(1000);
1862
2272
  }
1863
2273
  async scrollUntilLeft(startPoint) {
2274
+ await this.visualActions.scrollUntilLeft(startPoint);
2275
+ }
2276
+ async scrollUntilLeftRaw(startPoint) {
1864
2277
  if (startPoint) {
1865
2278
  const { width } = await this.size();
1866
2279
  const start = {
@@ -1875,10 +2288,13 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1875
2288
  await (0, core_utils_namespaceObject.sleep)(1000);
1876
2289
  return;
1877
2290
  }
1878
- await (0, utils_namespaceObject.repeat)(defaultScrollUntilTimes, ()=>this.scroll(-9999999, 0, defaultFastScrollDuration));
2291
+ await (0, utils_namespaceObject.repeat)(defaultScrollUntilTimes, ()=>this.scrollRaw(-9999999, 0, defaultFastScrollDuration));
1879
2292
  await (0, core_utils_namespaceObject.sleep)(1000);
1880
2293
  }
1881
2294
  async scrollUntilRight(startPoint) {
2295
+ await this.visualActions.scrollUntilRight(startPoint);
2296
+ }
2297
+ async scrollUntilRightRaw(startPoint) {
1882
2298
  if (startPoint) {
1883
2299
  const start = {
1884
2300
  x: Math.round(startPoint.left),
@@ -1892,10 +2308,13 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1892
2308
  await (0, core_utils_namespaceObject.sleep)(1000);
1893
2309
  return;
1894
2310
  }
1895
- await (0, utils_namespaceObject.repeat)(defaultScrollUntilTimes, ()=>this.scroll(9999999, 0, defaultFastScrollDuration));
2311
+ await (0, utils_namespaceObject.repeat)(defaultScrollUntilTimes, ()=>this.scrollRaw(9999999, 0, defaultFastScrollDuration));
1896
2312
  await (0, core_utils_namespaceObject.sleep)(1000);
1897
2313
  }
1898
2314
  async scrollUp(distance, startPoint) {
2315
+ await this.visualActions.scrollUp(distance, startPoint);
2316
+ }
2317
+ async scrollUpRaw(distance, startPoint) {
1899
2318
  const { height } = await this.size();
1900
2319
  const scrollDistance = Math.round(distance || height);
1901
2320
  const hasExplicitDistance = void 0 !== distance;
@@ -1909,9 +2328,12 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1909
2328
  await this.dragPoint(start, end);
1910
2329
  return;
1911
2330
  }
1912
- await this.scroll(0, -scrollDistance, void 0, hasExplicitDistance, 'up');
2331
+ await this.scrollRaw(0, -scrollDistance, void 0, hasExplicitDistance, 'up');
1913
2332
  }
1914
2333
  async scrollDown(distance, startPoint) {
2334
+ await this.visualActions.scrollDown(distance, startPoint);
2335
+ }
2336
+ async scrollDownRaw(distance, startPoint) {
1915
2337
  const { height } = await this.size();
1916
2338
  const scrollDistance = Math.round(distance || height);
1917
2339
  const hasExplicitDistance = void 0 !== distance;
@@ -1925,9 +2347,12 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1925
2347
  await this.dragPoint(start, end);
1926
2348
  return;
1927
2349
  }
1928
- await this.scroll(0, scrollDistance, void 0, hasExplicitDistance, 'down');
2350
+ await this.scrollRaw(0, scrollDistance, void 0, hasExplicitDistance, 'down');
1929
2351
  }
1930
2352
  async scrollLeft(distance, startPoint) {
2353
+ await this.visualActions.scrollLeft(distance, startPoint);
2354
+ }
2355
+ async scrollLeftRaw(distance, startPoint) {
1931
2356
  const { width } = await this.size();
1932
2357
  const scrollDistance = Math.round(distance || width);
1933
2358
  const hasExplicitDistance = void 0 !== distance;
@@ -1941,9 +2366,12 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1941
2366
  await this.dragPoint(start, end);
1942
2367
  return;
1943
2368
  }
1944
- await this.scroll(-scrollDistance, 0, void 0, hasExplicitDistance, 'left');
2369
+ await this.scrollRaw(-scrollDistance, 0, void 0, hasExplicitDistance, 'left');
1945
2370
  }
1946
2371
  async scrollRight(distance, startPoint) {
2372
+ await this.visualActions.scrollRight(distance, startPoint);
2373
+ }
2374
+ async scrollRightRaw(distance, startPoint) {
1947
2375
  const { width } = await this.size();
1948
2376
  const scrollDistance = Math.round(distance || width);
1949
2377
  const hasExplicitDistance = void 0 !== distance;
@@ -1957,7 +2385,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1957
2385
  await this.dragPoint(start, end);
1958
2386
  return;
1959
2387
  }
1960
- await this.scroll(scrollDistance, 0, void 0, hasExplicitDistance, 'right');
2388
+ await this.scrollRaw(scrollDistance, 0, void 0, hasExplicitDistance, 'right');
1961
2389
  }
1962
2390
  async ensureYadb() {
1963
2391
  if (!this.yadbPushed) {
@@ -1986,11 +2414,11 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1986
2414
  const reason = IME_STRATEGY === IME_STRATEGY_ALWAYS_YADB ? "imeStrategy 'always-yadb' requires yadb" : 'text contains characters that require yadb (non-ASCII, format specifiers, or mixed quotes)';
1987
2415
  throw new Error(`${reason}, but yadb (app_process) cannot target non-default displayId=${this.options?.displayId}. Use displayId=0 or a different imeStrategy.`);
1988
2416
  }
1989
- if (needsYadb) await this.execYadb(escapeForShell(text));
2417
+ if (needsYadb) await this.execYadbRaw(escapeForShell(text));
1990
2418
  else await this.shellInputText(text, {
1991
2419
  keyboardTypeDelay: typeDelay
1992
2420
  });
1993
- if (true === shouldAutoDismissKeyboard) await this.hideKeyboard(options);
2421
+ if (true === shouldAutoDismissKeyboard) await this.hideKeyboardRaw(options);
1994
2422
  }
1995
2423
  normalizeKeyName(key) {
1996
2424
  const keyMap = {
@@ -2060,6 +2488,9 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
2060
2488
  await adb.shell(`input${this.getDisplayArg()} swipe ${fromX} ${fromY} ${toX} ${toY} ${duration}`);
2061
2489
  }
2062
2490
  async scroll(deltaX, deltaY, duration, warnOnClamp = false, direction) {
2491
+ await this.visualActions.scroll(deltaX, deltaY, duration, warnOnClamp, direction);
2492
+ }
2493
+ async scrollRaw(deltaX, deltaY, duration, warnOnClamp = false, direction) {
2063
2494
  if (0 === deltaX && 0 === deltaY) throw new Error('Scroll distance cannot be zero in both directions');
2064
2495
  const { width, height } = await this.size();
2065
2496
  const n = 4;
@@ -2124,12 +2555,21 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
2124
2555
  }
2125
2556
  }
2126
2557
  async back() {
2558
+ await this.visualActions.back();
2559
+ }
2560
+ async backRaw() {
2127
2561
  await this.shellInputKeyevent(4);
2128
2562
  }
2129
2563
  async home() {
2564
+ await this.visualActions.home();
2565
+ }
2566
+ async homeRaw() {
2130
2567
  await this.shellInputKeyevent(3);
2131
2568
  }
2132
2569
  async recentApps() {
2570
+ await this.visualActions.recentApps();
2571
+ }
2572
+ async recentAppsRaw() {
2133
2573
  await this.shellInputKeyevent(187);
2134
2574
  }
2135
2575
  async longPressPoint(point, duration = 2000) {
@@ -2138,6 +2578,9 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
2138
2578
  await adb.shell(`input${this.getDisplayArg()} swipe ${adjustedX} ${adjustedY} ${adjustedX} ${adjustedY} ${duration}`);
2139
2579
  }
2140
2580
  async pullDown(startPoint, distance, duration = 800) {
2581
+ await this.visualActions.pullDown(startPoint, distance, duration);
2582
+ }
2583
+ async pullDownRaw(startPoint, distance, duration = 800) {
2141
2584
  const { width, height } = await this.size();
2142
2585
  const start = startPoint ? {
2143
2586
  x: Math.round(startPoint.left),
@@ -2151,13 +2594,19 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
2151
2594
  x: start.x,
2152
2595
  y: start.y + pullDistance
2153
2596
  };
2154
- await this.pullDrag(start, end, duration);
2597
+ await this.pullDragRaw(start, end, duration);
2155
2598
  await (0, core_utils_namespaceObject.sleep)(200);
2156
2599
  }
2157
2600
  async pullDrag(from, to, duration) {
2601
+ await this.visualActions.pullDrag(from, to, duration);
2602
+ }
2603
+ async pullDragRaw(from, to, duration) {
2158
2604
  await this.swipePoint(from, to, duration);
2159
2605
  }
2160
2606
  async pullUp(startPoint, distance, duration = 600) {
2607
+ await this.visualActions.pullUp(startPoint, distance, duration);
2608
+ }
2609
+ async pullUpRaw(startPoint, distance, duration = 600) {
2161
2610
  const { width, height } = await this.size();
2162
2611
  const start = startPoint ? {
2163
2612
  x: Math.round(startPoint.left),
@@ -2171,7 +2620,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
2171
2620
  x: start.x,
2172
2621
  y: start.y - pullDistance
2173
2622
  };
2174
- await this.pullDrag(start, end, duration);
2623
+ await this.pullDragRaw(start, end, duration);
2175
2624
  await (0, core_utils_namespaceObject.sleep)(100);
2176
2625
  }
2177
2626
  getDisplayArg() {
@@ -2234,6 +2683,9 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
2234
2683
  }
2235
2684
  }
2236
2685
  async hideKeyboard(options, timeoutMs = 1000) {
2686
+ return this.visualActions.hideKeyboard(options, timeoutMs);
2687
+ }
2688
+ async hideKeyboardRaw(options, timeoutMs = 1000) {
2237
2689
  const adb = await this.getAdb();
2238
2690
  const keyboardDismissStrategy = options?.keyboardDismissStrategy ?? this.options?.keyboardDismissStrategy ?? 'esc-first';
2239
2691
  const keyboardStatus = await adb.isSoftKeyboardPresent();
@@ -2288,55 +2740,91 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
2288
2740
  device_define_property(this, "interfaceType", 'android');
2289
2741
  device_define_property(this, "uri", void 0);
2290
2742
  device_define_property(this, "options", void 0);
2743
+ device_define_property(this, "visualActions", createVisualActionRegistry({
2744
+ tap: (point)=>this.tapPoint(point),
2745
+ doubleClick: (point)=>this.doubleTapPoint(point),
2746
+ longPress: (point, opts)=>this.longPressPoint(point, opts?.duration),
2747
+ dragAndDrop: (from, to)=>this.dragPoint(from, to),
2748
+ keyboardPress: (keyName)=>this.pressKey(keyName),
2749
+ typeText: async (value, opts)=>{
2750
+ const target = opts?.target;
2751
+ if (target && opts?.replace !== false) await this.clearInputRaw(target);
2752
+ else if (target) await this.tapPoint({
2753
+ x: target.center[0],
2754
+ y: target.center[1]
2755
+ });
2756
+ if (opts?.focusOnly) return;
2757
+ await this.typeText(value, opts);
2758
+ },
2759
+ clearInput: (target)=>this.clearInputRaw(target),
2760
+ cursorMove: async (direction, times = 1)=>{
2761
+ const arrowKey = 'left' === direction ? 'ArrowLeft' : 'ArrowRight';
2762
+ for(let index = 0; index < times; index++)await this.pressKey(arrowKey);
2763
+ },
2764
+ swipe: async (start, end, opts)=>{
2765
+ const duration = opts?.duration ?? 300;
2766
+ const repeatCount = opts?.repeat ?? 1;
2767
+ for(let index = 0; index < repeatCount; index++)await this.dragPoint(start, end, duration);
2768
+ },
2769
+ pinch: async (center, opts)=>{
2770
+ if ('number' == typeof this.options?.displayId && 0 !== this.options.displayId) throw new Error(`Pinch is not supported on a non-default display (displayId=${this.options.displayId}). The underlying yadb tool only injects gestures into the default display.`);
2771
+ const { x: adjCenterX, y: adjCenterY } = await this.adjustCoordinates(Math.round(center.x), Math.round(center.y));
2772
+ const ratio = 0 !== adjCenterX && 0 !== center.x ? adjCenterX / center.x : 1;
2773
+ const adjStartDist = Math.round(opts.startDistance * ratio);
2774
+ const adjEndDist = Math.round(opts.endDistance * ratio);
2775
+ await this.ensureYadb();
2776
+ const adb = await this.getAdb();
2777
+ await adb.shell(`app_process -Djava.class.path=/data/local/tmp/yadb /data/local/tmp com.ysbing.yadb.Main -pinch ${adjCenterX} ${adjCenterY} ${adjStartDist} ${adjEndDist} ${opts.duration}`);
2778
+ },
2779
+ actionScroll: (param)=>this.performActionScroll(param),
2780
+ back: ()=>this.backRaw(),
2781
+ home: ()=>this.homeRaw(),
2782
+ recentApps: ()=>this.recentAppsRaw(),
2783
+ pullGesture: (param)=>this.performPullGesture(param),
2784
+ launch: (uri)=>this.launchRaw(uri),
2785
+ terminate: (uri)=>this.terminateRaw(uri),
2786
+ runAdbShell: (param, context)=>this.runAdbShellRaw(param, context),
2787
+ execYadb: (keyboardContent)=>this.execYadbRaw(keyboardContent),
2788
+ scrollUntilTop: (startPoint)=>this.scrollUntilTopRaw(startPoint),
2789
+ scrollUntilBottom: (startPoint)=>this.scrollUntilBottomRaw(startPoint),
2790
+ scrollUntilLeft: (startPoint)=>this.scrollUntilLeftRaw(startPoint),
2791
+ scrollUntilRight: (startPoint)=>this.scrollUntilRightRaw(startPoint),
2792
+ scrollUp: (distance, startPoint)=>this.scrollUpRaw(distance, startPoint),
2793
+ scrollDown: (distance, startPoint)=>this.scrollDownRaw(distance, startPoint),
2794
+ scrollLeft: (distance, startPoint)=>this.scrollLeftRaw(distance, startPoint),
2795
+ scrollRight: (distance, startPoint)=>this.scrollRightRaw(distance, startPoint),
2796
+ scroll: (deltaX, deltaY, duration, warnOnClamp, direction)=>this.scrollRaw(deltaX, deltaY, duration, warnOnClamp, direction),
2797
+ pullDown: (startPoint, distance, duration)=>this.pullDownRaw(startPoint, distance, duration),
2798
+ pullDrag: (from, to, duration)=>this.pullDragRaw(from, to, duration),
2799
+ pullUp: (startPoint, distance, duration)=>this.pullUpRaw(startPoint, distance, duration),
2800
+ hideKeyboard: (options, timeoutMs)=>this.hideKeyboardRaw(options, timeoutMs)
2801
+ }, async ()=>{
2802
+ await this.scrcpyAdapter?.markActionBarrier();
2803
+ }));
2291
2804
  device_define_property(this, "inputPrimitives", {
2292
2805
  pointer: {
2293
- tap: (point)=>this.tapPoint(point),
2294
- doubleClick: (point)=>this.doubleTapPoint(point),
2295
- longPress: (point, opts)=>this.longPressPoint(point, opts?.duration),
2296
- dragAndDrop: (from, to)=>this.dragPoint(from, to)
2806
+ tap: this.visualActions.tap,
2807
+ doubleClick: this.visualActions.doubleClick,
2808
+ longPress: this.visualActions.longPress,
2809
+ dragAndDrop: this.visualActions.dragAndDrop
2297
2810
  },
2298
2811
  keyboard: {
2299
- keyboardPress: (keyName)=>this.pressKey(keyName),
2300
- typeText: async (value, opts)=>{
2301
- const target = opts?.target;
2302
- if (target && opts?.replace !== false) await this.clearInput(target);
2303
- else if (target) await this.tapPoint({
2304
- x: target.center[0],
2305
- y: target.center[1]
2306
- });
2307
- if (opts?.focusOnly) return;
2308
- await this.typeText(value, opts);
2309
- },
2310
- clearInput: (target)=>this.clearInput(target),
2311
- cursorMove: async (direction, times = 1)=>{
2312
- const arrowKey = 'left' === direction ? 'ArrowLeft' : 'ArrowRight';
2313
- for(let i = 0; i < times; i++)await this.pressKey(arrowKey);
2314
- }
2812
+ keyboardPress: this.visualActions.keyboardPress,
2813
+ typeText: this.visualActions.typeText,
2814
+ clearInput: this.visualActions.clearInput,
2815
+ cursorMove: this.visualActions.cursorMove
2315
2816
  },
2316
2817
  touch: {
2317
- swipe: async (start, end, opts)=>{
2318
- const duration = opts?.duration ?? 300;
2319
- const repeatCount = opts?.repeat ?? 1;
2320
- for(let i = 0; i < repeatCount; i++)await this.dragPoint(start, end, duration);
2321
- },
2322
- pinch: async (center, opts)=>{
2323
- if ('number' == typeof this.options?.displayId && 0 !== this.options.displayId) throw new Error(`Pinch is not supported on a non-default display (displayId=${this.options.displayId}). The underlying yadb tool only injects gestures into the default display.`);
2324
- const { x: adjCenterX, y: adjCenterY } = await this.adjustCoordinates(Math.round(center.x), Math.round(center.y));
2325
- const ratio = 0 !== adjCenterX && 0 !== center.x ? adjCenterX / center.x : 1;
2326
- const adjStartDist = Math.round(opts.startDistance * ratio);
2327
- const adjEndDist = Math.round(opts.endDistance * ratio);
2328
- await this.ensureYadb();
2329
- const adb = await this.getAdb();
2330
- await adb.shell(`app_process -Djava.class.path=/data/local/tmp/yadb /data/local/tmp com.ysbing.yadb.Main -pinch ${adjCenterX} ${adjCenterY} ${adjStartDist} ${adjEndDist} ${opts.duration}`);
2331
- }
2818
+ swipe: this.visualActions.swipe,
2819
+ pinch: this.visualActions.pinch
2332
2820
  },
2333
2821
  scroll: {
2334
- scroll: (param)=>this.performActionScroll(param)
2822
+ scroll: this.visualActions.actionScroll
2335
2823
  },
2336
2824
  system: {
2337
- backButton: ()=>this.back(),
2338
- homeButton: ()=>this.home(),
2339
- recentAppsButton: ()=>this.recentApps()
2825
+ backButton: this.visualActions.back,
2826
+ homeButton: this.visualActions.home,
2827
+ recentAppsButton: this.visualActions.recentApps
2340
2828
  }
2341
2829
  });
2342
2830
  external_node_assert_default()(deviceId, 'deviceId is required for AndroidDevice');
@@ -2358,7 +2846,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
2358
2846
  const terminateParamSchema = core_namespaceObject.z.object({
2359
2847
  uri: core_namespaceObject.z.string().describe('Package name or app name to terminate. Use the exact package name, e.g. com.android.settings.')
2360
2848
  });
2361
- const createPlatformActions = (device)=>({
2849
+ const createPlatformActions = (visualActions)=>({
2362
2850
  RunAdbShell: (0, device_namespaceObject.defineAction)({
2363
2851
  name: 'RunAdbShell',
2364
2852
  description: 'Execute an ADB shell command on the Android device and return the command stdout. Read the returned stdout to decide the next step; the stdout may indicate either success or failure.',
@@ -2369,16 +2857,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
2369
2857
  },
2370
2858
  call: async (param, context)=>{
2371
2859
  if (!param.command || '' === param.command.trim()) throw new Error('RunAdbShell requires a non-empty command parameter');
2372
- const adb = await device.getAdb();
2373
- const stdout = await runAdbShellStdoutOrThrow(adb, param.command, void 0 === param.timeout ? void 0 : {
2374
- timeout: param.timeout
2375
- });
2376
- const planningFeedback = buildRunAdbShellPlanningFeedback({
2377
- command: param.command,
2378
- stdout
2379
- });
2380
- if (planningFeedback && context?.task) context.task.planningFeedback = planningFeedback;
2381
- return stdout;
2860
+ return visualActions.runAdbShell(param, context);
2382
2861
  }
2383
2862
  }),
2384
2863
  Launch: (0, device_namespaceObject.defineAction)({
@@ -2391,7 +2870,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
2391
2870
  },
2392
2871
  call: async (param)=>{
2393
2872
  if (!param.uri || '' === param.uri.trim()) throw new Error('Launch requires a non-empty uri parameter');
2394
- await device.launch(param.uri);
2873
+ await visualActions.launch(param.uri);
2395
2874
  }
2396
2875
  }),
2397
2876
  Terminate: (0, device_namespaceObject.defineAction)({
@@ -2401,7 +2880,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
2401
2880
  paramSchema: terminateParamSchema,
2402
2881
  call: async (param)=>{
2403
2882
  if (!param.uri || '' === param.uri.trim()) throw new Error('Terminate requires a non-empty uri parameter');
2404
- await device.terminate(param.uri);
2883
+ await visualActions.terminate(param.uri);
2405
2884
  }
2406
2885
  })
2407
2886
  });
@@ -2600,7 +3079,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
2600
3079
  const tools = new AndroidMidsceneTools();
2601
3080
  (0, cli_namespaceObject.runToolsCLI)(tools, 'midscene-android', {
2602
3081
  stripPrefix: 'android_',
2603
- version: "1.10.9",
3082
+ version: "1.10.10-beta-20260805080137.0",
2604
3083
  extraCommands: (0, core_namespaceObject.createReportCliCommands)()
2605
3084
  }).catch((e)=>{
2606
3085
  process.exit((0, cli_namespaceObject.reportCLIError)(e));