@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/index.js CHANGED
@@ -5,8 +5,10 @@ const __rslib_import_meta_url__ = /*#__PURE__*/ function() {
5
5
  var __webpack_modules__ = {
6
6
  "./src/scrcpy-manager.ts" (__unused_rspack_module, __webpack_exports__, __webpack_require__) {
7
7
  __webpack_require__.d(__webpack_exports__, {
8
+ GJ: ()=>isScrcpyFreshFrameUnavailableError,
8
9
  ScrcpyScreenshotManager: ()=>ScrcpyScreenshotManager,
9
- 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);
@@ -672,6 +911,7 @@ ${stdout ? truncateAdbShellStream(stdout, 'stdout') : EMPTY_ADB_SHELL_STDOUT}`;
672
911
  class ScrcpyDeviceAdapter {
673
912
  isEnabled() {
674
913
  if (!this.isConfigured()) return false;
914
+ if (this.freshnessRecoveryPending || this.recoveryPromise) return false;
675
915
  return null === this.retryAfter || Date.now() >= this.retryAfter;
676
916
  }
677
917
  getStatus() {
@@ -683,12 +923,18 @@ ${stdout ? truncateAdbShellStream(stdout, 'stdout') : EMPTY_ADB_SHELL_STDOUT}`;
683
923
  };
684
924
  }
685
925
  isConfigured() {
686
- return this.scrcpyConfig?.enabled ?? scrcpy_manager.o.enabled;
926
+ return this.scrcpyConfig?.enabled ?? scrcpy_manager.ov.enabled;
687
927
  }
688
928
  async initialize(deviceInfo) {
929
+ if (this.recoveryPromise) {
930
+ await this.recoveryPromise;
931
+ if (this.manager?.isConnected()) return;
932
+ }
933
+ this.freshnessRecoveryPending = false;
689
934
  try {
690
935
  const manager = await this.ensureManager(deviceInfo);
691
936
  await manager.ensureConnected();
937
+ await this.applyPendingActionBarrier(manager);
692
938
  this.clearFailure();
693
939
  } catch (error) {
694
940
  this.recordFailure(error);
@@ -698,24 +944,27 @@ ${stdout ? truncateAdbShellStream(stdout, 'stdout') : EMPTY_ADB_SHELL_STDOUT}`;
698
944
  recordFailure(error) {
699
945
  this.lastError = error instanceof Error ? error.message : String(error);
700
946
  this.retryAfter = Date.now() + SCRCPY_RETRY_COOLDOWN_MS;
947
+ this.freshnessRecoveryPending = false;
701
948
  }
702
949
  clearFailure() {
703
950
  this.lastError = null;
704
951
  this.retryAfter = null;
952
+ this.freshnessRecoveryPending = false;
705
953
  }
706
954
  ensureRetryReady() {
955
+ if (this.freshnessRecoveryPending || this.recoveryPromise) throw new Error('scrcpy freshness recovery is in progress');
707
956
  if (null === this.retryAfter || Date.now() >= this.retryAfter) return;
708
957
  throw new Error(`scrcpy retry is cooling down until ${new Date(this.retryAfter).toISOString()}. Last error: ${this.lastError}`);
709
958
  }
710
959
  resolveConfig(deviceInfo) {
711
960
  if (this.resolvedConfig) return this.resolvedConfig;
712
961
  const config = this.scrcpyConfig;
713
- const maxSize = config?.maxSize ?? scrcpy_manager.o.maxSize;
714
- const videoBitRate = config?.videoBitRate ?? scrcpy_manager.o.videoBitRate;
962
+ const maxSize = config?.maxSize ?? scrcpy_manager.ov.maxSize;
963
+ const videoBitRate = config?.videoBitRate ?? scrcpy_manager.ov.videoBitRate;
715
964
  this.resolvedConfig = {
716
965
  enabled: this.isConfigured(),
717
966
  maxSize,
718
- idleTimeoutMs: config?.idleTimeoutMs ?? scrcpy_manager.o.idleTimeoutMs,
967
+ idleTimeoutMs: config?.idleTimeoutMs ?? scrcpy_manager.ov.idleTimeoutMs,
719
968
  videoBitRate
720
969
  };
721
970
  return this.resolvedConfig;
@@ -749,24 +998,40 @@ ${stdout ? truncateAdbShellStream(stdout, 'stdout') : EMPTY_ADB_SHELL_STDOUT}`;
749
998
  }
750
999
  async screenshotBase64(deviceInfo) {
751
1000
  this.ensureRetryReady();
1001
+ let manager = null;
752
1002
  try {
753
- const manager = await this.ensureManager(deviceInfo);
1003
+ manager = await this.ensureManager(deviceInfo);
1004
+ await manager.ensureConnected();
1005
+ await this.applyPendingActionBarrier(manager);
754
1006
  const screenshotBuffer = await manager.getScreenshotJpeg();
755
1007
  this.clearFailure();
756
1008
  return (0, img_namespaceObject.createImgBase64ByFormat)('jpeg', screenshotBuffer.toString('base64'));
757
1009
  } catch (error) {
1010
+ if ((0, scrcpy_manager.GJ)(error)) {
1011
+ this.markFreshnessRecoveryPending(manager, error);
1012
+ throw error;
1013
+ }
758
1014
  this.recordFailure(error);
759
1015
  throw error;
760
1016
  }
761
1017
  }
762
1018
  async subscribeKeyframes(deviceInfo, listener) {
763
1019
  this.ensureRetryReady();
1020
+ this.keyframeListeners.add(listener);
764
1021
  try {
765
1022
  const manager = await this.ensureManager(deviceInfo);
766
1023
  await manager.ensureConnected();
1024
+ await this.applyPendingActionBarrier(manager);
1025
+ await manager.ensureFrameClockCalibration();
767
1026
  this.clearFailure();
768
- return manager.subscribeKeyframes(listener);
1027
+ this.attachKeyframeListener(manager, listener);
1028
+ return ()=>{
1029
+ this.keyframeListeners.delete(listener);
1030
+ this.keyframeUnsubscribers.get(listener)?.();
1031
+ this.keyframeUnsubscribers.delete(listener);
1032
+ };
769
1033
  } catch (error) {
1034
+ this.keyframeListeners.delete(listener);
770
1035
  this.recordFailure(error);
771
1036
  throw error;
772
1037
  }
@@ -774,6 +1039,83 @@ ${stdout ? truncateAdbShellStream(stdout, 'stdout') : EMPTY_ADB_SHELL_STDOUT}`;
774
1039
  getLatestRawKeyframe() {
775
1040
  return this.manager?.getLatestRawKeyframe() ?? null;
776
1041
  }
1042
+ attachKeyframeListener(manager, listener) {
1043
+ this.keyframeUnsubscribers.get(listener)?.();
1044
+ this.keyframeUnsubscribers.set(listener, manager.subscribeKeyframes(listener));
1045
+ }
1046
+ attachKeyframeListeners(manager) {
1047
+ this.keyframeUnsubscribers.clear();
1048
+ for (const listener of this.keyframeListeners)this.attachKeyframeListener(manager, listener);
1049
+ }
1050
+ markFreshnessRecoveryPending(manager, error) {
1051
+ this.lastError = error.message;
1052
+ this.retryAfter = null;
1053
+ this.freshnessRecoveryPending = true;
1054
+ this.keyframeUnsubscribers.clear();
1055
+ if (manager && this.manager === manager) this.manager = null;
1056
+ }
1057
+ async applyPendingActionBarrier(manager) {
1058
+ if (!this.pendingActionBarrier) return;
1059
+ await manager.setFreshnessBarrier('completed input action while scrcpy was unavailable');
1060
+ this.pendingActionBarrier = false;
1061
+ }
1062
+ recoverAfterAdbScreenshot(deviceInfo) {
1063
+ if (!this.freshnessRecoveryPending || this.recoveryPromise) return;
1064
+ const generation = this.lifecycleGeneration;
1065
+ const recovery = (async ()=>{
1066
+ let manager = null;
1067
+ try {
1068
+ manager = await this.ensureManager(deviceInfo);
1069
+ await manager.ensureConnected();
1070
+ await this.applyPendingActionBarrier(manager);
1071
+ await manager.prepareFreshFrame();
1072
+ if (generation !== this.lifecycleGeneration) {
1073
+ await manager.disconnect();
1074
+ if (this.manager === manager) this.manager = null;
1075
+ return;
1076
+ }
1077
+ this.attachKeyframeListeners(manager);
1078
+ this.clearFailure();
1079
+ debugAdapter('Scrcpy freshness recovery completed in background');
1080
+ } catch (error) {
1081
+ if (manager) {
1082
+ await manager.disconnect();
1083
+ if (this.manager === manager) this.manager = null;
1084
+ }
1085
+ this.freshnessRecoveryPending = false;
1086
+ this.recordFailure(error);
1087
+ debugAdapter(`Scrcpy background freshness recovery failed: ${error}`);
1088
+ throw error;
1089
+ }
1090
+ })();
1091
+ this.recoveryPromise = recovery;
1092
+ recovery.catch(()=>{}).finally(()=>{
1093
+ if (this.recoveryPromise === recovery) this.recoveryPromise = null;
1094
+ });
1095
+ }
1096
+ async markActionBarrier() {
1097
+ const manager = this.manager;
1098
+ if (!manager?.isConnected()) {
1099
+ this.pendingActionBarrier = true;
1100
+ return;
1101
+ }
1102
+ try {
1103
+ await manager.setFreshnessBarrier('completed input action');
1104
+ this.pendingActionBarrier = false;
1105
+ this.clearFailure();
1106
+ } catch (error) {
1107
+ this.pendingActionBarrier = true;
1108
+ this.recordFailure(error);
1109
+ debugAdapter(`Unable to mark scrcpy action barrier; disabling this stream: ${error}`);
1110
+ try {
1111
+ await manager.disconnect();
1112
+ } catch (disconnectError) {
1113
+ debugAdapter(`Error disconnecting scrcpy after barrier failure: ${disconnectError}`);
1114
+ } finally{
1115
+ if (this.manager === manager) this.manager = null;
1116
+ }
1117
+ }
1118
+ }
777
1119
  async decodeRawKeyframeToJpegBase64(frame) {
778
1120
  if (!this.manager) throw new Error('scrcpy manager is not initialized');
779
1121
  const jpegBuffer = await this.manager.decodeRawKeyframeToJpeg(frame);
@@ -797,6 +1139,13 @@ ${stdout ? truncateAdbShellStream(stdout, 'stdout') : EMPTY_ADB_SHELL_STDOUT}`;
797
1139
  return resolution.width / physicalWidth;
798
1140
  }
799
1141
  async disconnect() {
1142
+ this.lifecycleGeneration += 1;
1143
+ this.freshnessRecoveryPending = false;
1144
+ this.pendingActionBarrier = false;
1145
+ for (const unsubscribe of this.keyframeUnsubscribers.values())unsubscribe();
1146
+ this.keyframeUnsubscribers.clear();
1147
+ this.keyframeListeners.clear();
1148
+ if (this.recoveryPromise) await this.recoveryPromise.catch(()=>{});
800
1149
  if (this.manager) {
801
1150
  try {
802
1151
  await this.manager.disconnect();
@@ -816,6 +1165,12 @@ ${stdout ? truncateAdbShellStream(stdout, 'stdout') : EMPTY_ADB_SHELL_STDOUT}`;
816
1165
  _define_property(this, "resolvedConfig", void 0);
817
1166
  _define_property(this, "lastError", void 0);
818
1167
  _define_property(this, "retryAfter", void 0);
1168
+ _define_property(this, "freshnessRecoveryPending", void 0);
1169
+ _define_property(this, "recoveryPromise", void 0);
1170
+ _define_property(this, "lifecycleGeneration", void 0);
1171
+ _define_property(this, "pendingActionBarrier", void 0);
1172
+ _define_property(this, "keyframeListeners", void 0);
1173
+ _define_property(this, "keyframeUnsubscribers", void 0);
819
1174
  this.deviceId = deviceId;
820
1175
  this.scrcpyConfig = scrcpyConfig;
821
1176
  this.resolveAdbServerEndpoint = resolveAdbServerEndpoint;
@@ -823,6 +1178,12 @@ ${stdout ? truncateAdbShellStream(stdout, 'stdout') : EMPTY_ADB_SHELL_STDOUT}`;
823
1178
  this.resolvedConfig = null;
824
1179
  this.lastError = null;
825
1180
  this.retryAfter = null;
1181
+ this.freshnessRecoveryPending = false;
1182
+ this.recoveryPromise = null;
1183
+ this.lifecycleGeneration = 0;
1184
+ this.pendingActionBarrier = false;
1185
+ this.keyframeListeners = new Set();
1186
+ this.keyframeUnsubscribers = new Map();
826
1187
  }
827
1188
  }
828
1189
  const TAG_RE = /<(\/?)([A-Za-z_][A-Za-z0-9_.\-:]*)([^>]*?)(\/?)>/g;
@@ -1049,6 +1410,20 @@ ${stdout ? truncateAdbShellStream(stdout, 'stdout') : EMPTY_ADB_SHELL_STDOUT}`;
1049
1410
  }
1050
1411
  throw new Error(`Unable to capture Android UI tree after ${failures.length} attempt(s): ${failures.join('; ')}`);
1051
1412
  }
1413
+ function createVisualActionRegistry(definitions, onActionSettled) {
1414
+ const registeredActions = {};
1415
+ for (const actionName of Object.keys(definitions)){
1416
+ const dispatch = definitions[actionName];
1417
+ registeredActions[actionName] = async (...args)=>{
1418
+ try {
1419
+ return await dispatch(...args);
1420
+ } finally{
1421
+ await onActionSettled(actionName);
1422
+ }
1423
+ };
1424
+ }
1425
+ return registeredActions;
1426
+ }
1052
1427
  function device_define_property(obj, key, value) {
1053
1428
  if (key in obj) Object.defineProperty(obj, key, {
1054
1429
  value: value,
@@ -1149,19 +1524,12 @@ ${stdout ? truncateAdbShellStream(stdout, 'stdout') : EMPTY_ADB_SHELL_STDOUT}`;
1149
1524
  }
1150
1525
  },
1151
1526
  call: async (param)=>{
1152
- const element = param.locate;
1153
- const startPoint = element ? {
1154
- left: element.center[0],
1155
- top: element.center[1]
1156
- } : void 0;
1157
1527
  if (!param || !param.direction) throw new Error('PullGesture requires a direction parameter');
1158
- if ('down' === param.direction) await this.pullDown(startPoint, param.distance, param.duration);
1159
- else if ('up' === param.direction) await this.pullUp(startPoint, param.distance, param.duration);
1160
- else throw new Error(`Unknown pull direction: ${param.direction}`);
1528
+ await this.visualActions.pullGesture(param);
1161
1529
  }
1162
1530
  })
