@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/cli.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);
@@ -1039,6 +1100,9 @@ ${stdout ? truncateAdbShellStream(stdout, 'stdout') : EMPTY_ADB_SHELL_STDOUT}`;
1039
1100
  return obj;
1040
1101
  }
1041
1102
  const debugAdapter = (0, logger_.getDebug)('android:scrcpy-adapter');
1103
+ const warnAdapter = (0, logger_.getDebug)('android:scrcpy-adapter', {
1104
+ console: true
1105
+ });
1042
1106
  const SCRCPY_RETRY_COOLDOWN_MS = 5000;
1043
1107
  const DEFAULT_ADB_SERVER_ENDPOINT = {
1044
1108
  host: '127.0.0.1',
@@ -1092,10 +1156,11 @@ ${stdout ? truncateAdbShellStream(stdout, 'stdout') : EMPTY_ADB_SHELL_STDOUT}`;
1092
1156
  if (null === this.retryAfter || Date.now() >= this.retryAfter) return;
1093
1157
  throw new Error(`scrcpy retry is cooling down until ${new Date(this.retryAfter).toISOString()}. Last error: ${this.lastError}`);
1094
1158
  }
1095
- resolveConfig(deviceInfo) {
1159
+ resolveConfig(_deviceInfo) {
1096
1160
  if (this.resolvedConfig) return this.resolvedConfig;
1097
1161
  const config = this.scrcpyConfig;
1098
1162
  const maxSize = config?.maxSize ?? scrcpy_manager.ov.maxSize;
1163
+ if (!Number.isInteger(maxSize) || maxSize < 0) throw new Error(`Invalid scrcpyConfig.maxSize: expected a non-negative integer, received ${maxSize}`);
1099
1164
  const videoBitRate = config?.videoBitRate ?? scrcpy_manager.ov.videoBitRate;
1100
1165
  this.resolvedConfig = {
1101
1166
  enabled: this.isConfigured(),
@@ -1105,31 +1170,68 @@ ${stdout ? truncateAdbShellStream(stdout, 'stdout') : EMPTY_ADB_SHELL_STDOUT}`;
1105
1170
  };
1106
1171
  return this.resolvedConfig;
1107
1172
  }
1173
+ async prepareFallbackScreenshot(screenshotBase64) {
1174
+ if (!this.isConfigured()) return screenshotBase64;
1175
+ const { maxSize } = this.resolveConfig();
1176
+ if (0 === maxSize) return screenshotBase64;
1177
+ const sourceSize = await (0, img_namespaceObject.imageInfoOfBase64)(screenshotBase64);
1178
+ const largestDimension = Math.max(sourceSize.width, sourceSize.height);
1179
+ if (largestDimension <= maxSize) return screenshotBase64;
1180
+ const scale = maxSize / largestDimension;
1181
+ const targetSize = {
1182
+ width: Math.max(1, Math.round(sourceSize.width * scale)),
1183
+ height: Math.max(1, Math.round(sourceSize.height * scale))
1184
+ };
1185
+ debugAdapter(`Applying scrcpy maxSize to fallback screenshot: ${sourceSize.width}x${sourceSize.height} -> ${targetSize.width}x${targetSize.height}`);
1186
+ return (0, img_namespaceObject.resizeBase64ImageToJpeg)(screenshotBase64, {
1187
+ sourceSize,
1188
+ targetSize
1189
+ });
1190
+ }
1108
1191
  async ensureManager(deviceInfo) {
1109
1192
  if (this.manager) return this.manager;
1193
+ if (this.managerPromise) return this.managerPromise;
1110
1194
  debugAdapter('Initializing Scrcpy manager...');
1195
+ const generation = this.lifecycleGeneration;
1196
+ const managerPromise = (async ()=>{
1197
+ let adb = null;
1198
+ let manager = null;
1199
+ try {
1200
+ const { Adb, AdbServerClient } = await import("@yume-chan/adb");
1201
+ const { AdbServerNodeTcpConnector } = await import("@yume-chan/adb-server-node-tcp");
1202
+ const { ScrcpyScreenshotManager: ScrcpyManager } = await Promise.resolve().then(__webpack_require__.bind(__webpack_require__, "./src/scrcpy-manager.ts"));
1203
+ const adbServerEndpoint = await this.resolveAdbServerEndpoint();
1204
+ const adbClient = new AdbServerClient(new AdbServerNodeTcpConnector(adbServerEndpoint));
1205
+ adb = new Adb(await adbClient.createTransport({
1206
+ serial: this.deviceId
1207
+ }));
1208
+ const config = this.resolveConfig(deviceInfo);
1209
+ manager = new ScrcpyManager(adb, {
1210
+ maxSize: config.maxSize,
1211
+ videoBitRate: config.videoBitRate,
1212
+ idleTimeoutMs: config.idleTimeoutMs
1213
+ });
1214
+ await manager.validateEnvironment();
1215
+ if (generation !== this.lifecycleGeneration) throw new Error('Scrcpy manager initialization was superseded by device cleanup');
1216
+ this.manager = manager;
1217
+ debugAdapter('Scrcpy manager initialized');
1218
+ return manager;
1219
+ } catch (error) {
1220
+ try {
1221
+ if (manager) await manager.dispose();
1222
+ else await adb?.close();
1223
+ } catch (cleanupError) {
1224
+ debugAdapter(`Failed to clean up Scrcpy manager initialization: ${cleanupError}`);
1225
+ }
1226
+ debugAdapter(`Failed to initialize Scrcpy manager: ${error}`);
1227
+ throw new Error(`Failed to initialize Scrcpy for device ${this.deviceId}. Ensure ADB server is running and device is connected. Error: ${error}`);
1228
+ }
1229
+ })();
1230
+ this.managerPromise = managerPromise;
1111
1231
  try {
1112
- const { Adb, AdbServerClient } = await import("@yume-chan/adb");
1113
- const { AdbServerNodeTcpConnector } = await import("@yume-chan/adb-server-node-tcp");
1114
- const { ScrcpyScreenshotManager: ScrcpyManager } = await Promise.resolve().then(__webpack_require__.bind(__webpack_require__, "./src/scrcpy-manager.ts"));
1115
- const adbServerEndpoint = await this.resolveAdbServerEndpoint();
1116
- const adbClient = new AdbServerClient(new AdbServerNodeTcpConnector(adbServerEndpoint));
1117
- const adb = new Adb(await adbClient.createTransport({
1118
- serial: this.deviceId
1119
- }));
1120
- const config = this.resolveConfig(deviceInfo);
1121
- const manager = new ScrcpyManager(adb, {
1122
- maxSize: config.maxSize,
1123
- videoBitRate: config.videoBitRate,
1124
- idleTimeoutMs: config.idleTimeoutMs
1125
- });
1126
- await manager.validateEnvironment();
1127
- this.manager = manager;
1128
- debugAdapter('Scrcpy manager initialized');
1129
- return this.manager;
1130
- } catch (error) {
1131
- debugAdapter(`Failed to initialize Scrcpy manager: ${error}`);
1132
- throw new Error(`Failed to initialize Scrcpy for device ${this.deviceId}. Ensure ADB server is running and device is connected. Error: ${error}`);
1232
+ return await managerPromise;
1233
+ } finally{
1234
+ if (this.managerPromise === managerPromise) this.managerPromise = null;
1133
1235
  }
1134
1236
  }
1135
1237
  async screenshotBase64(deviceInfo) {
@@ -1141,16 +1243,54 @@ ${stdout ? truncateAdbShellStream(stdout, 'stdout') : EMPTY_ADB_SHELL_STDOUT}`;
1141
1243
  await this.applyPendingActionBarrier(manager);
1142
1244
  const screenshotBuffer = await manager.getScreenshotJpeg();
1143
1245
  this.clearFailure();
1144
- return (0, img_namespaceObject.createImgBase64ByFormat)('jpeg', screenshotBuffer.toString('base64'));
1246
+ return this.jpegBufferToBase64(screenshotBuffer);
1145
1247
  } catch (error) {
1146
- if ((0, scrcpy_manager.GJ)(error)) {
1147
- this.markFreshnessRecoveryPending(manager, error);
1248
+ if (!(0, scrcpy_manager.GJ)(error)) {
1249
+ this.recordFailure(error);
1148
1250
  throw error;
1149
1251
  }
1150
- this.recordFailure(error);
1151
- throw error;
1252
+ this.markFreshnessRecoveryPending(error);
1253
+ debugAdapter(`Scrcpy freshness target was unavailable; restarting the stream once before ADB fallback: ${error}`);
1254
+ try {
1255
+ const screenshotBuffer = await this.restartAndCaptureOnce(deviceInfo);
1256
+ manager = this.manager;
1257
+ if (!manager) throw new Error('Scrcpy manager disappeared after freshness recovery');
1258
+ this.attachKeyframeListeners(manager);
1259
+ this.clearFailure();
1260
+ debugAdapter('Scrcpy screenshot recovered on a new stream epoch');
1261
+ return this.jpegBufferToBase64(screenshotBuffer);
1262
+ } catch (retryError) {
1263
+ if ((0, scrcpy_manager.GJ)(retryError)) this.markFreshnessRecoveryPending(retryError);
1264
+ else this.recordFailure(retryError);
1265
+ this.warnFreshnessFallback(error, retryError);
1266
+ throw retryError;
1267
+ }
1152
1268
  }
1153
1269
  }
1270
+ async restartAndCaptureOnce(deviceInfo) {
1271
+ if (this.freshnessRestartPromise) return this.freshnessRestartPromise;
1272
+ const generation = this.lifecycleGeneration;
1273
+ const restartPromise = (async ()=>{
1274
+ const manager = await this.ensureManager(deviceInfo);
1275
+ await manager.ensureConnected();
1276
+ await this.applyPendingActionBarrier(manager);
1277
+ if (generation !== this.lifecycleGeneration) throw new Error('Scrcpy freshness restart was cancelled by cleanup');
1278
+ return manager.getScreenshotJpeg();
1279
+ })();
1280
+ this.freshnessRestartPromise = restartPromise;
1281
+ try {
1282
+ return await restartPromise;
1283
+ } finally{
1284
+ if (this.freshnessRestartPromise === restartPromise) this.freshnessRestartPromise = null;
1285
+ }
1286
+ }
1287
+ jpegBufferToBase64(screenshotBuffer) {
1288
+ return (0, img_namespaceObject.createImgBase64ByFormat)('jpeg', screenshotBuffer.toString('base64'));
1289
+ }
1290
+ warnFreshnessFallback(firstError, retryError) {
1291
+ const retryDiagnostic = (0, scrcpy_manager.GJ)(retryError) ? retryError.diagnosticMessage : void 0;
1292
+ warnAdapter(retryDiagnostic ?? (firstError.diagnosticMessage ? `${firstError.diagnosticMessage}\nScrcpy stream restart error: ${retryError}` : void 0) ?? `Scrcpy stream restart failed; falling back to ADB screenshot. Error: ${retryError}`);
1293
+ }
1154
1294
  async subscribeKeyframes(deviceInfo, listener) {
1155
1295
  this.ensureRetryReady();
1156
1296
  this.keyframeListeners.add(listener);
@@ -1183,17 +1323,26 @@ ${stdout ? truncateAdbShellStream(stdout, 'stdout') : EMPTY_ADB_SHELL_STDOUT}`;
1183
1323
  this.keyframeUnsubscribers.clear();
1184
1324
  for (const listener of this.keyframeListeners)this.attachKeyframeListener(manager, listener);
1185
1325
  }
1186
- markFreshnessRecoveryPending(manager, error) {
1326
+ markFreshnessRecoveryPending(error) {
1187
1327
  this.lastError = error.message;
1188
1328
  this.retryAfter = null;
1189
1329
  this.freshnessRecoveryPending = true;
1190
1330
  this.keyframeUnsubscribers.clear();
1191
- if (manager && this.manager === manager) this.manager = null;
1192
1331
  }
1193
1332
  async applyPendingActionBarrier(manager) {
1194
- if (!this.pendingActionBarrier) return;
1195
- await manager.setFreshnessBarrier('completed input action while scrcpy was unavailable');
1196
- this.pendingActionBarrier = false;
1333
+ const actionCompletedAtHostUs = this.pendingActionBarrierAtHostUs;
1334
+ if (null === actionCompletedAtHostUs) return;
1335
+ await manager.setFreshnessBarrier('completed input action while scrcpy was unavailable', {
1336
+ allowOverAgeForNextCapture: true,
1337
+ hostMonotonicUs: actionCompletedAtHostUs
1338
+ });
1339
+ if (this.pendingActionBarrierAtHostUs === actionCompletedAtHostUs) this.pendingActionBarrierAtHostUs = null;
1340
+ }
1341
+ monotonicTimeUs() {
1342
+ return process.hrtime.bigint() / 1000n;
1343
+ }
1344
+ deferActionBarrier(actionCompletedAtHostUs) {
1345
+ if (null === this.pendingActionBarrierAtHostUs || actionCompletedAtHostUs > this.pendingActionBarrierAtHostUs) this.pendingActionBarrierAtHostUs = actionCompletedAtHostUs;
1197
1346
  }
1198
1347
  recoverAfterAdbScreenshot(deviceInfo) {
1199
1348
  if (!this.freshnessRecoveryPending || this.recoveryPromise) return;
@@ -1206,7 +1355,7 @@ ${stdout ? truncateAdbShellStream(stdout, 'stdout') : EMPTY_ADB_SHELL_STDOUT}`;
1206
1355
  await this.applyPendingActionBarrier(manager);
1207
1356
  await manager.prepareFreshFrame();
1208
1357
  if (generation !== this.lifecycleGeneration) {
1209
- await manager.disconnect();
1358
+ await manager.dispose();
1210
1359
  if (this.manager === manager) this.manager = null;
1211
1360
  return;
1212
1361
  }
@@ -1215,7 +1364,7 @@ ${stdout ? truncateAdbShellStream(stdout, 'stdout') : EMPTY_ADB_SHELL_STDOUT}`;
1215
1364
  debugAdapter('Scrcpy freshness recovery completed in background');
1216
1365
  } catch (error) {
1217
1366
  if (manager) {
1218
- await manager.disconnect();
1367
+ await manager.dispose();
1219
1368
  if (this.manager === manager) this.manager = null;
1220
1369
  }
1221
1370
  this.freshnessRecoveryPending = false;
@@ -1230,21 +1379,22 @@ ${stdout ? truncateAdbShellStream(stdout, 'stdout') : EMPTY_ADB_SHELL_STDOUT}`;
1230
1379
  });
1231
1380
  }
