@midscene/android 1.12.0 → 1.12.1-beta-20260824081858.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
@@ -17,7 +17,6 @@ var __webpack_modules__ = {
17
17
  __webpack_require__.d(__webpack_exports__, {
18
18
  GJ: ()=>isScrcpyFreshFrameUnavailableError,
19
19
  ScrcpyScreenshotManager: ()=>ScrcpyScreenshotManager,
20
- Xr: ()=>SCRCPY_VIDEO_BIT_RATE_NETWORK_HINT,
21
20
  ov: ()=>DEFAULT_SCRCPY_CONFIG
22
21
  });
23
22
  var node_fs__rspack_import_0 = __webpack_require__("node:fs");
@@ -52,7 +51,6 @@ var __webpack_modules__ = {
52
51
  const FRESH_FRAME_TIMEOUT_MS = 300;
53
52
  const KEYFRAME_POLL_INTERVAL_MS = 200;
54
53
  const MAX_SCAN_BYTES = 1000;
55
- const CONNECTION_WAIT_MS = 1000;
56
54
  const MAX_SERVER_OUTPUT_LINES = 100;
57
55
  const SERVER_OUTPUT_DRAIN_TIMEOUT_MS = 500;
58
56
  const MAX_FRAME_AGE_US = 500000n;
@@ -62,7 +60,12 @@ var __webpack_modules__ = {
62
60
  'dumpsys',
63
61
  'power'
64
62
  ];
65
- const SCRCPY_VIDEO_BIT_RATE_NETWORK_HINT = 'The appropriate scrcpy video bitrate depends on network conditions. For constrained remote links, consider setting scrcpyConfig.videoBitRate to 4_000_000 (4 Mbps) as a starting point, and lower it further if backlog persists.';
63
+ const SCRCPY_VIDEO_BIT_RATE_NETWORK_HINT = 'The appropriate scrcpy video bitrate depends on network conditions. For constrained remote links, pass --scrcpy-video-bit-rate 4000000 in the Android CLI, or set scrcpyConfig.videoBitRate to 4_000_000 (4 Mbps) in SDK/YAML configuration. Lower it further if backlog persists.';
64
+ const CONSTRAINED_LINK_STARTING_VIDEO_BIT_RATE = 4000000;
65
+ function getScrcpyVideoBitRateNetworkHint(currentVideoBitRate) {
66
+ if (currentVideoBitRate <= CONSTRAINED_LINK_STARTING_VIDEO_BIT_RATE) return 'The current scrcpy video bitrate is already at or below 4 Mbps. Lowering it further is unlikely to help on a local USB connection; check whether the screen was static or the device encoder emitted no new frame.';
67
+ return SCRCPY_VIDEO_BIT_RATE_NETWORK_HINT;
68
+ }
66
69
  const BUSY_LOOP_WINDOW_MS = 1000;
67
70
  const BUSY_LOOP_MAX_READS = 500;
68
71
  const BUSY_LOOP_COOLDOWN_MS = 50;
@@ -76,8 +79,9 @@ var __webpack_modules__ = {
76
79
  const SCRCPY_FRESH_FRAME_UNAVAILABLE_ERROR_CODE = 'ERR_SCRCPY_FRESH_FRAME_UNAVAILABLE';
77
80
  class ScrcpyFreshFrameUnavailableError extends Error {
78
81
  constructor(message, options){
79
- super(message, options), _define_property(this, "code", SCRCPY_FRESH_FRAME_UNAVAILABLE_ERROR_CODE);
82
+ super(message, options), _define_property(this, "code", SCRCPY_FRESH_FRAME_UNAVAILABLE_ERROR_CODE), _define_property(this, "diagnosticMessage", void 0);
80
83
  this.name = 'ScrcpyFreshFrameUnavailableError';
84
+ this.diagnosticMessage = options?.diagnosticMessage;
81
85
  }
82
86
  }
83
87
  function isScrcpyFreshFrameUnavailableError(error) {
@@ -110,26 +114,30 @@ var __webpack_modules__ = {
110
114
  await this.ensureFfmpegAvailable();
111
115
  }
112
116
  async ensureConnected() {
117
+ if (this.disposed) throw new Error('Scrcpy manager has been disposed');
113
118
  if (this.scrcpyClient && this.videoStream) {
114
119
  debugScrcpy('Scrcpy already connected');
115
120
  await this.ensureFrameClockCalibration();
116
121
  this.resetIdleTimer();
117
122
  return;
118
123
  }
119
- if (this.isConnecting) {
120
- debugScrcpy('Connection already in progress, waiting...');
121
- await new Promise((resolve)=>setTimeout(resolve, CONNECTION_WAIT_MS));
122
- if (this.scrcpyClient && this.videoStream) {
123
- await this.ensureFrameClockCalibration();
124
- this.resetIdleTimer();
125
- return;
126
- }
127
- throw new Error('Scrcpy connection failed: another connection attempt did not complete in time');
124
+ if (this.connectionPromise) {
125
+ debugScrcpy('Connection already in progress, sharing the same attempt...');
126
+ await this.connectionPromise;
127
+ return;
128
128
  }
129
+ const connectionPromise = this.connectScrcpy();
130
+ this.connectionPromise = connectionPromise;
131
+ try {
132
+ await connectionPromise;
133
+ } finally{
134
+ if (this.connectionPromise === connectionPromise) this.connectionPromise = null;
135
+ }
136
+ }
137
+ async connectScrcpy() {
129
138
  const serverOutput = [];
130
139
  let serverOutputTask = null;
131
140
  try {
132
- this.isConnecting = true;
133
141
  debugScrcpy('Starting scrcpy connection...');
134
142
  const { AdbScrcpyClient } = await import("@yume-chan/adb-scrcpy");
135
143
  const { ReadableStream } = await import("@yume-chan/stream-extra");
@@ -149,6 +157,8 @@ var __webpack_modules__ = {
149
157
  height
150
158
  };
151
159
  await this.ensureFrameClockCalibration();
160
+ this.streamBaselineFramePending = true;
161
+ this.streamBaselineFrameDeadlineAt = Date.now() + MAX_KEYFRAME_WAIT_MS;
152
162
  this.startFrameConsumer();
153
163
  this.resetIdleTimer();
154
164
  this.isInitialized = true;
@@ -161,8 +171,6 @@ var __webpack_modules__ = {
161
171
  new Promise((resolve)=>setTimeout(resolve, SERVER_OUTPUT_DRAIN_TIMEOUT_MS))
162
172
  ]);
163
173
  throw this.createConnectionError(error, serverOutput);
164
- } finally{
165
- this.isConnecting = false;
166
174
  }
167
175
  }
168
176
  async createScrcpyOptions() {
@@ -347,24 +355,30 @@ var __webpack_modules__ = {
347
355
  if (this.deviceClockCalibrationPromise === calibrationPromise) this.deviceClockCalibrationPromise = null;
348
356
  }
349
357
  }
350
- async setFreshnessBarrier(reason) {
358
+ async setFreshnessBarrier(reason, options = {}) {
359
+ const cachedFrame = this.getCachedKeyframeCandidate();
351
360
  const generation = ++this.frameFreshnessBarrierGeneration;
352
361
  this.frameFreshnessBarrierPending = true;
362
+ this.frameFreshnessBarrierAllowsOverAgeForNextCapture = false;
353
363
  this.clearFrameCache();
354
364
  try {
355
365
  const calibration = this.deviceClockCalibration;
356
366
  if (!calibration) throw new Error('Scrcpy frame clock is not calibrated for the current stream epoch');
357
- const hostMonotonicUs = this.monotonicTimeUs();
367
+ const hostMonotonicUs = options.hostMonotonicUs ?? this.monotonicTimeUs();
358
368
  const estimatedDeviceNowUs = this.estimateDeviceTimeUs(hostMonotonicUs, calibration);
359
369
  const conservativeDeviceNowUs = estimatedDeviceNowUs + this.getCalibrationUncertaintyUs(calibration);
360
370
  const barrierPtsUs = (conservativeDeviceNowUs / 1000n + 1n) * 1000n;
361
371
  if (generation !== this.frameFreshnessBarrierGeneration) return this.frameFreshnessBarrierPtsUs ?? barrierPtsUs;
362
372
  this.frameFreshnessBarrierPtsUs = null === this.frameFreshnessBarrierPtsUs || barrierPtsUs > this.frameFreshnessBarrierPtsUs ? barrierPtsUs : this.frameFreshnessBarrierPtsUs;
363
373
  this.frameFreshnessBarrierReason = reason;
374
+ this.frameFreshnessBarrierAllowsOverAgeForNextCapture = options.allowOverAgeForNextCapture ?? false;
364
375
  this.frameFreshnessBarrierPending = false;
365
376
  this.frameFreshnessError = null;
366
377
  this.lastFramePtsUs = null;
367
- this.clearFrameCache();
378
+ if (cachedFrame?.ptsUs !== void 0 && cachedFrame.ptsUs >= this.frameFreshnessBarrierPtsUs) {
379
+ this.restoreFrameCache(cachedFrame);
380
+ debugScrcpy(`Preserved cached frame PTS ${cachedFrame.ptsUs}µs because it crosses the newly armed ${reason} barrier`);
381
+ } else this.clearFrameCache();
368
382
  debugScrcpy(`Armed frame freshness barrier at PTS ${this.frameFreshnessBarrierPtsUs}µs (${reason}, projected from stream-epoch clock anchor, uncertainty<=${Number(this.getCalibrationUncertaintyUs(calibration)) / 1000}ms)`);
369
383
  return this.frameFreshnessBarrierPtsUs;
370
384
  } catch (error) {
@@ -385,6 +399,7 @@ var __webpack_modules__ = {
385
399
  this.frameFreshnessBarrierPending = true;
386
400
  this.frameFreshnessBarrierPtsUs = null;
387
401
  this.frameFreshnessBarrierReason = null;
402
+ this.frameFreshnessBarrierAllowsOverAgeForNextCapture = false;
388
403
  this.deviceClockCalibration = null;
389
404
  this.clearFrameCache();
390
405
  this.warnFrameFreshness();
@@ -406,7 +421,7 @@ var __webpack_modules__ = {
406
421
  const behindBarrierUs = this.frameFreshnessBarrierPtsUs - packetPtsUs;
407
422
  this.frameFreshnessError = new Error(`Scrcpy frame predates the ${this.frameFreshnessBarrierReason ?? 'active'} freshness barrier by ${Number(behindBarrierUs) / 1000}ms; refusing to use it`);
408
423
  this.clearFrameCache();
409
- this.warnFrameFreshness();
424
+ debugScrcpy(this.frameFreshnessError.message);
410
425
  return false;
411
426
  }
412
427
  estimateFrameAgeUs(packetPtsUs, hostMonotonicUs = this.monotonicTimeUs()) {
@@ -456,13 +471,18 @@ var __webpack_modules__ = {
456
471
  this.lastFrameFreshnessWarningAt = now;
457
472
  }
458
473
  }
459
- warnTransportBacklog(error) {
460
- const now = Date.now();
461
- if (now - this.lastTransportBacklogWarningAt < TRANSPORT_BACKLOG_WARN_INTERVAL_MS) return;
474
+ transportBacklogMessage(error) {
462
475
  const cause = this.frameFreshnessError ?? error;
463
476
  const causeMessage = cause instanceof Error ? cause.message : String(cause);
477
+ if (this.streamBaselineFramePending && null === this.frameFreshnessBarrierPtsUs) return `The new scrcpy stream did not produce a usable baseline frame within its ${MAX_KEYFRAME_WAIT_MS}ms startup window; closing the stream epoch and falling back to ADB screenshot. This is a stream startup or encoder-readiness failure, not evidence that the configured video bitrate is too high.\nError: ${causeMessage}`;
464
478
  const currentBitRateMbps = this.options.videoBitRate / 1000000;
465
- 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}`);
479
+ const networkHint = getScrcpyVideoBitRateNetworkHint(this.options.videoBitRate);
480
+ return `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. ${networkHint} Current videoBitRate: ${this.options.videoBitRate} bps (${currentBitRateMbps} Mbps).\nError: ${causeMessage}`;
481
+ }
482
+ warnTransportBacklog(error) {
483
+ const now = Date.now();
484
+ if (now - this.lastTransportBacklogWarningAt < TRANSPORT_BACKLOG_WARN_INTERVAL_MS) return;
485
+ warnScrcpy(this.transportBacklogMessage(error));
466
486
  this.lastTransportBacklogWarningAt = now;
467
487
  }
468
488
  estimateFrameTiming(packetPtsUs, receivedAtUs) {
@@ -484,12 +504,21 @@ var __webpack_modules__ = {
484
504
  this.lastRawKeyframePtsUs = void 0;
485
505
  this.lastRawKeyframeEstimatedAgeMs = void 0;
486
506
  }
507
+ restoreFrameCache(frame) {
508
+ this.lastRawKeyframe = frame.data;
509
+ this.lastRawKeyframeAt = frame.capturedAt;
510
+ this.lastRawKeyframePtsUs = frame.ptsUs;
511
+ this.lastRawKeyframeEstimatedAgeMs = frame.estimatedAgeMs;
512
+ }
487
513
  monotonicTimeUs() {
488
514
  return process.hrtime.bigint() / 1000n;
489
515
  }
490
516
  resetFrameFreshnessState() {
491
517
  this.frameFreshnessBarrierPtsUs = null;
492
518
  this.frameFreshnessBarrierReason = null;
519
+ this.frameFreshnessBarrierAllowsOverAgeForNextCapture = false;
520
+ this.streamBaselineFramePending = false;
521
+ this.streamBaselineFrameDeadlineAt = 0;
493
522
  this.frameFreshnessBarrierPending = false;
494
523
  this.frameFreshnessBarrierGeneration = 0;
495
524
  this.deviceClockCalibration = null;
@@ -521,35 +550,52 @@ var __webpack_modules__ = {
521
550
  capturedAt: this.lastRawKeyframeAt
522
551
  };
523
552
  }
553
+ canReuseFrameAcrossActionWait(frame) {
554
+ return this.frameFreshnessBarrierAllowsOverAgeForNextCapture && null !== this.frameFreshnessBarrierPtsUs && void 0 !== frame.ptsUs && frame.ptsUs >= this.frameFreshnessBarrierPtsUs;
555
+ }
556
+ canUseStreamBaseline(frame) {
557
+ return this.streamBaselineFramePending && !this.frameFreshnessBarrierPending && null === this.frameFreshnessBarrierPtsUs && Date.now() <= this.streamBaselineFrameDeadlineAt && void 0 !== frame.ptsUs;
558
+ }
559
+ consumeActionFreshnessBarrier(frame) {
560
+ if (!this.canReuseFrameAcrossActionWait(frame)) return;
561
+ this.frameFreshnessBarrierPtsUs = null;
562
+ this.frameFreshnessBarrierReason = null;
563
+ this.frameFreshnessBarrierAllowsOverAgeForNextCapture = false;
564
+ this.frameFreshnessError = null;
565
+ }
524
566
  async decodeRawKeyframeToJpeg(frame) {
525
567
  return this.decodeH264ToJpeg(Buffer.concat([
526
568
  frame.header,
527
569
  frame.data
528
570
  ]));
529
571
  }
530
- async waitForUsableKeyframe(timeoutMs) {
531
- const deadline = Date.now() + timeoutMs;
532
- let candidate = this.getCachedKeyframeCandidate();
533
- while(true){
534
- if (candidate && this.isFrameAgeAcceptable(candidate.ptsUs)) return candidate;
535
- const remainingMs = deadline - Date.now();
536
- if (remainingMs <= 0) throw new Error(`No fresh keyframe received within ${timeoutMs}ms`);
537
- candidate = await this.waitForNextKeyframe(remainingMs);
538
- }
539
- }
540
572
  async prepareFreshFrame() {
541
573
  await this.ensureConnected();
542
574
  await this.ensureFrameClockCalibration();
543
575
  await this.waitForKeyframe();
544
- await this.waitForUsableKeyframe(MAX_KEYFRAME_WAIT_MS);
576
+ await this.waitForPlanningFrame();
545
577
  }
546
578
  async waitForPlanningFrame() {
547
579
  let planningBarrierArmed = false;
548
- let deadline = Date.now() + FRESH_FRAME_TIMEOUT_MS;
580
+ const planningStartedAt = Date.now();
581
+ const startupWindowRemainingMs = this.streamBaselineFramePending ? this.streamBaselineFrameDeadlineAt - planningStartedAt : 0;
582
+ if (this.streamBaselineFramePending && startupWindowRemainingMs <= 0) {
583
+ this.streamBaselineFramePending = false;
584
+ this.streamBaselineFrameDeadlineAt = 0;
585
+ }
586
+ let deadline = planningStartedAt + (startupWindowRemainingMs > 0 ? startupWindowRemainingMs : FRESH_FRAME_TIMEOUT_MS);
549
587
  let candidate = this.getCachedKeyframeCandidate();
550
588
  while(true){
551
589
  if (candidate) {
552
590
  if (void 0 === candidate.ptsUs) throw new Error('Scrcpy frame has no PTS metadata; cannot prove planning freshness');
591
+ if (this.canReuseFrameAcrossActionWait(candidate)) {
592
+ debugScrcpy(`Using frame PTS ${candidate.ptsUs}µs that crossed the active input-action barrier; preserving the settled frame across wait-after-action`);
593
+ return candidate;
594
+ }
595
+ if (this.canUseStreamBaseline(candidate)) {
596
+ debugScrcpy(`Using frame PTS ${candidate.ptsUs}µs as the first planning baseline for the new scrcpy stream epoch`);
597
+ return candidate;
598
+ }
553
599
  const age = this.estimateFrameAge(candidate.ptsUs);
554
600
  if (null === age) throw new Error('Scrcpy frame clock is not calibrated; cannot prove planning freshness');
555
601
  if (age.upperBoundUs <= MAX_FRAME_AGE_US) return candidate;
@@ -567,11 +613,12 @@ var __webpack_modules__ = {
567
613
  }
568
614
  }
569
615
  async closeStaleStreamAndCreateFallbackError(error) {
570
- this.warnTransportBacklog(error);
616
+ const diagnosticMessage = this.transportBacklogMessage(error);
571
617
  const causeMessage = error instanceof Error ? error.message : String(error);
572
618
  await this.disconnect();
573
- 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}`, {
574
- cause: error
619
+ return new ScrcpyFreshFrameUnavailableError(`Unable to obtain a fresh scrcpy frame; the stale stream epoch was closed so the caller can restart it or use ADB screenshot fallback. ${causeMessage}`, {
620
+ cause: error,
621
+ diagnosticMessage
575
622
  });
576
623
  }
577
624
  async getScreenshotJpeg() {
@@ -598,6 +645,9 @@ var __webpack_modules__ = {
598
645
  const t5 = Date.now();
599
646
  const result = await this.decodeH264ToJpeg(keyframeBuffer);
600
647
  const decodeTime = Date.now() - t5;
648
+ this.consumeActionFreshnessBarrier(frame);
649
+ this.streamBaselineFramePending = false;
650
+ this.streamBaselineFrameDeadlineAt = 0;
601
651
  const totalTime = Date.now() - perfStart;
602
652
  debugScrcpy(`Performance: total=${totalTime}ms (connect=${connectTime}ms, frameWait=${frameWaitTime}ms, decode=${decodeTime}ms)`);
603
653
  return result;
@@ -754,6 +804,13 @@ var __webpack_modules__ = {
754
804
  }
755
805
  debugScrcpy('Scrcpy disconnected');
756
806
  }
807
+ async dispose() {
808
+ if (this.disposed) return;
809
+ this.disposed = true;
810
+ if (this.connectionPromise) await this.connectionPromise.catch(()=>{});
811
+ await this.disconnect();
812
+ await this.adb.close();
813
+ }
757
814
  isConnected() {
758
815
  return this.isInitialized && null !== this.scrcpyClient;
759
816
  }
@@ -763,7 +820,8 @@ var __webpack_modules__ = {
763
820
  _define_property(this, "videoStream", null);
764
821
  _define_property(this, "spsHeader", null);
765
822
  _define_property(this, "idleTimer", null);
766
- _define_property(this, "isConnecting", false);
823
+ _define_property(this, "connectionPromise", null);
824
+ _define_property(this, "disposed", false);
767
825
  _define_property(this, "isInitialized", false);
768
826
  _define_property(this, "options", void 0);
769
827
  _define_property(this, "ffmpegAvailable", null);
@@ -777,6 +835,9 @@ var __webpack_modules__ = {
777
835
  _define_property(this, "streamReader", null);
778
836
  _define_property(this, "frameFreshnessBarrierPtsUs", null);
779
837
  _define_property(this, "frameFreshnessBarrierReason", null);
838
+ _define_property(this, "frameFreshnessBarrierAllowsOverAgeForNextCapture", false);
839
+ _define_property(this, "streamBaselineFramePending", false);
840
+ _define_property(this, "streamBaselineFrameDeadlineAt", 0);
780
841
  _define_property(this, "frameFreshnessBarrierPending", false);
781
842
  _define_property(this, "frameFreshnessBarrierGeneration", 0);
782
843
  _define_property(this, "deviceClockCalibration", null);
@@ -960,6 +1021,9 @@ ${stdout ? truncateAdbShellStream(stdout, 'stdout') : EMPTY_ADB_SHELL_STDOUT}`;
960
1021
  return obj;
961
1022
  }
962
1023
  const debugAdapter = (0, logger_.getDebug)('android:scrcpy-adapter');
1024
+ const warnAdapter = (0, logger_.getDebug)('android:scrcpy-adapter', {
1025
+ console: true
1026
+ });
963
1027
  const SCRCPY_RETRY_COOLDOWN_MS = 5000;
964
1028
  const DEFAULT_ADB_SERVER_ENDPOINT = {
965
1029
  host: '127.0.0.1',
@@ -1013,10 +1077,11 @@ ${stdout ? truncateAdbShellStream(stdout, 'stdout') : EMPTY_ADB_SHELL_STDOUT}`;
1013
1077
  if (null === this.retryAfter || Date.now() >= this.retryAfter) return;
1014
1078
  throw new Error(`scrcpy retry is cooling down until ${new Date(this.retryAfter).toISOString()}. Last error: ${this.lastError}`);
1015
1079
  }
1016
- resolveConfig(deviceInfo) {
1080
+ resolveConfig(_deviceInfo) {
1017
1081
  if (this.resolvedConfig) return this.resolvedConfig;
1018
1082
  const config = this.scrcpyConfig;
1019
1083
  const maxSize = config?.maxSize ?? scrcpy_manager.ov.maxSize;
1084
+ if (!Number.isInteger(maxSize) || maxSize < 0) throw new Error(`Invalid scrcpyConfig.maxSize: expected a non-negative integer, received ${maxSize}`);
1020
1085
  const videoBitRate = config?.videoBitRate ?? scrcpy_manager.ov.videoBitRate;
1021
1086
  this.resolvedConfig = {
1022
1087
  enabled: this.isConfigured(),
@@ -1026,31 +1091,68 @@ ${stdout ? truncateAdbShellStream(stdout, 'stdout') : EMPTY_ADB_SHELL_STDOUT}`;
1026
1091
  };
1027
1092
  return this.resolvedConfig;
1028
1093
  }
1094
+ async prepareFallbackScreenshot(screenshotBase64) {
1095
+ if (!this.isConfigured()) return screenshotBase64;
1096
+ const { maxSize } = this.resolveConfig();
1097
+ if (0 === maxSize) return screenshotBase64;
1098
+ const sourceSize = await (0, img_namespaceObject.imageInfoOfBase64)(screenshotBase64);
1099
+ const largestDimension = Math.max(sourceSize.width, sourceSize.height);
1100
+ if (largestDimension <= maxSize) return screenshotBase64;
1101
+ const scale = maxSize / largestDimension;
1102
+ const targetSize = {
1103
+ width: Math.max(1, Math.round(sourceSize.width * scale)),
1104
+ height: Math.max(1, Math.round(sourceSize.height * scale))
1105
+ };
1106
+ debugAdapter(`Applying scrcpy maxSize to fallback screenshot: ${sourceSize.width}x${sourceSize.height} -> ${targetSize.width}x${targetSize.height}`);
1107
+ return (0, img_namespaceObject.resizeBase64ImageToJpeg)(screenshotBase64, {
1108
+ sourceSize,
1109
+ targetSize
1110
+ });
1111
+ }
1029
1112
  async ensureManager(deviceInfo) {
1030
1113
  if (this.manager) return this.manager;
1114
+ if (this.managerPromise) return this.managerPromise;
1031
1115
  debugAdapter('Initializing Scrcpy manager...');
1116
+ const generation = this.lifecycleGeneration;
1117
+ const managerPromise = (async ()=>{
1118
+ let adb = null;
1119
+ let manager = null;
1120
+ try {
1121
+ const { Adb, AdbServerClient } = await import("@yume-chan/adb");
1122
+ const { AdbServerNodeTcpConnector } = await import("@yume-chan/adb-server-node-tcp");
1123
+ const { ScrcpyScreenshotManager: ScrcpyManager } = await Promise.resolve().then(__webpack_require__.bind(__webpack_require__, "./src/scrcpy-manager.ts"));
1124
+ const adbServerEndpoint = await this.resolveAdbServerEndpoint();
1125
+ const adbClient = new AdbServerClient(new AdbServerNodeTcpConnector(adbServerEndpoint));
1126
+ adb = new Adb(await adbClient.createTransport({
1127
+ serial: this.deviceId
1128
+ }));
1129
+ const config = this.resolveConfig(deviceInfo);
1130
+ manager = new ScrcpyManager(adb, {
1131
+ maxSize: config.maxSize,
1132
+ videoBitRate: config.videoBitRate,
1133
+ idleTimeoutMs: config.idleTimeoutMs
1134
+ });
1135
+ await manager.validateEnvironment();
1136
+ if (generation !== this.lifecycleGeneration) throw new Error('Scrcpy manager initialization was superseded by device cleanup');
1137
+ this.manager = manager;
1138
+ debugAdapter('Scrcpy manager initialized');
1139
+ return manager;
1140
+ } catch (error) {
1141
+ try {
1142
+ if (manager) await manager.dispose();
1143
+ else await adb?.close();
1144
+ } catch (cleanupError) {
1145
+ debugAdapter(`Failed to clean up Scrcpy manager initialization: ${cleanupError}`);
1146
+ }
1147
+ debugAdapter(`Failed to initialize Scrcpy manager: ${error}`);
1148
+ throw new Error(`Failed to initialize Scrcpy for device ${this.deviceId}. Ensure ADB server is running and device is connected. Error: ${error}`);
1149
+ }
1150
+ })();
1151
+ this.managerPromise = managerPromise;
1032
1152
  try {
1033
- const { Adb, AdbServerClient } = await import("@yume-chan/adb");
1034
- const { AdbServerNodeTcpConnector } = await import("@yume-chan/adb-server-node-tcp");
1035
- const { ScrcpyScreenshotManager: ScrcpyManager } = await Promise.resolve().then(__webpack_require__.bind(__webpack_require__, "./src/scrcpy-manager.ts"));
1036
- const adbServerEndpoint = await this.resolveAdbServerEndpoint();
1037
- const adbClient = new AdbServerClient(new AdbServerNodeTcpConnector(adbServerEndpoint));
1038
- const adb = new Adb(await adbClient.createTransport({
1039
- serial: this.deviceId
1040
- }));
1041
- const config = this.resolveConfig(deviceInfo);
1042
- const manager = new ScrcpyManager(adb, {
1043
- maxSize: config.maxSize,
1044
- videoBitRate: config.videoBitRate,
1045
- idleTimeoutMs: config.idleTimeoutMs
1046
- });
1047
- await manager.validateEnvironment();
1048
- this.manager = manager;
1049
- debugAdapter('Scrcpy manager initialized');
1050
- return this.manager;
1051
- } catch (error) {
1052
- debugAdapter(`Failed to initialize Scrcpy manager: ${error}`);
1053
- throw new Error(`Failed to initialize Scrcpy for device ${this.deviceId}. Ensure ADB server is running and device is connected. Error: ${error}`);
1153
+ return await managerPromise;
1154
+ } finally{
1155
+ if (this.managerPromise === managerPromise) this.managerPromise = null;
1054
1156
  }
1055
1157
  }
1056
1158
  async screenshotBase64(deviceInfo) {
@@ -1062,16 +1164,54 @@ ${stdout ? truncateAdbShellStream(stdout, 'stdout') : EMPTY_ADB_SHELL_STDOUT}`;
1062
1164
  await this.applyPendingActionBarrier(manager);
1063
1165
  const screenshotBuffer = await manager.getScreenshotJpeg();
1064
1166
  this.clearFailure();
1065
- return (0, img_namespaceObject.createImgBase64ByFormat)('jpeg', screenshotBuffer.toString('base64'));
1167
+ return this.jpegBufferToBase64(screenshotBuffer);
1066
1168
  } catch (error) {
1067
- if ((0, scrcpy_manager.GJ)(error)) {
1068
- this.markFreshnessRecoveryPending(manager, error);
1169
+ if (!(0, scrcpy_manager.GJ)(error)) {
1170
+ this.recordFailure(error);
1069
1171
  throw error;
1070
1172
  }
1071
- this.recordFailure(error);
1072
- throw error;
1173
+ this.markFreshnessRecoveryPending(error);
1174
+ debugAdapter(`Scrcpy freshness target was unavailable; restarting the stream once before ADB fallback: ${error}`);
1175
+ try {
1176
+ const screenshotBuffer = await this.restartAndCaptureOnce(deviceInfo);
1177
+ manager = this.manager;
1178
+ if (!manager) throw new Error('Scrcpy manager disappeared after freshness recovery');
1179
+ this.attachKeyframeListeners(manager);
1180
+ this.clearFailure();
1181
+ debugAdapter('Scrcpy screenshot recovered on a new stream epoch');
1182
+ return this.jpegBufferToBase64(screenshotBuffer);
1183
+ } catch (retryError) {
1184
+ if ((0, scrcpy_manager.GJ)(retryError)) this.markFreshnessRecoveryPending(retryError);
1185
+ else this.recordFailure(retryError);
1186
+ this.warnFreshnessFallback(error, retryError);
1187
+ throw retryError;
1188
+ }
1073
1189
  }
1074
1190
  }
1191
+ async restartAndCaptureOnce(deviceInfo) {
1192
+ if (this.freshnessRestartPromise) return this.freshnessRestartPromise;
1193
+ const generation = this.lifecycleGeneration;
1194
+ const restartPromise = (async ()=>{
1195
+ const manager = await this.ensureManager(deviceInfo);
1196
+ await manager.ensureConnected();
1197
+ await this.applyPendingActionBarrier(manager);
1198
+ if (generation !== this.lifecycleGeneration) throw new Error('Scrcpy freshness restart was cancelled by cleanup');
1199
+ return manager.getScreenshotJpeg();
1200
+ })();
1201
+ this.freshnessRestartPromise = restartPromise;
1202
+ try {
1203
+ return await restartPromise;
1204
+ } finally{
1205
+ if (this.freshnessRestartPromise === restartPromise) this.freshnessRestartPromise = null;
1206
+ }
1207
+ }
1208
+ jpegBufferToBase64(screenshotBuffer) {
1209
+ return (0, img_namespaceObject.createImgBase64ByFormat)('jpeg', screenshotBuffer.toString('base64'));
1210
+ }
1211
+ warnFreshnessFallback(firstError, retryError) {
1212
+ const retryDiagnostic = (0, scrcpy_manager.GJ)(retryError) ? retryError.diagnosticMessage : void 0;
1213
+ warnAdapter(retryDiagnostic ?? (firstError.diagnosticMessage ? `${firstError.diagnosticMessage}\nScrcpy stream restart error: ${retryError}` : void 0) ?? `Scrcpy stream restart failed; falling back to ADB screenshot. Error: ${retryError}`);
1214
+ }
1075
1215
  async subscribeKeyframes(deviceInfo, listener) {
1076
1216
  this.ensureRetryReady();
1077
1217
  this.keyframeListeners.add(listener);
@@ -1104,17 +1244,26 @@ ${stdout ? truncateAdbShellStream(stdout, 'stdout') : EMPTY_ADB_SHELL_STDOUT}`;
1104
1244
  this.keyframeUnsubscribers.clear();
1105
1245
  for (const listener of this.keyframeListeners)this.attachKeyframeListener(manager, listener);
1106
1246
  }
1107
- markFreshnessRecoveryPending(manager, error) {
1247
+ markFreshnessRecoveryPending(error) {
1108
1248
  this.lastError = error.message;
1109
1249
  this.retryAfter = null;
1110
1250
  this.freshnessRecoveryPending = true;
1111
1251
  this.keyframeUnsubscribers.clear();
1112
- if (manager && this.manager === manager) this.manager = null;
1113
1252
  }
1114
1253
  async applyPendingActionBarrier(manager) {
1115
- if (!this.pendingActionBarrier) return;
1116
- await manager.setFreshnessBarrier('completed input action while scrcpy was unavailable');
1117
- this.pendingActionBarrier = false;
1254
+ const actionCompletedAtHostUs = this.pendingActionBarrierAtHostUs;
1255
+ if (null === actionCompletedAtHostUs) return;
1256
+ await manager.setFreshnessBarrier('completed input action while scrcpy was unavailable', {
1257
+ allowOverAgeForNextCapture: true,
1258
+ hostMonotonicUs: actionCompletedAtHostUs
1259
+ });
1260
+ if (this.pendingActionBarrierAtHostUs === actionCompletedAtHostUs) this.pendingActionBarrierAtHostUs = null;
1261
+ }
1262
+ monotonicTimeUs() {
1263
+ return process.hrtime.bigint() / 1000n;
1264
+ }
1265
+ deferActionBarrier(actionCompletedAtHostUs) {
1266
+ if (null === this.pendingActionBarrierAtHostUs || actionCompletedAtHostUs > this.pendingActionBarrierAtHostUs) this.pendingActionBarrierAtHostUs = actionCompletedAtHostUs;
1118
1267
  }
1119
1268
  recoverAfterAdbScreenshot(deviceInfo) {
1120
1269
  if (!this.freshnessRecoveryPending || this.recoveryPromise) return;
@@ -1127,7 +1276,7 @@ ${stdout ? truncateAdbShellStream(stdout, 'stdout') : EMPTY_ADB_SHELL_STDOUT}`;
1127
1276
  await this.applyPendingActionBarrier(manager);
1128
1277
  await manager.prepareFreshFrame();
1129
1278
  if (generation !== this.lifecycleGeneration) {
1130
- await manager.disconnect();
1279
+ await manager.dispose();
1131
1280
  if (this.manager === manager) this.manager = null;
1132
1281
  return;
1133
1282
  }
@@ -1136,7 +1285,7 @@ ${stdout ? truncateAdbShellStream(stdout, 'stdout') : EMPTY_ADB_SHELL_STDOUT}`;
1136
1285
  debugAdapter('Scrcpy freshness recovery completed in background');
1137
1286
  } catch (error) {
1138
1287
  if (manager) {
1139
- await manager.disconnect();
1288
+ await manager.dispose();
1140
1289
  if (this.manager === manager) this.manager = null;
1141
1290
  }
1142
1291
  this.freshnessRecoveryPending = false;
@@ -1151,21 +1300,22 @@ ${stdout ? truncateAdbShellStream(stdout, 'stdout') : EMPTY_ADB_SHELL_STDOUT}`;
1151
1300
  });
1152
1301
  }
1153
1302
  async markActionBarrier() {
1303
+ const actionCompletedAtHostUs = this.monotonicTimeUs();
1154
1304
  const manager = this.manager;
1155
- if (!manager?.isConnected()) {
1156
- this.pendingActionBarrier = true;
1157
- return;
1158
- }
1305
+ if (!manager?.isConnected()) return void this.deferActionBarrier(actionCompletedAtHostUs);
1159
1306
  try {
1160
- await manager.setFreshnessBarrier('completed input action');
1161
- this.pendingActionBarrier = false;
1307
+ await manager.setFreshnessBarrier('completed input action', {
1308
+ allowOverAgeForNextCapture: true,
1309
+ hostMonotonicUs: actionCompletedAtHostUs
1310
+ });
1311
+ if (null !== this.pendingActionBarrierAtHostUs && this.pendingActionBarrierAtHostUs <= actionCompletedAtHostUs) this.pendingActionBarrierAtHostUs = null;
1162
1312
  this.clearFailure();
1163
1313
  } catch (error) {
1164
- this.pendingActionBarrier = true;
1314
+ this.deferActionBarrier(actionCompletedAtHostUs);
1165
1315
  this.recordFailure(error);
1166
1316
  debugAdapter(`Unable to mark scrcpy action barrier; disabling this stream: ${error}`);
1167
1317
  try {
1168
- await manager.disconnect();
1318
+ await manager.dispose();
1169
1319
  } catch (disconnectError) {
1170
1320
  debugAdapter(`Error disconnecting scrcpy after barrier failure: ${disconnectError}`);
1171
1321
  } finally{
@@ -1198,14 +1348,16 @@ ${stdout ? truncateAdbShellStream(stdout, 'stdout') : EMPTY_ADB_SHELL_STDOUT}`;
1198
1348
  async disconnect() {
1199
1349
  this.lifecycleGeneration += 1;
1200
1350
  this.freshnessRecoveryPending = false;
1201
- this.pendingActionBarrier = false;
1351
+ this.pendingActionBarrierAtHostUs = null;
1202
1352
  for (const unsubscribe of this.keyframeUnsubscribers.values())unsubscribe();
1203
1353
  this.keyframeUnsubscribers.clear();
1204
1354
  this.keyframeListeners.clear();
1205
1355
  if (this.recoveryPromise) await this.recoveryPromise.catch(()=>{});
1356
+ if (this.freshnessRestartPromise) await this.freshnessRestartPromise.catch(()=>{});
1357
+ if (this.managerPromise) await this.managerPromise.catch(()=>{});
1206
1358
  if (this.manager) {
1207
1359
  try {
1208
- await this.manager.disconnect();
1360
+ await this.manager.dispose();
1209
1361
  } catch (error) {
1210
1362
  debugAdapter(`Error disconnecting scrcpy: ${error}`);
1211
1363
  }
@@ -1219,26 +1371,30 @@ ${stdout ? truncateAdbShellStream(stdout, 'stdout') : EMPTY_ADB_SHELL_STDOUT}`;
1219
1371
  _define_property(this, "scrcpyConfig", void 0);
1220
1372
  _define_property(this, "resolveAdbServerEndpoint", void 0);
1221
1373
  _define_property(this, "manager", void 0);
1374
+ _define_property(this, "managerPromise", void 0);
1222
1375
  _define_property(this, "resolvedConfig", void 0);
1223
1376
  _define_property(this, "lastError", void 0);
1224
1377
  _define_property(this, "retryAfter", void 0);
1225
1378
  _define_property(this, "freshnessRecoveryPending", void 0);
1379
+ _define_property(this, "freshnessRestartPromise", void 0);
1226
1380
  _define_property(this, "recoveryPromise", void 0);
1227
1381
  _define_property(this, "lifecycleGeneration", void 0);
1228
- _define_property(this, "pendingActionBarrier", void 0);
1382
+ _define_property(this, "pendingActionBarrierAtHostUs", void 0);
1229
1383
  _define_property(this, "keyframeListeners", void 0);
1230
1384
  _define_property(this, "keyframeUnsubscribers", void 0);
1231
1385
  this.deviceId = deviceId;
1232
1386
  this.scrcpyConfig = scrcpyConfig;
1233
1387
  this.resolveAdbServerEndpoint = resolveAdbServerEndpoint;
1234
1388
  this.manager = null;
1389
+ this.managerPromise = null;
1235
1390
  this.resolvedConfig = null;
1236
1391
  this.lastError = null;
1237
1392
  this.retryAfter = null;
1238
1393
  this.freshnessRecoveryPending = false;
1394
+ this.freshnessRestartPromise = null;
1239
1395
  this.recoveryPromise = null;
1240
1396
  this.lifecycleGeneration = 0;
1241
- this.pendingActionBarrier = false;
1397
+ this.pendingActionBarrierAtHostUs = null;
1242
1398
  this.keyframeListeners = new Set();
1243
1399
  this.keyframeUnsubscribers = new Map();
1244
1400
  }
@@ -2089,9 +2245,9 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
2089
2245
  }
2090
2246
  async screenshotBase64() {
2091
2247
  debugDevice('screenshotBase64 begin');
2092
- const screenshotStrategy = this.options?.screenshotStrategy || env_namespaceObject.globalConfigManager.getEnvConfigValue(env_namespaceObject.MIDSCENE_ANDROID_SCREENSHOT_STRATEGY) || SCREENSHOT_STRATEGY_AUTO;
2093
- if (screenshotStrategy === SCREENSHOT_STRATEGY_ALWAYS_YADB) return this.screenshotBase64ViaYadb();
2094
2248
  const adapter = this.getScrcpyAdapter();
2249
+ const screenshotStrategy = this.options?.screenshotStrategy || env_namespaceObject.globalConfigManager.getEnvConfigValue(env_namespaceObject.MIDSCENE_ANDROID_SCREENSHOT_STRATEGY) || SCREENSHOT_STRATEGY_AUTO;
2250
+ if (screenshotStrategy === SCREENSHOT_STRATEGY_ALWAYS_YADB) return adapter.prepareFallbackScreenshot(await this.screenshotBase64ViaYadb());
2095
2251
  let scrcpyDeviceInfo = null;
2096
2252
  if (adapter.isEnabled()) try {
2097
2253
  debugDevice('Attempting scrcpy screenshot...');
@@ -2100,7 +2256,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
2100
2256
  debugDevice('screenshotBase64 end (scrcpy mode)');
2101
2257
  return result;
2102
2258
  } catch (error) {
2103
- warnDevice(`Scrcpy screenshot failed, falling back to standard ADB method. This may be caused by transport backlog. ${scrcpy_manager.Xr}\nError: ${error}`);
2259
+ warnDevice(`Scrcpy screenshot failed, falling back to standard ADB method.\nError: ${error}`);
2104
2260
  }
2105
2261
  const adb = await this.getAdb();
2106
2262
  let screenshotBuffer;
@@ -2142,14 +2298,14 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
2142
2298
  });
2143
2299
  debugDevice('screenshotBase64 end (fallback)');
2144
2300
  if (scrcpyDeviceInfo) adapter.recoverAfterAdbScreenshot(scrcpyDeviceInfo);
2145
- return result;
2301
+ return adapter.prepareFallbackScreenshot(result);
2146
2302
  }
2147
2303
  if (!screenshotBuffer) throw new Error('Failed to capture screenshot: all methods failed');
2148
2304
  debugDevice('Converting to base64');
2149
2305
  const result = (0, img_namespaceObject.createImgBase64ByFormat)('png', screenshotBuffer.toString('base64'));
2150
2306
  debugDevice('screenshotBase64 end');
2151
2307
  if (scrcpyDeviceInfo) adapter.recoverAfterAdbScreenshot(scrcpyDeviceInfo);
2152
- return result;
2308
+ return adapter.prepareFallbackScreenshot(result);
2153
2309
  }
2154
2310
  async captureScreenshotBase64FromDeviceFile(label, capture) {
2155
2311
  const adb = await this.getAdb();
@@ -3170,12 +3326,18 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
3170
3326
  const debug = (0, logger_.getDebug)('agent-tools:android');
3171
3327
  function adaptAndroidInitArgs(extracted) {
3172
3328
  if (!extracted) return;
3329
+ const scrcpyVideoBitRate = 'number' == typeof extracted.scrcpyVideoBitRate ? extracted.scrcpyVideoBitRate : void 0;
3330
+ const requestedUseScrcpy = 'boolean' == typeof extracted.useScrcpy ? extracted.useScrcpy : void 0;
3331
+ const useScrcpy = void 0 !== scrcpyVideoBitRate ? true : requestedUseScrcpy;
3173
3332
  const initArgs = {
3174
3333
  ...'string' == typeof extracted.deviceId ? {
3175
3334
  deviceId: extracted.deviceId
3176
3335
  } : {},
3177
- ...'boolean' == typeof extracted.useScrcpy ? {
3178
- useScrcpy: extracted.useScrcpy
3336
+ ...void 0 !== useScrcpy ? {
3337
+ useScrcpy
3338
+ } : {},
3339
+ ...void 0 !== scrcpyVideoBitRate ? {
3340
+ scrcpyVideoBitRate
3179
3341
  } : {},
3180
3342
  ...(0, agent_behavior_init_args_namespaceObject.extractAgentBehaviorInitArgs)(extracted) ?? {}
3181
3343
  };
@@ -3207,7 +3369,10 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
3207
3369
  ...(0, agent_behavior_init_args_namespaceObject.extractAgentBehaviorInitArgs)(initArgs) ?? {},
3208
3370
  ...initArgs?.useScrcpy ? {
3209
3371
  scrcpyConfig: {
3210
- enabled: true
3372
+ enabled: true,
3373
+ ...void 0 !== initArgs.scrcpyVideoBitRate ? {
3374
+ videoBitRate: initArgs.scrcpyVideoBitRate
3375
+ } : {}
3211
3376
  }
3212
3377
  } : {},
3213
3378
  ...reportOptions ?? {}
@@ -3265,6 +3430,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
3265
3430
  shape: {
3266
3431
  deviceId: core_namespaceObject.z.string().optional().describe('Android device ID (from adb devices)'),
3267
3432
  useScrcpy: core_namespaceObject.z.boolean().optional().describe('Enable scrcpy accelerated screenshots'),
3433
+ scrcpyVideoBitRate: core_namespaceObject.z.number().int().positive().optional().describe('scrcpy H.264 video bitrate in bits per second; providing this option also enables scrcpy. Default with --use-scrcpy: 100000000 (100 Mbps). For constrained remote links, start with 4000000 (4 Mbps).'),
3268
3434
  ...agent_behavior_init_args_namespaceObject.agentBehaviorInitArgShape
3269
3435
  },
3270
3436
  cli: {