1163
1531
  ];
1164
- const platformActions = createPlatformActions(this);
1532
+ const platformActions = createPlatformActions(this.visualActions);
1165
1533
  let platformSpecificActions = Object.values(platformActions);
1166
1534
  if (this.options?.exposeRunAdbShellAction === false) platformSpecificActions = platformSpecificActions.filter((action)=>action !== platformActions.RunAdbShell);
1167
1535
  const customActions = this.customActions || [];
@@ -1171,6 +1539,16 @@ ${stdout ? truncateAdbShellStream(stdout, 'stdout') : EMPTY_ADB_SHELL_STDOUT}`;
1171
1539
  ...customActions
1172
1540
  ];
1173
1541
  }
1542
+ async performPullGesture(param) {
1543
+ const element = param.locate;
1544
+ const startPoint = element ? {
1545
+ left: element.center[0],
1546
+ top: element.center[1]
1547
+ } : void 0;
1548
+ if ('down' === param.direction) await this.pullDownRaw(startPoint, param.distance, param.duration);
1549
+ else if ('up' === param.direction) await this.pullUpRaw(startPoint, param.distance, param.duration);
1550
+ else throw new Error(`Unknown pull direction: ${param.direction}`);
1551
+ }
1174
1552
  async performActionScroll(param) {
1175
1553
  const element = param.locate;
1176
1554
  const startingPoint = element ? {
@@ -1178,20 +1556,32 @@ ${stdout ? truncateAdbShellStream(stdout, 'stdout') : EMPTY_ADB_SHELL_STDOUT}`;
1178
1556
  top: element.center[1]