1232
1381
  async markActionBarrier() {
1382
+ const actionCompletedAtHostUs = this.monotonicTimeUs();
1233
1383
  const manager = this.manager;
1234
- if (!manager?.isConnected()) {
1235
- this.pendingActionBarrier = true;
1236
- return;
1237
- }
1384
+ if (!manager?.isConnected()) return void this.deferActionBarrier(actionCompletedAtHostUs);
1238
1385
  try {
1239
- await manager.setFreshnessBarrier('completed input action');
1240
- this.pendingActionBarrier = false;
1386
+ await manager.setFreshnessBarrier('completed input action', {
1387
+ allowOverAgeForNextCapture: true,
1388
+ hostMonotonicUs: actionCompletedAtHostUs
1389
+ });
1390
+ if (null !== this.pendingActionBarrierAtHostUs && this.pendingActionBarrierAtHostUs <= actionCompletedAtHostUs) this.pendingActionBarrierAtHostUs = null;
1241
1391
  this.clearFailure();
1242
1392
  } catch (error) {
1243
- this.pendingActionBarrier = true;
1393
+ this.deferActionBarrier(actionCompletedAtHostUs);
1244
1394
  this.recordFailure(error);
1245
1395
  debugAdapter(`Unable to mark scrcpy action barrier; disabling this stream: ${error}`);
1246
1396
  try {
1247
- await manager.disconnect();
1397
+ await manager.dispose();
1248
1398
  } catch (disconnectError) {
1249
1399
  debugAdapter(`Error disconnecting scrcpy after barrier failure: ${disconnectError}`);
1250
1400
  } finally{
@@ -1277,14 +1427,16 @@ ${stdout ? truncateAdbShellStream(stdout, 'stdout') : EMPTY_ADB_SHELL_STDOUT}`;
1277
1427
  async disconnect() {
1278
1428
  this.lifecycleGeneration += 1;
1279
1429
  this.freshnessRecoveryPending = false;
1280
- this.pendingActionBarrier = false;
1430
+ this.pendingActionBarrierAtHostUs = null;
1281
1431
  for (const unsubscribe of this.keyframeUnsubscribers.values())unsubscribe();
1282
1432
  this.keyframeUnsubscribers.clear();
1283
1433
  this.keyframeListeners.clear();
1284
1434
  if (this.recoveryPromise) await this.recoveryPromise.catch(()=>{});
1435
+ if (this.freshnessRestartPromise) await this.freshnessRestartPromise.catch(()=>{});
1436
+ if (this.managerPromise) await this.managerPromise.catch(()=>{});
1285
1437
  if (this.manager) {
1286
1438
  try {
1287
- await this.manager.disconnect();
1439
+ await this.manager.dispose();
1288
1440
  } catch (error) {
1289
1441
  debugAdapter(`Error disconnecting scrcpy: ${error}`);
1290
1442
  }
@@ -1298,26 +1450,30 @@ ${stdout ? truncateAdbShellStream(stdout, 'stdout') : EMPTY_ADB_SHELL_STDOUT}`;
1298
1450
  _define_property(this, "scrcpyConfig", void 0);
1299
1451
  _define_property(this, "resolveAdbServerEndpoint", void 0);
1300
1452
  _define_property(this, "manager", void 0);
1453
+ _define_property(this, "managerPromise", void 0);
1301
1454
  _define_property(this, "resolvedConfig", void 0);
1302
1455
  _define_property(this, "lastError", void 0);
1303
1456
  _define_property(this, "retryAfter", void 0);
1304
1457
  _define_property(this, "freshnessRecoveryPending", void 0);
1458
+ _define_property(this, "freshnessRestartPromise", void 0);
1305
1459
  _define_property(this, "recoveryPromise", void 0);
1306
1460
  _define_property(this, "lifecycleGeneration", void 0);
1307
- _define_property(this, "pendingActionBarrier", void 0);
1461
+ _define_property(this, "pendingActionBarrierAtHostUs", void 0);
1308
1462
  _define_property(this, "keyframeListeners", void 0);
1309
1463
  _define_property(this, "keyframeUnsubscribers", void 0);
1310
1464
  this.deviceId = deviceId;
1311
1465
  this.scrcpyConfig = scrcpyConfig;
1312
1466
  this.resolveAdbServerEndpoint = resolveAdbServerEndpoint;
1313
1467
  this.manager = null;
1468
+ this.managerPromise = null;
1314
1469
  this.resolvedConfig = null;
1315
1470
  this.lastError = null;
1316
1471
  this.retryAfter = null;
1317
1472
  this.freshnessRecoveryPending = false;
1473
+ this.freshnessRestartPromise = null;
1318
1474
  this.recoveryPromise = null;
1319
1475
  this.lifecycleGeneration = 0;
1320
- this.pendingActionBarrier = false;
1476
+ this.pendingActionBarrierAtHostUs = null;
1321
1477
  this.keyframeListeners = new Set();
1322
1478
  this.keyframeUnsubscribers = new Map();
1323
1479
  }
@@ -2168,9 +2324,9 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
2168
2324
  }
2169
2325
  async screenshotBase64() {
2170
2326
  debugDevice('screenshotBase64 begin');
2171
- const screenshotStrategy = this.options?.screenshotStrategy || env_namespaceObject.globalConfigManager.getEnvConfigValue(env_namespaceObject.MIDSCENE_ANDROID_SCREENSHOT_STRATEGY) || SCREENSHOT_STRATEGY_AUTO;
2172
- if (screenshotStrategy === SCREENSHOT_STRATEGY_ALWAYS_YADB) return this.screenshotBase64ViaYadb();
2173
2327
  const adapter = this.getScrcpyAdapter();
2328
+ const screenshotStrategy = this.options?.screenshotStrategy || env_namespaceObject.globalConfigManager.getEnvConfigValue(env_namespaceObject.MIDSCENE_ANDROID_SCREENSHOT_STRATEGY) || SCREENSHOT_STRATEGY_AUTO;
2329
+ if (screenshotStrategy === SCREENSHOT_STRATEGY_ALWAYS_YADB) return adapter.prepareFallbackScreenshot(await this.screenshotBase64ViaYadb());
2174
2330
  let scrcpyDeviceInfo = null;
2175
2331
  if (adapter.isEnabled()) try {
2176
2332
  debugDevice('Attempting scrcpy screenshot...');
@@ -2179,7 +2335,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
2179
2335
  debugDevice('screenshotBase64 end (scrcpy mode)');
2180
2336
  return result;
2181
2337
  } catch (error) {
2182
- warnDevice(`Scrcpy screenshot failed, falling back to standard ADB method. This may be caused by transport backlog. ${scrcpy_manager.Xr}\nError: ${error}`);
2338
+ warnDevice(`Scrcpy screenshot failed, falling back to standard ADB method.\nError: ${error}`);
2183
2339
  }
2184
2340
  const adb = await this.getAdb();
2185
2341
  let screenshotBuffer;
@@ -2221,14 +2377,14 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
2221
2377
  });