1179
1557
  } : void 0;
1180
1558
  const scrollToEventName = param?.scrollType;
1181
- if ('scrollToTop' === scrollToEventName) await this.scrollUntilTop(startingPoint);
1182
- else if ('scrollToBottom' === scrollToEventName) await this.scrollUntilBottom(startingPoint);
1183
- else if ('scrollToRight' === scrollToEventName) await this.scrollUntilRight(startingPoint);
1184
- else if ('scrollToLeft' === scrollToEventName) await this.scrollUntilLeft(startingPoint);
1559
+ if ('scrollToTop' === scrollToEventName) await this.scrollUntilTopRaw(startingPoint);
1560
+ else if ('scrollToBottom' === scrollToEventName) await this.scrollUntilBottomRaw(startingPoint);
1561
+ else if ('scrollToRight' === scrollToEventName) await this.scrollUntilRightRaw(startingPoint);
1562
+ else if ('scrollToLeft' === scrollToEventName) await this.scrollUntilLeftRaw(startingPoint);
1185
1563
  else if ('singleAction' !== scrollToEventName && scrollToEventName) throw new Error(`Unknown scroll event type: ${scrollToEventName}, param: ${JSON.stringify(param)}`);
1186
1564
  else {
1187
- if (param?.direction !== 'down' && param && param.direction) if ('up' === param.direction) await this.scrollUp(param.distance || void 0, startingPoint);
1188
- else if ('left' === param.direction) await this.scrollLeft(param.distance || void 0, startingPoint);
1189
- else if ('right' === param.direction) await this.scrollRight(param.distance || void 0, startingPoint);
1565
+ if (param?.direction !== 'down' && param && param.direction) if ('up' === param.direction) await this.scrollUpRaw(param.distance || void 0, startingPoint);
1566
+ else if ('left' === param.direction) await this.scrollLeftRaw(param.distance || void 0, startingPoint);
1567
+ else if ('right' === param.direction) await this.scrollRightRaw(param.distance || void 0, startingPoint);
1190
1568
  else throw new Error(`Unknown scroll direction: ${param.direction}`);
1191
- else await this.scrollDown(param?.distance || void 0, startingPoint);
1569
+ else await this.scrollDownRaw(param?.distance || void 0, startingPoint);
1192
1570
  await (0, utils_namespaceObject.sleep)(500);
1193
1571
  }
1194
1572
  }
1573
+ async runAdbShellRaw(param, context) {
1574
+ const adb = await this.getAdb();
1575
+ const stdout = await runAdbShellStdoutOrThrow(adb, param.command, void 0 === param.timeout ? void 0 : {
1576
+ timeout: param.timeout
1577
+ });
1578
+ const planningFeedback = buildRunAdbShellPlanningFeedback({
1579
+ command: param.command,
1580
+ stdout
1581
+ });
1582
+ if (planningFeedback && context?.task) context.task.planningFeedback = planningFeedback;
1583
+ return stdout;
1584
+ }
1195
1585
  describe() {
1196
1586
  return this.description || `DeviceId: ${this.deviceId}`;
1197
1587
  }
@@ -1288,15 +1678,15 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1288
1678
  const adapter = this.getScrcpyAdapter();
1289
1679
  if (!adapter.isEnabled()) throw new Error('scrcpy is not available for frame observation');
1290
1680
  const deviceInfo = await this.getDevicePhysicalInfo();
1291
- let latest = adapter.getLatestRawKeyframe();
1292
- const unsubscribe = await adapter.subscribeKeyframes(deviceInfo, (frame)=>{
1293
- latest = frame;
1294
- });
1681
+ const unsubscribe = await adapter.subscribeKeyframes(deviceInfo, ()=>{});
1295
1682
  return {
1296
- latest: ()=>latest ? {
1683
+ latest: ()=>{
1684
+ const latest = adapter.getLatestRawKeyframe();
1685
+ return latest ? {
1297
1686
  ref: latest,
1298
1687
  capturedAt: latest.capturedAt
1299
- } : null,
1688
+ } : null;
1689
+ },
1300
1690
  decode: async (refs)=>{
1301
1691
  const images = [];
1302
1692
  for (const frameRef of refs)images.push(await adapter.decodeRawKeyframeToJpegBase64(frameRef.ref));
@@ -1330,6 +1720,9 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1330
1720
  return this.appNameMapping[normalizedAppName];
1331
1721
  }
1332
1722
  async launch(uri) {
1723
+ return this.visualActions.launch(uri);
1724
+ }
1725
+ async launchRaw(uri) {
1333
1726
  const adb = await this.getAdb();
1334
1727
  this.uri = uri;
1335
1728
  try {
@@ -1355,6 +1748,9 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1355
1748
  return this;
1356
1749
  }
1357
1750
  async terminate(uri) {
1751
+ await this.visualActions.terminate(uri);
1752
+ }
1753
+ async terminateRaw(uri) {
1358
1754
  const packagePart = uri.includes('/') ? uri.split('/')[0] : uri;
1359
1755
  const resolved = this.resolvePackageName(packagePart) ?? packagePart;
1360
1756
  const adb = await this.getAdb();
@@ -1370,6 +1766,9 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1370
1766
  }
1371
1767
  }
1372
1768
  async execYadb(keyboardContent) {
1769
+ await this.visualActions.execYadb(keyboardContent);
1770
+ }
1771
+ async execYadbRaw(keyboardContent) {
1373
1772
  this.warnYadbOnNonDefaultDisplay('keyboard input');
1374
1773
  await this.ensureYadb();
1375
1774
  const adb = await this.getAdb();
@@ -1626,14 +2025,15 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1626
2025
  async screenshotBase64() {
1627
2026
  debugDevice('screenshotBase64 begin');
1628
2027
  const adapter = this.getScrcpyAdapter();
2028
+ let scrcpyDeviceInfo = null;
1629
2029
  if (adapter.isEnabled()) try {
1630
2030
  debugDevice('Attempting scrcpy screenshot...');
1631
- const deviceInfo = await this.getDevicePhysicalInfo();
1632
- const result = await adapter.screenshotBase64(deviceInfo);
2031
+ scrcpyDeviceInfo = await this.getDevicePhysicalInfo();
2032
+ const result = await adapter.screenshotBase64(scrcpyDeviceInfo);
1633
2033
  debugDevice('screenshotBase64 end (scrcpy mode)');
1634
2034
  return result;
1635
2035
  } catch (error) {
1636
- warnDevice(`Scrcpy screenshot failed, falling back to standard ADB method.\nError: ${error}`);
2036
+ warnDevice(`Scrcpy screenshot failed, falling back to standard ADB method. This may be caused by transport backlog. ${scrcpy_manager.Xr}\nError: ${error}`);
1637
2037
  }
1638
2038
  const adb = await this.getAdb();
1639
2039
  let screenshotBuffer;
@@ -1711,9 +2111,13 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1711
2111
  });
1712
2112
  }
1713
2113
  debugDevice('screenshotBase64 end');
2114
+ if (scrcpyDeviceInfo) adapter.recoverAfterAdbScreenshot(scrcpyDeviceInfo);
1714
2115
  return result;
1715
2116
  }