2222
2378
  debugDevice('screenshotBase64 end (fallback)');
2223
2379
  if (scrcpyDeviceInfo) adapter.recoverAfterAdbScreenshot(scrcpyDeviceInfo);
2224
- return result;
2380
+ return adapter.prepareFallbackScreenshot(result);
2225
2381
  }
2226
2382
  if (!screenshotBuffer) throw new Error('Failed to capture screenshot: all methods failed');
2227
2383
  debugDevice('Converting to base64');
2228
2384
  const result = (0, img_namespaceObject.createImgBase64ByFormat)('png', screenshotBuffer.toString('base64'));
2229
2385
  debugDevice('screenshotBase64 end');
2230
2386
  if (scrcpyDeviceInfo) adapter.recoverAfterAdbScreenshot(scrcpyDeviceInfo);
2231
- return result;
2387
+ return adapter.prepareFallbackScreenshot(result);
2232
2388
  }
2233
2389
  async captureScreenshotBase64FromDeviceFile(label, capture) {
2234
2390
  const adb = await this.getAdb();
@@ -3082,12 +3238,18 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
3082
3238
  const debug = (0, logger_.getDebug)('agent-tools:android');
3083
3239
  function adaptAndroidInitArgs(extracted) {
3084
3240
  if (!extracted) return;
3241
+ const scrcpyVideoBitRate = 'number' == typeof extracted.scrcpyVideoBitRate ? extracted.scrcpyVideoBitRate : void 0;
3242
+ const requestedUseScrcpy = 'boolean' == typeof extracted.useScrcpy ? extracted.useScrcpy : void 0;
3243
+ const useScrcpy = void 0 !== scrcpyVideoBitRate ? true : requestedUseScrcpy;
3085
3244
  const initArgs = {
3086
3245
  ...'string' == typeof extracted.deviceId ? {
3087
3246
  deviceId: extracted.deviceId
3088
3247
  } : {},
3089
- ...'boolean' == typeof extracted.useScrcpy ? {
3090
- useScrcpy: extracted.useScrcpy
3248
+ ...void 0 !== useScrcpy ? {
3249
+ useScrcpy
3250
+ } : {},
3251
+ ...void 0 !== scrcpyVideoBitRate ? {
3252
+ scrcpyVideoBitRate
3091
3253
  } : {},
3092
3254
  ...(0, agent_behavior_init_args_namespaceObject.extractAgentBehaviorInitArgs)(extracted) ?? {}
3093
3255
  };
@@ -3119,7 +3281,10 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
3119
3281
  ...(0, agent_behavior_init_args_namespaceObject.extractAgentBehaviorInitArgs)(initArgs) ?? {},
3120
3282
  ...initArgs?.useScrcpy ? {
3121
3283
  scrcpyConfig: {
3122
- enabled: true
3284
+ enabled: true,
3285
+ ...void 0 !== initArgs.scrcpyVideoBitRate ? {
3286
+ videoBitRate: initArgs.scrcpyVideoBitRate
3287
+ } : {}
3123
3288
  }
3124
3289
  } : {},
3125
3290
  ...reportOptions ?? {}
@@ -3177,6 +3342,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
3177
3342
  shape: {
3178
3343
  deviceId: core_namespaceObject.z.string().optional().describe('Android device ID (from adb devices)'),
3179
3344
  useScrcpy: core_namespaceObject.z.boolean().optional().describe('Enable scrcpy accelerated screenshots'),
3345
+ 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).'),
3180
3346
  ...agent_behavior_init_args_namespaceObject.agentBehaviorInitArgShape
3181
3347
  },
3182
3348
  cli: {
@@ -3189,7 +3355,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
3189
3355
  const tools = new AndroidMidsceneTools();
3190
3356
  (0, cli_namespaceObject.runToolsCLI)(tools, 'midscene-android', {
3191
3357
  stripPrefix: 'android_',
3192
- version: "1.12.0",
3358
+ version: "1.12.1-beta-20260824081858.0",
3193
3359
  extraCommands: (0, core_namespaceObject.createReportCliCommands)()
3194
3360
  }).catch((e)=>{
3195
3361
  process.exit((0, cli_namespaceObject.reportCLIError)(e));