1716
2117
  async clearInput(element) {
2118
+ await this.visualActions.clearInput(element);
2119
+ }
2120
+ async clearInputRaw(element) {
1717
2121
  if (element) await this.tapPoint({
1718
2122
  x: element.center[0],
1719
2123
  y: element.center[1]
@@ -1747,6 +2151,9 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1747
2151
  return '';
1748
2152
  }
1749
2153
  async scrollUntilTop(startPoint) {
2154
+ await this.visualActions.scrollUntilTop(startPoint);
2155
+ }
2156
+ async scrollUntilTopRaw(startPoint) {
1750
2157
  if (startPoint) {
1751
2158
  const { height } = await this.size();
1752
2159
  const start = {
@@ -1761,10 +2168,13 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1761
2168
  await (0, utils_namespaceObject.sleep)(1000);
1762
2169
  return;
1763
2170
  }
1764
- await (0, shared_utils_namespaceObject.repeat)(defaultScrollUntilTimes, ()=>this.scroll(0, -9999999, defaultFastScrollDuration));
2171
+ await (0, shared_utils_namespaceObject.repeat)(defaultScrollUntilTimes, ()=>this.scrollRaw(0, -9999999, defaultFastScrollDuration));
1765
2172
  await (0, utils_namespaceObject.sleep)(1000);
1766
2173
  }
1767
2174
  async scrollUntilBottom(startPoint) {
2175
+ await this.visualActions.scrollUntilBottom(startPoint);
2176
+ }
2177
+ async scrollUntilBottomRaw(startPoint) {
1768
2178
  if (startPoint) {
1769
2179
  const start = {
1770
2180
  x: Math.round(startPoint.left),
@@ -1778,10 +2188,13 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1778
2188
  await (0, utils_namespaceObject.sleep)(1000);
1779
2189
  return;
1780
2190
  }
1781
- await (0, shared_utils_namespaceObject.repeat)(defaultScrollUntilTimes, ()=>this.scroll(0, 9999999, defaultFastScrollDuration));
2191
+ await (0, shared_utils_namespaceObject.repeat)(defaultScrollUntilTimes, ()=>this.scrollRaw(0, 9999999, defaultFastScrollDuration));
1782
2192
  await (0, utils_namespaceObject.sleep)(1000);
1783
2193
  }
1784
2194
  async scrollUntilLeft(startPoint) {
2195
+ await this.visualActions.scrollUntilLeft(startPoint);
2196
+ }
2197
+ async scrollUntilLeftRaw(startPoint) {
1785
2198
  if (startPoint) {
1786
2199
  const { width } = await this.size();
1787
2200
  const start = {
@@ -1796,10 +2209,13 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1796
2209
  await (0, utils_namespaceObject.sleep)(1000);
1797
2210
  return;
1798
2211
  }
1799
- await (0, shared_utils_namespaceObject.repeat)(defaultScrollUntilTimes, ()=>this.scroll(-9999999, 0, defaultFastScrollDuration));
2212
+ await (0, shared_utils_namespaceObject.repeat)(defaultScrollUntilTimes, ()=>this.scrollRaw(-9999999, 0, defaultFastScrollDuration));
1800
2213
  await (0, utils_namespaceObject.sleep)(1000);
1801
2214
  }
1802
2215
  async scrollUntilRight(startPoint) {
2216
+ await this.visualActions.scrollUntilRight(startPoint);
2217
+ }
2218
+ async scrollUntilRightRaw(startPoint) {
1803
2219
  if (startPoint) {
1804
2220
  const start = {
1805
2221
  x: Math.round(startPoint.left),
@@ -1813,10 +2229,13 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1813
2229
  await (0, utils_namespaceObject.sleep)(1000);
1814
2230
  return;
1815
2231
  }
1816
- await (0, shared_utils_namespaceObject.repeat)(defaultScrollUntilTimes, ()=>this.scroll(9999999, 0, defaultFastScrollDuration));
2232
+ await (0, shared_utils_namespaceObject.repeat)(defaultScrollUntilTimes, ()=>this.scrollRaw(9999999, 0, defaultFastScrollDuration));
1817
2233
  await (0, utils_namespaceObject.sleep)(1000);
1818
2234
  }
1819
2235
  async scrollUp(distance, startPoint) {
2236
+ await this.visualActions.scrollUp(distance, startPoint);
2237
+ }
2238
+ async scrollUpRaw(distance, startPoint) {
1820
2239
  const { height } = await this.size();
1821
2240
  const scrollDistance = Math.round(distance || height);
1822
2241
  const hasExplicitDistance = void 0 !== distance;
@@ -1830,9 +2249,12 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1830
2249
  await this.dragPoint(start, end);
1831
2250
  return;
1832
2251
  }
1833
- await this.scroll(0, -scrollDistance, void 0, hasExplicitDistance, 'up');
2252
+ await this.scrollRaw(0, -scrollDistance, void 0, hasExplicitDistance, 'up');
1834
2253
  }
1835
2254
  async scrollDown(distance, startPoint) {
2255
+ await this.visualActions.scrollDown(distance, startPoint);
2256
+ }
2257
+ async scrollDownRaw(distance, startPoint) {
1836
2258
  const { height } = await this.size();
1837
2259
  const scrollDistance = Math.round(distance || height);
1838
2260
  const hasExplicitDistance = void 0 !== distance;
@@ -1846,9 +2268,12 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1846
2268
  await this.dragPoint(start, end);
1847
2269
  return;
1848
2270
  }
1849
- await this.scroll(0, scrollDistance, void 0, hasExplicitDistance, 'down');
2271
+ await this.scrollRaw(0, scrollDistance, void 0, hasExplicitDistance, 'down');
1850
2272
  }
1851
2273
  async scrollLeft(distance, startPoint) {
2274
+ await this.visualActions.scrollLeft(distance, startPoint);
2275
+ }
2276
+ async scrollLeftRaw(distance, startPoint) {
1852
2277
  const { width } = await this.size();
1853
2278
  const scrollDistance = Math.round(distance || width);
1854
2279
  const hasExplicitDistance = void 0 !== distance;
@@ -1862,9 +2287,12 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1862
2287
  await this.dragPoint(start, end);
1863
2288
  return;
1864
2289
  }
1865
- await this.scroll(-scrollDistance, 0, void 0, hasExplicitDistance, 'left');
2290
+ await this.scrollRaw(-scrollDistance, 0, void 0, hasExplicitDistance, 'left');
1866
2291
  }
1867
2292
  async scrollRight(distance, startPoint) {
2293
+ await this.visualActions.scrollRight(distance, startPoint);
2294
+ }
2295
+ async scrollRightRaw(distance, startPoint) {
1868
2296
  const { width } = await this.size();
1869
2297
  const scrollDistance = Math.round(distance || width);
1870
2298
  const hasExplicitDistance = void 0 !== distance;
@@ -1878,7 +2306,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1878
2306
  await this.dragPoint(start, end);
1879
2307
  return;
1880
2308
  }
1881
- await this.scroll(scrollDistance, 0, void 0, hasExplicitDistance, 'right');
2309
+ await this.scrollRaw(scrollDistance, 0, void 0, hasExplicitDistance, 'right');
1882
2310
  }
1883
2311
  async ensureYadb() {
1884
2312
  if (!this.yadbPushed) {
@@ -1907,11 +2335,11 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1907
2335
  const reason = IME_STRATEGY === IME_STRATEGY_ALWAYS_YADB ? "imeStrategy 'always-yadb' requires yadb" : 'text contains characters that require yadb (non-ASCII, format specifiers, or mixed quotes)';
1908
2336
  throw new Error(`${reason}, but yadb (app_process) cannot target non-default displayId=${this.options?.displayId}. Use displayId=0 or a different imeStrategy.`);
1909
2337
  }
1910
- if (needsYadb) await this.execYadb(escapeForShell(text));
2338
+ if (needsYadb) await this.execYadbRaw(escapeForShell(text));
1911
2339
  else await this.shellInputText(text, {
1912
2340
  keyboardTypeDelay: typeDelay
1913
2341
  });
1914
- if (true === shouldAutoDismissKeyboard) await this.hideKeyboard(options);
2342
+ if (true === shouldAutoDismissKeyboard) await this.hideKeyboardRaw(options);
1915
2343
  }
1916
2344
  normalizeKeyName(key) {
1917
2345
  const keyMap = {
@@ -1981,6 +2409,9 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1981
2409
  await adb.shell(`input${this.getDisplayArg()} swipe ${fromX} ${fromY} ${toX} ${toY} ${duration}`);
1982
2410
  }
1983
2411
  async scroll(deltaX, deltaY, duration, warnOnClamp = false, direction) {
2412
+ await this.visualActions.scroll(deltaX, deltaY, duration, warnOnClamp, direction);
2413
+ }
2414
+ async scrollRaw(deltaX, deltaY, duration, warnOnClamp = false, direction) {
1984
2415
  if (0 === deltaX && 0 === deltaY) throw new Error('Scroll distance cannot be zero in both directions');
1985
2416
  const { width, height } = await this.size();
1986
2417
  const n = 4;
@@ -2045,12 +2476,21 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
2045
2476
  }
2046
2477
  }
2047
2478
  async back() {
2479
+ await this.visualActions.back();
2480
+ }
2481
+ async backRaw() {
2048
2482
  await this.shellInputKeyevent(4);
2049
2483
  }
2050
2484
  async home() {
2485
+ await this.visualActions.home();
2486
+ }
2487
+ async homeRaw() {
2051
2488
  await this.shellInputKeyevent(3);
2052
2489
  }
2053
2490
  async recentApps() {
2491
+ await this.visualActions.recentApps();
2492
+ }
2493
+ async recentAppsRaw() {
2054
2494
  await this.shellInputKeyevent(187);
2055
2495
  }
2056
2496
  async longPressPoint(point, duration = 2000) {
@@ -2059,6 +2499,9 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
2059
2499
  await adb.shell(`input${this.getDisplayArg()} swipe ${adjustedX} ${adjustedY} ${adjustedX} ${adjustedY} ${duration}`);
2060
2500
  }
2061
2501
  async pullDown(startPoint, distance, duration = 800) {
2502
+ await this.visualActions.pullDown(startPoint, distance, duration);
2503
+ }
2504
+ async pullDownRaw(startPoint, distance, duration = 800) {
2062
2505
  const { width, height } = await this.size();
2063
2506
  const start = startPoint ? {
2064
2507
  x: Math.round(startPoint.left),
@@ -2072,13 +2515,19 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
2072
2515
  x: start.x,
2073
2516
  y: start.y + pullDistance
2074
2517
  };
2075
- await this.pullDrag(start, end, duration);
2518
+ await this.pullDragRaw(start, end, duration);
2076
2519
  await (0, utils_namespaceObject.sleep)(200);
2077
2520
  }
2078
2521
  async pullDrag(from, to, duration) {
2522
+ await this.visualActions.pullDrag(from, to, duration);
2523
+ }
2524
+ async pullDragRaw(from, to, duration) {
2079
2525
  await this.swipePoint(from, to, duration);
2080
2526
  }
2081
2527
  async pullUp(startPoint, distance, duration = 600) {
2528
+ await this.visualActions.pullUp(startPoint, distance, duration);
2529
+ }
2530
+ async pullUpRaw(startPoint, distance, duration = 600) {
2082
2531
  const { width, height } = await this.size();
2083
2532
  const start = startPoint ? {
2084
2533
  x: Math.round(startPoint.left),
@@ -2092,7 +2541,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
2092
2541
  x: start.x,
2093
2542
  y: start.y - pullDistance
2094
2543
  };
2095
- await this.pullDrag(start, end, duration);
2544
+ await this.pullDragRaw(start, end, duration);
2096
2545
  await (0, utils_namespaceObject.sleep)(100);
2097
2546
  }
2098
2547
  getDisplayArg() {
@@ -2155,6 +2604,9 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
2155
2604
  }
2156
2605
  }
2157
2606
  async hideKeyboard(options, timeoutMs = 1000) {
2607
+ return this.visualActions.hideKeyboard(options, timeoutMs);
2608
+ }
2609
+ async hideKeyboardRaw(options, timeoutMs = 1000) {
2158
2610
  const adb = await this.getAdb();
2159
2611
  const keyboardDismissStrategy = options?.keyboardDismissStrategy ?? this.options?.keyboardDismissStrategy ?? 'esc-first';
2160
2612
  const keyboardStatus = await adb.isSoftKeyboardPresent();
@@ -2209,55 +2661,91 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
2209
2661
  device_define_property(this, "interfaceType", 'android');
2210
2662
  device_define_property(this, "uri", void 0);
2211
2663
  device_define_property(this, "options", void 0);
2664
+ device_define_property(this, "visualActions", createVisualActionRegistry({
2665
+ tap: (point)=>this.tapPoint(point),
2666
+ doubleClick: (point)=>this.doubleTapPoint(point),
2667
+ longPress: (point, opts)=>this.longPressPoint(point, opts?.duration),
2668
+ dragAndDrop: (from, to)=>this.dragPoint(from, to),
2669
+ keyboardPress: (keyName)=>this.pressKey(keyName),
2670
+ typeText: async (value, opts)=>{
2671
+ const target = opts?.target;
2672
+ if (target && opts?.replace !== false) await this.clearInputRaw(target);
2673
+ else if (target) await this.tapPoint({
2674
+ x: target.center[0],
2675
+ y: target.center[1]
2676
+ });
2677
+ if (opts?.focusOnly) return;
2678
+ await this.typeText(value, opts);
2679
+ },
2680
+ clearInput: (target)=>this.clearInputRaw(target),
2681
+ cursorMove: async (direction, times = 1)=>{
2682
+ const arrowKey = 'left' === direction ? 'ArrowLeft' : 'ArrowRight';
2683
+ for(let index = 0; index < times; index++)await this.pressKey(arrowKey);
2684
+ },
2685
+ swipe: async (start, end, opts)=>{
2686
+ const duration = opts?.duration ?? 300;
2687
+ const repeatCount = opts?.repeat ?? 1;
2688
+ for(let index = 0; index < repeatCount; index++)await this.dragPoint(start, end, duration);
2689
+ },
2690
+ pinch: async (center, opts)=>{
2691
+ if ('number' == typeof this.options?.displayId && 0 !== this.options.displayId) throw new Error(`Pinch is not supported on a non-default display (displayId=${this.options.displayId}). The underlying yadb tool only injects gestures into the default display.`);
2692
+ const { x: adjCenterX, y: adjCenterY } = await this.adjustCoordinates(Math.round(center.x), Math.round(center.y));
2693
+ const ratio = 0 !== adjCenterX && 0 !== center.x ? adjCenterX / center.x : 1;
2694
+ const adjStartDist = Math.round(opts.startDistance * ratio);
2695
+ const adjEndDist = Math.round(opts.endDistance * ratio);
2696
+ await this.ensureYadb();
2697
+ const adb = await this.getAdb();
2698
+ await adb.shell(`app_process -Djava.class.path=/data/local/tmp/yadb /data/local/tmp com.ysbing.yadb.Main -pinch ${adjCenterX} ${adjCenterY} ${adjStartDist} ${adjEndDist} ${opts.duration}`);
2699
+ },
2700
+ actionScroll: (param)=>this.performActionScroll(param),
2701
+ back: ()=>this.backRaw(),
2702
+ home: ()=>this.homeRaw(),
2703
+ recentApps: ()=>this.recentAppsRaw(),
2704
+ pullGesture: (param)=>this.performPullGesture(param),
2705
+ launch: (uri)=>this.launchRaw(uri),
2706
+ terminate: (uri)=>this.terminateRaw(uri),
2707
+ runAdbShell: (param, context)=>this.runAdbShellRaw(param, context),
2708
+ execYadb: (keyboardContent)=>this.execYadbRaw(keyboardContent),
2709
+ scrollUntilTop: (startPoint)=>this.scrollUntilTopRaw(startPoint),
2710
+ scrollUntilBottom: (startPoint)=>this.scrollUntilBottomRaw(startPoint),
2711
+ scrollUntilLeft: (startPoint)=>this.scrollUntilLeftRaw(startPoint),
2712
+ scrollUntilRight: (startPoint)=>this.scrollUntilRightRaw(startPoint),
2713
+ scrollUp: (distance, startPoint)=>this.scrollUpRaw(distance, startPoint),
2714
+ scrollDown: (distance, startPoint)=>this.scrollDownRaw(distance, startPoint),
2715
+ scrollLeft: (distance, startPoint)=>this.scrollLeftRaw(distance, startPoint),
2716
+ scrollRight: (distance, startPoint)=>this.scrollRightRaw(distance, startPoint),
2717
+ scroll: (deltaX, deltaY, duration, warnOnClamp, direction)=>this.scrollRaw(deltaX, deltaY, duration, warnOnClamp, direction),
2718
+ pullDown: (startPoint, distance, duration)=>this.pullDownRaw(startPoint, distance, duration),
2719
+ pullDrag: (from, to, duration)=>this.pullDragRaw(from, to, duration),
2720
+ pullUp: (startPoint, distance, duration)=>this.pullUpRaw(startPoint, distance, duration),
2721
+ hideKeyboard: (options, timeoutMs)=>this.hideKeyboardRaw(options, timeoutMs)
2722
+ }, async ()=>{
2723
+ await this.scrcpyAdapter?.markActionBarrier();
2724
+ }));
2212
2725
  device_define_property(this, "inputPrimitives", {
2213
2726
  pointer: {
2214
- tap: (point)=>this.tapPoint(point),
2215
- doubleClick: (point)=>this.doubleTapPoint(point),
2216
- longPress: (point, opts)=>this.longPressPoint(point, opts?.duration),
2217
- dragAndDrop: (from, to)=>this.dragPoint(from, to)
2727
+ tap: this.visualActions.tap,
2728
+ doubleClick: this.visualActions.doubleClick,
2729
+ longPress: this.visualActions.longPress,
2730
+ dragAndDrop: this.visualActions.dragAndDrop
2218
2731
  },
2219
2732
  keyboard: {
2220
- keyboardPress: (keyName)=>this.pressKey(keyName),
2221
- typeText: async (value, opts)=>{
2222
- const target = opts?.target;
2223
- if (target && opts?.replace !== false) await this.clearInput(target);
2224
- else if (target) await this.tapPoint({
2225
- x: target.center[0],
2226
- y: target.center[1]
2227
- });
2228
- if (opts?.focusOnly) return;
2229
- await this.typeText(value, opts);
2230
- },
2231
- clearInput: (target)=>this.clearInput(target),
2232
- cursorMove: async (direction, times = 1)=>{
2233
- const arrowKey = 'left' === direction ? 'ArrowLeft' : 'ArrowRight';
2234
- for(let i = 0; i < times; i++)await this.pressKey(arrowKey);
2235
- }
2733
+ keyboardPress: this.visualActions.keyboardPress,
2734
+ typeText: this.visualActions.typeText,
2735
+ clearInput: this.visualActions.clearInput,
2736
+ cursorMove: this.visualActions.cursorMove
2236
2737
  },
2237
2738
  touch: {
2238
- swipe: async (start, end, opts)=>{
2239
- const duration = opts?.duration ?? 300;
2240
- const repeatCount = opts?.repeat ?? 1;
2241
- for(let i = 0; i < repeatCount; i++)await this.dragPoint(start, end, duration);
2242
- },
2243
- pinch: async (center, opts)=>{
2244
- if ('number' == typeof this.options?.displayId && 0 !== this.options.displayId) throw new Error(`Pinch is not supported on a non-default display (displayId=${this.options.displayId}). The underlying yadb tool only injects gestures into the default display.`);
2245
- const { x: adjCenterX, y: adjCenterY } = await this.adjustCoordinates(Math.round(center.x), Math.round(center.y));
2246
- const ratio = 0 !== adjCenterX && 0 !== center.x ? adjCenterX / center.x : 1;
2247
- const adjStartDist = Math.round(opts.startDistance * ratio);
2248
- const adjEndDist = Math.round(opts.endDistance * ratio);
2249
- await this.ensureYadb();
2250
- const adb = await this.getAdb();
2251
- await adb.shell(`app_process -Djava.class.path=/data/local/tmp/yadb /data/local/tmp com.ysbing.yadb.Main -pinch ${adjCenterX} ${adjCenterY} ${adjStartDist} ${adjEndDist} ${opts.duration}`);
2252
- }
2739
+ swipe: this.visualActions.swipe,
2740
+ pinch: this.visualActions.pinch
2253
2741
  },
2254
2742
  scroll: {
2255
- scroll: (param)=>this.performActionScroll(param)
2743
+ scroll: this.visualActions.actionScroll
2256
2744
  },
2257
2745
  system: {
2258
- backButton: ()=>this.back(),
2259
- homeButton: ()=>this.home(),
2260
- recentAppsButton: ()=>this.recentApps()
2746
+ backButton: this.visualActions.back,
2747
+ homeButton: this.visualActions.home,
2748
+ recentAppsButton: this.visualActions.recentApps
2261
2749
  }
2262
2750
  });
2263
2751
  external_node_assert_default()(deviceId, 'deviceId is required for AndroidDevice');
@@ -2279,7 +2767,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
2279
2767
  const terminateParamSchema = core_namespaceObject.z.object({
2280
2768
  uri: core_namespaceObject.z.string().describe('Package name or app name to terminate. Use the exact package name, e.g. com.android.settings.')
2281
2769
  });
2282
- const createPlatformActions = (device)=>({
2770
+ const createPlatformActions = (visualActions)=>({
2283
2771
  RunAdbShell: (0, device_namespaceObject.defineAction)({
2284
2772
  name: 'RunAdbShell',
2285
2773
  description: 'Execute an ADB shell command on the Android device and return the command stdout. Read the returned stdout to decide the next step; the stdout may indicate either success or failure.',
@@ -2290,16 +2778,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
2290
2778
  },
2291
2779
  call: async (param, context)=>{
2292
2780
  if (!param.command || '' === param.command.trim()) throw new Error('RunAdbShell requires a non-empty command parameter');
2293
- const adb = await device.getAdb();
2294
- const stdout = await runAdbShellStdoutOrThrow(adb, param.command, void 0 === param.timeout ? void 0 : {
2295
- timeout: param.timeout
2296
- });
2297
- const planningFeedback = buildRunAdbShellPlanningFeedback({
2298
- command: param.command,
2299
- stdout
2300
- });
2301
- if (planningFeedback && context?.task) context.task.planningFeedback = planningFeedback;
2302
- return stdout;
2781
+ return visualActions.runAdbShell(param, context);
2303
2782
  }
2304
2783
  }),
2305
2784
  Launch: (0, device_namespaceObject.defineAction)({
@@ -2312,7 +2791,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
2312
2791
  },
2313
2792
  call: async (param)=>{
2314
2793
  if (!param.uri || '' === param.uri.trim()) throw new Error('Launch requires a non-empty uri parameter');
2315
- await device.launch(param.uri);
2794
+ await visualActions.launch(param.uri);
2316
2795
  }
2317
2796
  }),
2318
2797
  Terminate: (0, device_namespaceObject.defineAction)({
@@ -2322,7 +2801,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
2322
2801
  paramSchema: terminateParamSchema,
2323
2802
  call: async (param)=>{
2324
2803
  if (!param.uri || '' === param.uri.trim()) throw new Error('Terminate requires a non-empty uri parameter');
2325
- await device.terminate(param.uri);
2804
+ await visualActions.terminate(param.uri);
2326
2805
  }
2327
2806
  })
2328
2807
  });