@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/es/cli.mjs +262 -96
- package/dist/es/index.mjs +261 -95
- package/dist/lib/cli.js +261 -95
- package/dist/lib/index.js +260 -94
- package/dist/types/index.d.ts +61 -15
- package/package.json +6 -6
package/dist/es/cli.mjs
CHANGED
|
@@ -13,7 +13,7 @@ import { execFile } from "node:child_process";
|
|
|
13
13
|
import { createDefaultMobileActions, defineAction } from "@midscene/core/device";
|
|
14
14
|
import { getTmpFile, sleep } from "@midscene/core/utils";
|
|
15
15
|
import { MIDSCENE_ADB_PATH, MIDSCENE_ADB_REMOTE_HOST, MIDSCENE_ADB_REMOTE_PORT, MIDSCENE_ANDROID_IME_STRATEGY, MIDSCENE_ANDROID_SCREENSHOT_STRATEGY, globalConfigManager } from "@midscene/shared/env";
|
|
16
|
-
import { createImgBase64ByFormat, validateScreenshotBuffer } from "@midscene/shared/img";
|
|
16
|
+
import { createImgBase64ByFormat, imageInfoOfBase64, resizeBase64ImageToJpeg, validateScreenshotBuffer } from "@midscene/shared/img";
|
|
17
17
|
import { ADB, getSdkRootFromEnv } from "appium-adb";
|
|
18
18
|
var __webpack_modules__ = {
|
|
19
19
|
"./src/resource-path.ts" (__unused_rspack_module, __webpack_exports__, __webpack_require__) {
|
|
@@ -30,7 +30,6 @@ var __webpack_modules__ = {
|
|
|
30
30
|
__webpack_require__.d(__webpack_exports__, {
|
|
31
31
|
GJ: ()=>isScrcpyFreshFrameUnavailableError,
|
|
32
32
|
ScrcpyScreenshotManager: ()=>ScrcpyScreenshotManager,
|
|
33
|
-
Xr: ()=>SCRCPY_VIDEO_BIT_RATE_NETWORK_HINT,
|
|
34
33
|
ov: ()=>DEFAULT_SCRCPY_CONFIG
|
|
35
34
|
});
|
|
36
35
|
var node_fs__rspack_import_0 = __webpack_require__("node:fs");
|
|
@@ -64,7 +63,6 @@ var __webpack_modules__ = {
|
|
|
64
63
|
const FRESH_FRAME_TIMEOUT_MS = 300;
|
|
65
64
|
const KEYFRAME_POLL_INTERVAL_MS = 200;
|
|
66
65
|
const MAX_SCAN_BYTES = 1000;
|
|
67
|
-
const CONNECTION_WAIT_MS = 1000;
|
|
68
66
|
const MAX_SERVER_OUTPUT_LINES = 100;
|
|
69
67
|
const SERVER_OUTPUT_DRAIN_TIMEOUT_MS = 500;
|
|
70
68
|
const MAX_FRAME_AGE_US = 500000n;
|
|
@@ -74,7 +72,12 @@ var __webpack_modules__ = {
|
|
|
74
72
|
'dumpsys',
|
|
75
73
|
'power'
|
|
76
74
|
];
|
|
77
|
-
const SCRCPY_VIDEO_BIT_RATE_NETWORK_HINT = 'The appropriate scrcpy video bitrate depends on network conditions. For constrained remote links,
|
|
75
|
+
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.';
|
|
76
|
+
const CONSTRAINED_LINK_STARTING_VIDEO_BIT_RATE = 4000000;
|
|
77
|
+
function getScrcpyVideoBitRateNetworkHint(currentVideoBitRate) {
|
|
78
|
+
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.';
|
|
79
|
+
return SCRCPY_VIDEO_BIT_RATE_NETWORK_HINT;
|
|
80
|
+
}
|
|
78
81
|
const BUSY_LOOP_WINDOW_MS = 1000;
|
|
79
82
|
const BUSY_LOOP_MAX_READS = 500;
|
|
80
83
|
const BUSY_LOOP_COOLDOWN_MS = 50;
|
|
@@ -88,8 +91,9 @@ var __webpack_modules__ = {
|
|
|
88
91
|
const SCRCPY_FRESH_FRAME_UNAVAILABLE_ERROR_CODE = 'ERR_SCRCPY_FRESH_FRAME_UNAVAILABLE';
|
|
89
92
|
class ScrcpyFreshFrameUnavailableError extends Error {
|
|
90
93
|
constructor(message, options){
|
|
91
|
-
super(message, options), _define_property(this, "code", SCRCPY_FRESH_FRAME_UNAVAILABLE_ERROR_CODE);
|
|
94
|
+
super(message, options), _define_property(this, "code", SCRCPY_FRESH_FRAME_UNAVAILABLE_ERROR_CODE), _define_property(this, "diagnosticMessage", void 0);
|
|
92
95
|
this.name = 'ScrcpyFreshFrameUnavailableError';
|
|
96
|
+
this.diagnosticMessage = options?.diagnosticMessage;
|
|
93
97
|
}
|
|
94
98
|
}
|
|
95
99
|
function isScrcpyFreshFrameUnavailableError(error) {
|
|
@@ -122,26 +126,30 @@ var __webpack_modules__ = {
|
|
|
122
126
|
await this.ensureFfmpegAvailable();
|
|
123
127
|
}
|
|
124
128
|
async ensureConnected() {
|
|
129
|
+
if (this.disposed) throw new Error('Scrcpy manager has been disposed');
|
|
125
130
|
if (this.scrcpyClient && this.videoStream) {
|
|
126
131
|
debugScrcpy('Scrcpy already connected');
|
|
127
132
|
await this.ensureFrameClockCalibration();
|
|
128
133
|
this.resetIdleTimer();
|
|
129
134
|
return;
|
|
130
135
|
}
|
|
131
|
-
if (this.
|
|
132
|
-
debugScrcpy('Connection already in progress,
|
|
133
|
-
await
|
|
134
|
-
|
|
135
|
-
await this.ensureFrameClockCalibration();
|
|
136
|
-
this.resetIdleTimer();
|
|
137
|
-
return;
|
|
138
|
-
}
|
|
139
|
-
throw new Error('Scrcpy connection failed: another connection attempt did not complete in time');
|
|
136
|
+
if (this.connectionPromise) {
|
|
137
|
+
debugScrcpy('Connection already in progress, sharing the same attempt...');
|
|
138
|
+
await this.connectionPromise;
|
|
139
|
+
return;
|
|
140
140
|
}
|
|
141
|
+
const connectionPromise = this.connectScrcpy();
|
|
142
|
+
this.connectionPromise = connectionPromise;
|
|
143
|
+
try {
|
|
144
|
+
await connectionPromise;
|
|
145
|
+
} finally{
|
|
146
|
+
if (this.connectionPromise === connectionPromise) this.connectionPromise = null;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
async connectScrcpy() {
|
|
141
150
|
const serverOutput = [];
|
|
142
151
|
let serverOutputTask = null;
|
|
143
152
|
try {
|
|
144
|
-
this.isConnecting = true;
|
|
145
153
|
debugScrcpy('Starting scrcpy connection...');
|
|
146
154
|
const { AdbScrcpyClient } = await import("@yume-chan/adb-scrcpy");
|
|
147
155
|
const { ReadableStream } = await import("@yume-chan/stream-extra");
|
|
@@ -161,6 +169,8 @@ var __webpack_modules__ = {
|
|
|
161
169
|
height
|
|
162
170
|
};
|
|
163
171
|
await this.ensureFrameClockCalibration();
|
|
172
|
+
this.streamBaselineFramePending = true;
|
|
173
|
+
this.streamBaselineFrameDeadlineAt = Date.now() + MAX_KEYFRAME_WAIT_MS;
|
|
164
174
|
this.startFrameConsumer();
|
|
165
175
|
this.resetIdleTimer();
|
|
166
176
|
this.isInitialized = true;
|
|
@@ -173,8 +183,6 @@ var __webpack_modules__ = {
|
|
|
173
183
|
new Promise((resolve)=>setTimeout(resolve, SERVER_OUTPUT_DRAIN_TIMEOUT_MS))
|
|
174
184
|
]);
|
|
175
185
|
throw this.createConnectionError(error, serverOutput);
|
|
176
|
-
} finally{
|
|
177
|
-
this.isConnecting = false;
|
|
178
186
|
}
|
|
179
187
|
}
|
|
180
188
|
async createScrcpyOptions() {
|
|
@@ -359,24 +367,30 @@ var __webpack_modules__ = {
|
|
|
359
367
|
if (this.deviceClockCalibrationPromise === calibrationPromise) this.deviceClockCalibrationPromise = null;
|
|
360
368
|
}
|
|
361
369
|
}
|
|
362
|
-
async setFreshnessBarrier(reason) {
|
|
370
|
+
async setFreshnessBarrier(reason, options = {}) {
|
|
371
|
+
const cachedFrame = this.getCachedKeyframeCandidate();
|
|
363
372
|
const generation = ++this.frameFreshnessBarrierGeneration;
|
|
364
373
|
this.frameFreshnessBarrierPending = true;
|
|
374
|
+
this.frameFreshnessBarrierAllowsOverAgeForNextCapture = false;
|
|
365
375
|
this.clearFrameCache();
|
|
366
376
|
try {
|
|
367
377
|
const calibration = this.deviceClockCalibration;
|
|
368
378
|
if (!calibration) throw new Error('Scrcpy frame clock is not calibrated for the current stream epoch');
|
|
369
|
-
const hostMonotonicUs = this.monotonicTimeUs();
|
|
379
|
+
const hostMonotonicUs = options.hostMonotonicUs ?? this.monotonicTimeUs();
|
|
370
380
|
const estimatedDeviceNowUs = this.estimateDeviceTimeUs(hostMonotonicUs, calibration);
|
|
371
381
|
const conservativeDeviceNowUs = estimatedDeviceNowUs + this.getCalibrationUncertaintyUs(calibration);
|
|
372
382
|
const barrierPtsUs = (conservativeDeviceNowUs / 1000n + 1n) * 1000n;
|
|
373
383
|
if (generation !== this.frameFreshnessBarrierGeneration) return this.frameFreshnessBarrierPtsUs ?? barrierPtsUs;
|
|
374
384
|
this.frameFreshnessBarrierPtsUs = null === this.frameFreshnessBarrierPtsUs || barrierPtsUs > this.frameFreshnessBarrierPtsUs ? barrierPtsUs : this.frameFreshnessBarrierPtsUs;
|
|
375
385
|
this.frameFreshnessBarrierReason = reason;
|
|
386
|
+
this.frameFreshnessBarrierAllowsOverAgeForNextCapture = options.allowOverAgeForNextCapture ?? false;
|
|
376
387
|
this.frameFreshnessBarrierPending = false;
|
|
377
388
|
this.frameFreshnessError = null;
|
|
378
389
|
this.lastFramePtsUs = null;
|
|
379
|
-
this.
|
|
390
|
+
if (cachedFrame?.ptsUs !== void 0 && cachedFrame.ptsUs >= this.frameFreshnessBarrierPtsUs) {
|
|
391
|
+
this.restoreFrameCache(cachedFrame);
|
|
392
|
+
debugScrcpy(`Preserved cached frame PTS ${cachedFrame.ptsUs}µs because it crosses the newly armed ${reason} barrier`);
|
|
393
|
+
} else this.clearFrameCache();
|
|
380
394
|
debugScrcpy(`Armed frame freshness barrier at PTS ${this.frameFreshnessBarrierPtsUs}µs (${reason}, projected from stream-epoch clock anchor, uncertainty<=${Number(this.getCalibrationUncertaintyUs(calibration)) / 1000}ms)`);
|
|
381
395
|
return this.frameFreshnessBarrierPtsUs;
|
|
382
396
|
} catch (error) {
|
|
@@ -397,6 +411,7 @@ var __webpack_modules__ = {
|
|
|
397
411
|
this.frameFreshnessBarrierPending = true;
|
|
398
412
|
this.frameFreshnessBarrierPtsUs = null;
|
|
399
413
|
this.frameFreshnessBarrierReason = null;
|
|
414
|
+
this.frameFreshnessBarrierAllowsOverAgeForNextCapture = false;
|
|
400
415
|
this.deviceClockCalibration = null;
|
|
401
416
|
this.clearFrameCache();
|
|
402
417
|
this.warnFrameFreshness();
|
|
@@ -418,7 +433,7 @@ var __webpack_modules__ = {
|
|
|
418
433
|
const behindBarrierUs = this.frameFreshnessBarrierPtsUs - packetPtsUs;
|
|
419
434
|
this.frameFreshnessError = new Error(`Scrcpy frame predates the ${this.frameFreshnessBarrierReason ?? 'active'} freshness barrier by ${Number(behindBarrierUs) / 1000}ms; refusing to use it`);
|
|
420
435
|
this.clearFrameCache();
|
|
421
|
-
this.
|
|
436
|
+
debugScrcpy(this.frameFreshnessError.message);
|
|
422
437
|
return false;
|
|
423
438
|
}
|
|
424
439
|
estimateFrameAgeUs(packetPtsUs, hostMonotonicUs = this.monotonicTimeUs()) {
|
|
@@ -468,13 +483,18 @@ var __webpack_modules__ = {
|
|
|
468
483
|
this.lastFrameFreshnessWarningAt = now;
|
|
469
484
|
}
|
|
470
485
|
}
|
|
471
|
-
|
|
472
|
-
const now = Date.now();
|
|
473
|
-
if (now - this.lastTransportBacklogWarningAt < TRANSPORT_BACKLOG_WARN_INTERVAL_MS) return;
|
|
486
|
+
transportBacklogMessage(error) {
|
|
474
487
|
const cause = this.frameFreshnessError ?? error;
|
|
475
488
|
const causeMessage = cause instanceof Error ? cause.message : String(cause);
|
|
489
|
+
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}`;
|
|
476
490
|
const currentBitRateMbps = this.options.videoBitRate / 1000000;
|
|
477
|
-
|
|
491
|
+
const networkHint = getScrcpyVideoBitRateNetworkHint(this.options.videoBitRate);
|
|
492
|
+
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}`;
|
|
493
|
+
}
|
|
494
|
+
warnTransportBacklog(error) {
|
|
495
|
+
const now = Date.now();
|
|
496
|
+
if (now - this.lastTransportBacklogWarningAt < TRANSPORT_BACKLOG_WARN_INTERVAL_MS) return;
|
|
497
|
+
warnScrcpy(this.transportBacklogMessage(error));
|
|
478
498
|
this.lastTransportBacklogWarningAt = now;
|
|
479
499
|
}
|
|
480
500
|
estimateFrameTiming(packetPtsUs, receivedAtUs) {
|
|
@@ -496,12 +516,21 @@ var __webpack_modules__ = {
|
|
|
496
516
|
this.lastRawKeyframePtsUs = void 0;
|
|
497
517
|
this.lastRawKeyframeEstimatedAgeMs = void 0;
|
|
498
518
|
}
|
|
519
|
+
restoreFrameCache(frame) {
|
|
520
|
+
this.lastRawKeyframe = frame.data;
|
|
521
|
+
this.lastRawKeyframeAt = frame.capturedAt;
|
|
522
|
+
this.lastRawKeyframePtsUs = frame.ptsUs;
|
|
523
|
+
this.lastRawKeyframeEstimatedAgeMs = frame.estimatedAgeMs;
|
|
524
|
+
}
|
|
499
525
|
monotonicTimeUs() {
|
|
500
526
|
return process.hrtime.bigint() / 1000n;
|
|
501
527
|
}
|
|
502
528
|
resetFrameFreshnessState() {
|
|
503
529
|
this.frameFreshnessBarrierPtsUs = null;
|
|
504
530
|
this.frameFreshnessBarrierReason = null;
|
|
531
|
+
this.frameFreshnessBarrierAllowsOverAgeForNextCapture = false;
|
|
532
|
+
this.streamBaselineFramePending = false;
|
|
533
|
+
this.streamBaselineFrameDeadlineAt = 0;
|
|
505
534
|
this.frameFreshnessBarrierPending = false;
|
|
506
535
|
this.frameFreshnessBarrierGeneration = 0;
|
|
507
536
|
this.deviceClockCalibration = null;
|
|
@@ -533,35 +562,52 @@ var __webpack_modules__ = {
|
|
|
533
562
|
capturedAt: this.lastRawKeyframeAt
|
|
534
563
|
};
|
|
535
564
|
}
|
|
565
|
+
canReuseFrameAcrossActionWait(frame) {
|
|
566
|
+
return this.frameFreshnessBarrierAllowsOverAgeForNextCapture && null !== this.frameFreshnessBarrierPtsUs && void 0 !== frame.ptsUs && frame.ptsUs >= this.frameFreshnessBarrierPtsUs;
|
|
567
|
+
}
|
|
568
|
+
canUseStreamBaseline(frame) {
|
|
569
|
+
return this.streamBaselineFramePending && !this.frameFreshnessBarrierPending && null === this.frameFreshnessBarrierPtsUs && Date.now() <= this.streamBaselineFrameDeadlineAt && void 0 !== frame.ptsUs;
|
|
570
|
+
}
|
|
571
|
+
consumeActionFreshnessBarrier(frame) {
|
|
572
|
+
if (!this.canReuseFrameAcrossActionWait(frame)) return;
|
|
573
|
+
this.frameFreshnessBarrierPtsUs = null;
|
|
574
|
+
this.frameFreshnessBarrierReason = null;
|
|
575
|
+
this.frameFreshnessBarrierAllowsOverAgeForNextCapture = false;
|
|
576
|
+
this.frameFreshnessError = null;
|
|
577
|
+
}
|
|
536
578
|
async decodeRawKeyframeToJpeg(frame) {
|
|
537
579
|
return this.decodeH264ToJpeg(Buffer.concat([
|
|
538
580
|
frame.header,
|
|
539
581
|
frame.data
|
|
540
582
|
]));
|
|
541
583
|
}
|
|
542
|
-
async waitForUsableKeyframe(timeoutMs) {
|
|
543
|
-
const deadline = Date.now() + timeoutMs;
|
|
544
|
-
let candidate = this.getCachedKeyframeCandidate();
|
|
545
|
-
while(true){
|
|
546
|
-
if (candidate && this.isFrameAgeAcceptable(candidate.ptsUs)) return candidate;
|
|
547
|
-
const remainingMs = deadline - Date.now();
|
|
548
|
-
if (remainingMs <= 0) throw new Error(`No fresh keyframe received within ${timeoutMs}ms`);
|
|
549
|
-
candidate = await this.waitForNextKeyframe(remainingMs);
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
584
|
async prepareFreshFrame() {
|
|
553
585
|
await this.ensureConnected();
|
|
554
586
|
await this.ensureFrameClockCalibration();
|
|
555
587
|
await this.waitForKeyframe();
|
|
556
|
-
await this.
|
|
588
|
+
await this.waitForPlanningFrame();
|
|
557
589
|
}
|
|
558
590
|
async waitForPlanningFrame() {
|
|
559
591
|
let planningBarrierArmed = false;
|
|
560
|
-
|
|
592
|
+
const planningStartedAt = Date.now();
|
|
593
|
+
const startupWindowRemainingMs = this.streamBaselineFramePending ? this.streamBaselineFrameDeadlineAt - planningStartedAt : 0;
|
|
594
|
+
if (this.streamBaselineFramePending && startupWindowRemainingMs <= 0) {
|
|
595
|
+
this.streamBaselineFramePending = false;
|
|
596
|
+
this.streamBaselineFrameDeadlineAt = 0;
|
|
597
|
+
}
|
|
598
|
+
let deadline = planningStartedAt + (startupWindowRemainingMs > 0 ? startupWindowRemainingMs : FRESH_FRAME_TIMEOUT_MS);
|
|
561
599
|
let candidate = this.getCachedKeyframeCandidate();
|
|
562
600
|
while(true){
|
|
563
601
|
if (candidate) {
|
|
564
602
|
if (void 0 === candidate.ptsUs) throw new Error('Scrcpy frame has no PTS metadata; cannot prove planning freshness');
|
|
603
|
+
if (this.canReuseFrameAcrossActionWait(candidate)) {
|
|
604
|
+
debugScrcpy(`Using frame PTS ${candidate.ptsUs}µs that crossed the active input-action barrier; preserving the settled frame across wait-after-action`);
|
|
605
|
+
return candidate;
|
|
606
|
+
}
|
|
607
|
+
if (this.canUseStreamBaseline(candidate)) {
|
|
608
|
+
debugScrcpy(`Using frame PTS ${candidate.ptsUs}µs as the first planning baseline for the new scrcpy stream epoch`);
|
|
609
|
+
return candidate;
|
|
610
|
+
}
|
|
565
611
|
const age = this.estimateFrameAge(candidate.ptsUs);
|
|
566
612
|
if (null === age) throw new Error('Scrcpy frame clock is not calibrated; cannot prove planning freshness');
|
|
567
613
|
if (age.upperBoundUs <= MAX_FRAME_AGE_US) return candidate;
|
|
@@ -579,11 +625,12 @@ var __webpack_modules__ = {
|
|
|
579
625
|
}
|
|
580
626
|
}
|
|
581
627
|
async closeStaleStreamAndCreateFallbackError(error) {
|
|
582
|
-
this.
|
|
628
|
+
const diagnosticMessage = this.transportBacklogMessage(error);
|
|
583
629
|
const causeMessage = error instanceof Error ? error.message : String(error);
|
|
584
630
|
await this.disconnect();
|
|
585
|
-
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}`, {
|
|
586
|
-
cause: error
|
|
631
|
+
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}`, {
|
|
632
|
+
cause: error,
|
|
633
|
+
diagnosticMessage
|
|
587
634
|
});
|
|
588
635
|
}
|
|
589
636
|
async getScreenshotJpeg() {
|
|
@@ -610,6 +657,9 @@ var __webpack_modules__ = {
|
|
|
610
657
|
const t5 = Date.now();
|
|
611
658
|
const result = await this.decodeH264ToJpeg(keyframeBuffer);
|
|
612
659
|
const decodeTime = Date.now() - t5;
|
|
660
|
+
this.consumeActionFreshnessBarrier(frame);
|
|
661
|
+
this.streamBaselineFramePending = false;
|
|
662
|
+
this.streamBaselineFrameDeadlineAt = 0;
|
|
613
663
|
const totalTime = Date.now() - perfStart;
|
|
614
664
|
debugScrcpy(`Performance: total=${totalTime}ms (connect=${connectTime}ms, frameWait=${frameWaitTime}ms, decode=${decodeTime}ms)`);
|
|
615
665
|
return result;
|
|
@@ -766,6 +816,13 @@ var __webpack_modules__ = {
|
|
|
766
816
|
}
|
|
767
817
|
debugScrcpy('Scrcpy disconnected');
|
|
768
818
|
}
|
|
819
|
+
async dispose() {
|
|
820
|
+
if (this.disposed) return;
|
|
821
|
+
this.disposed = true;
|
|
822
|
+
if (this.connectionPromise) await this.connectionPromise.catch(()=>{});
|
|
823
|
+
await this.disconnect();
|
|
824
|
+
await this.adb.close();
|
|
825
|
+
}
|
|
769
826
|
isConnected() {
|
|
770
827
|
return this.isInitialized && null !== this.scrcpyClient;
|
|
771
828
|
}
|
|
@@ -775,7 +832,8 @@ var __webpack_modules__ = {
|
|
|
775
832
|
_define_property(this, "videoStream", null);
|
|
776
833
|
_define_property(this, "spsHeader", null);
|
|
777
834
|
_define_property(this, "idleTimer", null);
|
|
778
|
-
_define_property(this, "
|
|
835
|
+
_define_property(this, "connectionPromise", null);
|
|
836
|
+
_define_property(this, "disposed", false);
|
|
779
837
|
_define_property(this, "isInitialized", false);
|
|
780
838
|
_define_property(this, "options", void 0);
|
|
781
839
|
_define_property(this, "ffmpegAvailable", null);
|
|
@@ -789,6 +847,9 @@ var __webpack_modules__ = {
|
|
|
789
847
|
_define_property(this, "streamReader", null);
|
|
790
848
|
_define_property(this, "frameFreshnessBarrierPtsUs", null);
|
|
791
849
|
_define_property(this, "frameFreshnessBarrierReason", null);
|
|
850
|
+
_define_property(this, "frameFreshnessBarrierAllowsOverAgeForNextCapture", false);
|
|
851
|
+
_define_property(this, "streamBaselineFramePending", false);
|
|
852
|
+
_define_property(this, "streamBaselineFrameDeadlineAt", 0);
|
|
792
853
|
_define_property(this, "frameFreshnessBarrierPending", false);
|
|
793
854
|
_define_property(this, "frameFreshnessBarrierGeneration", 0);
|
|
794
855
|
_define_property(this, "deviceClockCalibration", null);
|
|
@@ -1024,6 +1085,9 @@ function _define_property(obj, key, value) {
|
|
|
1024
1085
|
return obj;
|
|
1025
1086
|
}
|
|
1026
1087
|
const debugAdapter = (0, logger_.getDebug)('android:scrcpy-adapter');
|
|
1088
|
+
const warnAdapter = (0, logger_.getDebug)('android:scrcpy-adapter', {
|
|
1089
|
+
console: true
|
|
1090
|
+
});
|
|
1027
1091
|
const SCRCPY_RETRY_COOLDOWN_MS = 5000;
|
|
1028
1092
|
const DEFAULT_ADB_SERVER_ENDPOINT = {
|
|
1029
1093
|
host: '127.0.0.1',
|
|
@@ -1077,10 +1141,11 @@ class ScrcpyDeviceAdapter {
|
|
|
1077
1141
|
if (null === this.retryAfter || Date.now() >= this.retryAfter) return;
|
|
1078
1142
|
throw new Error(`scrcpy retry is cooling down until ${new Date(this.retryAfter).toISOString()}. Last error: ${this.lastError}`);
|
|
1079
1143
|
}
|
|
1080
|
-
resolveConfig(
|
|
1144
|
+
resolveConfig(_deviceInfo) {
|
|
1081
1145
|
if (this.resolvedConfig) return this.resolvedConfig;
|
|
1082
1146
|
const config = this.scrcpyConfig;
|
|
1083
1147
|
const maxSize = config?.maxSize ?? scrcpy_manager.ov.maxSize;
|
|
1148
|
+
if (!Number.isInteger(maxSize) || maxSize < 0) throw new Error(`Invalid scrcpyConfig.maxSize: expected a non-negative integer, received ${maxSize}`);
|
|
1084
1149
|
const videoBitRate = config?.videoBitRate ?? scrcpy_manager.ov.videoBitRate;
|
|
1085
1150
|
this.resolvedConfig = {
|
|
1086
1151
|
enabled: this.isConfigured(),
|
|
@@ -1090,31 +1155,68 @@ class ScrcpyDeviceAdapter {
|
|
|
1090
1155
|
};
|
|
1091
1156
|
return this.resolvedConfig;
|
|
1092
1157
|
}
|
|
1158
|
+
async prepareFallbackScreenshot(screenshotBase64) {
|
|
1159
|
+
if (!this.isConfigured()) return screenshotBase64;
|
|
1160
|
+
const { maxSize } = this.resolveConfig();
|
|
1161
|
+
if (0 === maxSize) return screenshotBase64;
|
|
1162
|
+
const sourceSize = await imageInfoOfBase64(screenshotBase64);
|
|
1163
|
+
const largestDimension = Math.max(sourceSize.width, sourceSize.height);
|
|
1164
|
+
if (largestDimension <= maxSize) return screenshotBase64;
|
|
1165
|
+
const scale = maxSize / largestDimension;
|
|
1166
|
+
const targetSize = {
|
|
1167
|
+
width: Math.max(1, Math.round(sourceSize.width * scale)),
|
|
1168
|
+
height: Math.max(1, Math.round(sourceSize.height * scale))
|
|
1169
|
+
};
|
|
1170
|
+
debugAdapter(`Applying scrcpy maxSize to fallback screenshot: ${sourceSize.width}x${sourceSize.height} -> ${targetSize.width}x${targetSize.height}`);
|
|
1171
|
+
return resizeBase64ImageToJpeg(screenshotBase64, {
|
|
1172
|
+
sourceSize,
|
|
1173
|
+
targetSize
|
|
1174
|
+
});
|
|
1175
|
+
}
|
|
1093
1176
|
async ensureManager(deviceInfo) {
|
|
1094
1177
|
if (this.manager) return this.manager;
|
|
1178
|
+
if (this.managerPromise) return this.managerPromise;
|
|
1095
1179
|
debugAdapter('Initializing Scrcpy manager...');
|
|
1180
|
+
const generation = this.lifecycleGeneration;
|
|
1181
|
+
const managerPromise = (async ()=>{
|
|
1182
|
+
let adb = null;
|
|
1183
|
+
let manager = null;
|
|
1184
|
+
try {
|
|
1185
|
+
const { Adb, AdbServerClient } = await import("@yume-chan/adb");
|
|
1186
|
+
const { AdbServerNodeTcpConnector } = await import("@yume-chan/adb-server-node-tcp");
|
|
1187
|
+
const { ScrcpyScreenshotManager: ScrcpyManager } = await Promise.resolve().then(__webpack_require__.bind(__webpack_require__, "./src/scrcpy-manager.ts"));
|
|
1188
|
+
const adbServerEndpoint = await this.resolveAdbServerEndpoint();
|
|
1189
|
+
const adbClient = new AdbServerClient(new AdbServerNodeTcpConnector(adbServerEndpoint));
|
|
1190
|
+
adb = new Adb(await adbClient.createTransport({
|
|
1191
|
+
serial: this.deviceId
|
|
1192
|
+
}));
|
|
1193
|
+
const config = this.resolveConfig(deviceInfo);
|
|
1194
|
+
manager = new ScrcpyManager(adb, {
|
|
1195
|
+
maxSize: config.maxSize,
|
|
1196
|
+
videoBitRate: config.videoBitRate,
|
|
1197
|
+
idleTimeoutMs: config.idleTimeoutMs
|
|
1198
|
+
});
|
|
1199
|
+
await manager.validateEnvironment();
|
|
1200
|
+
if (generation !== this.lifecycleGeneration) throw new Error('Scrcpy manager initialization was superseded by device cleanup');
|
|
1201
|
+
this.manager = manager;
|
|
1202
|
+
debugAdapter('Scrcpy manager initialized');
|
|
1203
|
+
return manager;
|
|
1204
|
+
} catch (error) {
|
|
1205
|
+
try {
|
|
1206
|
+
if (manager) await manager.dispose();
|
|
1207
|
+
else await adb?.close();
|
|
1208
|
+
} catch (cleanupError) {
|
|
1209
|
+
debugAdapter(`Failed to clean up Scrcpy manager initialization: ${cleanupError}`);
|
|
1210
|
+
}
|
|
1211
|
+
debugAdapter(`Failed to initialize Scrcpy manager: ${error}`);
|
|
1212
|
+
throw new Error(`Failed to initialize Scrcpy for device ${this.deviceId}. Ensure ADB server is running and device is connected. Error: ${error}`);
|
|
1213
|
+
}
|
|
1214
|
+
})();
|
|
1215
|
+
this.managerPromise = managerPromise;
|
|
1096
1216
|
try {
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
const adbServerEndpoint = await this.resolveAdbServerEndpoint();
|
|
1101
|
-
const adbClient = new AdbServerClient(new AdbServerNodeTcpConnector(adbServerEndpoint));
|
|
1102
|
-
const adb = new Adb(await adbClient.createTransport({
|
|
1103
|
-
serial: this.deviceId
|
|
1104
|
-
}));
|
|
1105
|
-
const config = this.resolveConfig(deviceInfo);
|
|
1106
|
-
const manager = new ScrcpyManager(adb, {
|
|
1107
|
-
maxSize: config.maxSize,
|
|
1108
|
-
videoBitRate: config.videoBitRate,
|
|
1109
|
-
idleTimeoutMs: config.idleTimeoutMs
|
|
1110
|
-
});
|
|
1111
|
-
await manager.validateEnvironment();
|
|
1112
|
-
this.manager = manager;
|
|
1113
|
-
debugAdapter('Scrcpy manager initialized');
|
|
1114
|
-
return this.manager;
|
|
1115
|
-
} catch (error) {
|
|
1116
|
-
debugAdapter(`Failed to initialize Scrcpy manager: ${error}`);
|
|
1117
|
-
throw new Error(`Failed to initialize Scrcpy for device ${this.deviceId}. Ensure ADB server is running and device is connected. Error: ${error}`);
|
|
1217
|
+
return await managerPromise;
|
|
1218
|
+
} finally{
|
|
1219
|
+
if (this.managerPromise === managerPromise) this.managerPromise = null;
|
|
1118
1220
|
}
|
|
1119
1221
|
}
|
|
1120
1222
|
async screenshotBase64(deviceInfo) {
|
|
@@ -1126,16 +1228,54 @@ class ScrcpyDeviceAdapter {
|
|
|
1126
1228
|
await this.applyPendingActionBarrier(manager);
|
|
1127
1229
|
const screenshotBuffer = await manager.getScreenshotJpeg();
|
|
1128
1230
|
this.clearFailure();
|
|
1129
|
-
return
|
|
1231
|
+
return this.jpegBufferToBase64(screenshotBuffer);
|
|
1130
1232
|
} catch (error) {
|
|
1131
|
-
if ((0, scrcpy_manager.GJ)(error)) {
|
|
1132
|
-
this.
|
|
1233
|
+
if (!(0, scrcpy_manager.GJ)(error)) {
|
|
1234
|
+
this.recordFailure(error);
|
|
1133
1235
|
throw error;
|
|
1134
1236
|
}
|
|
1135
|
-
this.
|
|
1136
|
-
|
|
1237
|
+
this.markFreshnessRecoveryPending(error);
|
|
1238
|
+
debugAdapter(`Scrcpy freshness target was unavailable; restarting the stream once before ADB fallback: ${error}`);
|
|
1239
|
+
try {
|
|
1240
|
+
const screenshotBuffer = await this.restartAndCaptureOnce(deviceInfo);
|
|
1241
|
+
manager = this.manager;
|
|
1242
|
+
if (!manager) throw new Error('Scrcpy manager disappeared after freshness recovery');
|
|
1243
|
+
this.attachKeyframeListeners(manager);
|
|
1244
|
+
this.clearFailure();
|
|
1245
|
+
debugAdapter('Scrcpy screenshot recovered on a new stream epoch');
|
|
1246
|
+
return this.jpegBufferToBase64(screenshotBuffer);
|
|
1247
|
+
} catch (retryError) {
|
|
1248
|
+
if ((0, scrcpy_manager.GJ)(retryError)) this.markFreshnessRecoveryPending(retryError);
|
|
1249
|
+
else this.recordFailure(retryError);
|
|
1250
|
+
this.warnFreshnessFallback(error, retryError);
|
|
1251
|
+
throw retryError;
|
|
1252
|
+
}
|
|
1137
1253
|
}
|
|
1138
1254
|
}
|
|
1255
|
+
async restartAndCaptureOnce(deviceInfo) {
|
|
1256
|
+
if (this.freshnessRestartPromise) return this.freshnessRestartPromise;
|
|
1257
|
+
const generation = this.lifecycleGeneration;
|
|
1258
|
+
const restartPromise = (async ()=>{
|
|
1259
|
+
const manager = await this.ensureManager(deviceInfo);
|
|
1260
|
+
await manager.ensureConnected();
|
|
1261
|
+
await this.applyPendingActionBarrier(manager);
|
|
1262
|
+
if (generation !== this.lifecycleGeneration) throw new Error('Scrcpy freshness restart was cancelled by cleanup');
|
|
1263
|
+
return manager.getScreenshotJpeg();
|
|
1264
|
+
})();
|
|
1265
|
+
this.freshnessRestartPromise = restartPromise;
|
|
1266
|
+
try {
|
|
1267
|
+
return await restartPromise;
|
|
1268
|
+
} finally{
|
|
1269
|
+
if (this.freshnessRestartPromise === restartPromise) this.freshnessRestartPromise = null;
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
jpegBufferToBase64(screenshotBuffer) {
|
|
1273
|
+
return createImgBase64ByFormat('jpeg', screenshotBuffer.toString('base64'));
|
|
1274
|
+
}
|
|
1275
|
+
warnFreshnessFallback(firstError, retryError) {
|
|
1276
|
+
const retryDiagnostic = (0, scrcpy_manager.GJ)(retryError) ? retryError.diagnosticMessage : void 0;
|
|
1277
|
+
warnAdapter(retryDiagnostic ?? (firstError.diagnosticMessage ? `${firstError.diagnosticMessage}\nScrcpy stream restart error: ${retryError}` : void 0) ?? `Scrcpy stream restart failed; falling back to ADB screenshot. Error: ${retryError}`);
|
|
1278
|
+
}
|
|
1139
1279
|
async subscribeKeyframes(deviceInfo, listener) {
|
|
1140
1280
|
this.ensureRetryReady();
|
|
1141
1281
|
this.keyframeListeners.add(listener);
|
|
@@ -1168,17 +1308,26 @@ class ScrcpyDeviceAdapter {
|
|
|
1168
1308
|
this.keyframeUnsubscribers.clear();
|
|
1169
1309
|
for (const listener of this.keyframeListeners)this.attachKeyframeListener(manager, listener);
|
|
1170
1310
|
}
|
|
1171
|
-
markFreshnessRecoveryPending(
|
|
1311
|
+
markFreshnessRecoveryPending(error) {
|
|
1172
1312
|
this.lastError = error.message;
|
|
1173
1313
|
this.retryAfter = null;
|
|
1174
1314
|
this.freshnessRecoveryPending = true;
|
|
1175
1315
|
this.keyframeUnsubscribers.clear();
|
|
1176
|
-
if (manager && this.manager === manager) this.manager = null;
|
|
1177
1316
|
}
|
|
1178
1317
|
async applyPendingActionBarrier(manager) {
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1318
|
+
const actionCompletedAtHostUs = this.pendingActionBarrierAtHostUs;
|
|
1319
|
+
if (null === actionCompletedAtHostUs) return;
|
|
1320
|
+
await manager.setFreshnessBarrier('completed input action while scrcpy was unavailable', {
|
|
1321
|
+
allowOverAgeForNextCapture: true,
|
|
1322
|
+
hostMonotonicUs: actionCompletedAtHostUs
|
|
1323
|
+
});
|
|
1324
|
+
if (this.pendingActionBarrierAtHostUs === actionCompletedAtHostUs) this.pendingActionBarrierAtHostUs = null;
|
|
1325
|
+
}
|
|
1326
|
+
monotonicTimeUs() {
|
|
1327
|
+
return process.hrtime.bigint() / 1000n;
|
|
1328
|
+
}
|
|
1329
|
+
deferActionBarrier(actionCompletedAtHostUs) {
|
|
1330
|
+
if (null === this.pendingActionBarrierAtHostUs || actionCompletedAtHostUs > this.pendingActionBarrierAtHostUs) this.pendingActionBarrierAtHostUs = actionCompletedAtHostUs;
|
|
1182
1331
|
}
|
|
1183
1332
|
recoverAfterAdbScreenshot(deviceInfo) {
|
|
1184
1333
|
if (!this.freshnessRecoveryPending || this.recoveryPromise) return;
|
|
@@ -1191,7 +1340,7 @@ class ScrcpyDeviceAdapter {
|
|
|
1191
1340
|
await this.applyPendingActionBarrier(manager);
|
|
1192
1341
|
await manager.prepareFreshFrame();
|
|
1193
1342
|
if (generation !== this.lifecycleGeneration) {
|
|
1194
|
-
await manager.
|
|
1343
|
+
await manager.dispose();
|
|
1195
1344
|
if (this.manager === manager) this.manager = null;
|
|
1196
1345
|
return;
|
|
1197
1346
|
}
|
|
@@ -1200,7 +1349,7 @@ class ScrcpyDeviceAdapter {
|
|
|
1200
1349
|
debugAdapter('Scrcpy freshness recovery completed in background');
|
|
1201
1350
|
} catch (error) {
|
|
1202
1351
|
if (manager) {
|
|
1203
|
-
await manager.
|
|
1352
|
+
await manager.dispose();
|
|
1204
1353
|
if (this.manager === manager) this.manager = null;
|
|
1205
1354
|
}
|
|
1206
1355
|
this.freshnessRecoveryPending = false;
|
|
@@ -1215,21 +1364,22 @@ class ScrcpyDeviceAdapter {
|
|
|
1215
1364
|
});
|
|
1216
1365
|
}
|
|
1217
1366
|
async markActionBarrier() {
|
|
1367
|
+
const actionCompletedAtHostUs = this.monotonicTimeUs();
|
|
1218
1368
|
const manager = this.manager;
|
|
1219
|
-
if (!manager?.isConnected())
|
|
1220
|
-
this.pendingActionBarrier = true;
|
|
1221
|
-
return;
|
|
1222
|
-
}
|
|
1369
|
+
if (!manager?.isConnected()) return void this.deferActionBarrier(actionCompletedAtHostUs);
|
|
1223
1370
|
try {
|
|
1224
|
-
await manager.setFreshnessBarrier('completed input action'
|
|
1225
|
-
|
|
1371
|
+
await manager.setFreshnessBarrier('completed input action', {
|
|
1372
|
+
allowOverAgeForNextCapture: true,
|
|
1373
|
+
hostMonotonicUs: actionCompletedAtHostUs
|
|
1374
|
+
});
|
|
1375
|
+
if (null !== this.pendingActionBarrierAtHostUs && this.pendingActionBarrierAtHostUs <= actionCompletedAtHostUs) this.pendingActionBarrierAtHostUs = null;
|
|
1226
1376
|
this.clearFailure();
|
|
1227
1377
|
} catch (error) {
|
|
1228
|
-
this.
|
|
1378
|
+
this.deferActionBarrier(actionCompletedAtHostUs);
|
|
1229
1379
|
this.recordFailure(error);
|
|
1230
1380
|
debugAdapter(`Unable to mark scrcpy action barrier; disabling this stream: ${error}`);
|
|
1231
1381
|
try {
|
|
1232
|
-
await manager.
|
|
1382
|
+
await manager.dispose();
|
|
1233
1383
|
} catch (disconnectError) {
|
|
1234
1384
|
debugAdapter(`Error disconnecting scrcpy after barrier failure: ${disconnectError}`);
|
|
1235
1385
|
} finally{
|
|
@@ -1262,14 +1412,16 @@ class ScrcpyDeviceAdapter {
|
|
|
1262
1412
|
async disconnect() {
|
|
1263
1413
|
this.lifecycleGeneration += 1;
|
|
1264
1414
|
this.freshnessRecoveryPending = false;
|
|
1265
|
-
this.
|
|
1415
|
+
this.pendingActionBarrierAtHostUs = null;
|
|
1266
1416
|
for (const unsubscribe of this.keyframeUnsubscribers.values())unsubscribe();
|
|
1267
1417
|
this.keyframeUnsubscribers.clear();
|
|
1268
1418
|
this.keyframeListeners.clear();
|
|
1269
1419
|
if (this.recoveryPromise) await this.recoveryPromise.catch(()=>{});
|
|
1420
|
+
if (this.freshnessRestartPromise) await this.freshnessRestartPromise.catch(()=>{});
|
|
1421
|
+
if (this.managerPromise) await this.managerPromise.catch(()=>{});
|
|
1270
1422
|
if (this.manager) {
|
|
1271
1423
|
try {
|
|
1272
|
-
await this.manager.
|
|
1424
|
+
await this.manager.dispose();
|
|
1273
1425
|
} catch (error) {
|
|
1274
1426
|
debugAdapter(`Error disconnecting scrcpy: ${error}`);
|
|
1275
1427
|
}
|
|
@@ -1283,26 +1435,30 @@ class ScrcpyDeviceAdapter {
|
|
|
1283
1435
|
_define_property(this, "scrcpyConfig", void 0);
|
|
1284
1436
|
_define_property(this, "resolveAdbServerEndpoint", void 0);
|
|
1285
1437
|
_define_property(this, "manager", void 0);
|
|
1438
|
+
_define_property(this, "managerPromise", void 0);
|
|
1286
1439
|
_define_property(this, "resolvedConfig", void 0);
|
|
1287
1440
|
_define_property(this, "lastError", void 0);
|
|
1288
1441
|
_define_property(this, "retryAfter", void 0);
|
|
1289
1442
|
_define_property(this, "freshnessRecoveryPending", void 0);
|
|
1443
|
+
_define_property(this, "freshnessRestartPromise", void 0);
|
|
1290
1444
|
_define_property(this, "recoveryPromise", void 0);
|
|
1291
1445
|
_define_property(this, "lifecycleGeneration", void 0);
|
|
1292
|
-
_define_property(this, "
|
|
1446
|
+
_define_property(this, "pendingActionBarrierAtHostUs", void 0);
|
|
1293
1447
|
_define_property(this, "keyframeListeners", void 0);
|
|
1294
1448
|
_define_property(this, "keyframeUnsubscribers", void 0);
|
|
1295
1449
|
this.deviceId = deviceId;
|
|
1296
1450
|
this.scrcpyConfig = scrcpyConfig;
|
|
1297
1451
|
this.resolveAdbServerEndpoint = resolveAdbServerEndpoint;
|
|
1298
1452
|
this.manager = null;
|
|
1453
|
+
this.managerPromise = null;
|
|
1299
1454
|
this.resolvedConfig = null;
|
|
1300
1455
|
this.lastError = null;
|
|
1301
1456
|
this.retryAfter = null;
|
|
1302
1457
|
this.freshnessRecoveryPending = false;
|
|
1458
|
+
this.freshnessRestartPromise = null;
|
|
1303
1459
|
this.recoveryPromise = null;
|
|
1304
1460
|
this.lifecycleGeneration = 0;
|
|
1305
|
-
this.
|
|
1461
|
+
this.pendingActionBarrierAtHostUs = null;
|
|
1306
1462
|
this.keyframeListeners = new Set();
|
|
1307
1463
|
this.keyframeUnsubscribers = new Map();
|
|
1308
1464
|
}
|
|
@@ -2153,9 +2309,9 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
2153
2309
|
}
|
|
2154
2310
|
async screenshotBase64() {
|
|
2155
2311
|
debugDevice('screenshotBase64 begin');
|
|
2156
|
-
const screenshotStrategy = this.options?.screenshotStrategy || globalConfigManager.getEnvConfigValue(MIDSCENE_ANDROID_SCREENSHOT_STRATEGY) || SCREENSHOT_STRATEGY_AUTO;
|
|
2157
|
-
if (screenshotStrategy === SCREENSHOT_STRATEGY_ALWAYS_YADB) return this.screenshotBase64ViaYadb();
|
|
2158
2312
|
const adapter = this.getScrcpyAdapter();
|
|
2313
|
+
const screenshotStrategy = this.options?.screenshotStrategy || globalConfigManager.getEnvConfigValue(MIDSCENE_ANDROID_SCREENSHOT_STRATEGY) || SCREENSHOT_STRATEGY_AUTO;
|
|
2314
|
+
if (screenshotStrategy === SCREENSHOT_STRATEGY_ALWAYS_YADB) return adapter.prepareFallbackScreenshot(await this.screenshotBase64ViaYadb());
|
|
2159
2315
|
let scrcpyDeviceInfo = null;
|
|
2160
2316
|
if (adapter.isEnabled()) try {
|
|
2161
2317
|
debugDevice('Attempting scrcpy screenshot...');
|
|
@@ -2164,7 +2320,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
2164
2320
|
debugDevice('screenshotBase64 end (scrcpy mode)');
|
|
2165
2321
|
return result;
|
|
2166
2322
|
} catch (error) {
|
|
2167
|
-
warnDevice(`Scrcpy screenshot failed, falling back to standard ADB method
|
|
2323
|
+
warnDevice(`Scrcpy screenshot failed, falling back to standard ADB method.\nError: ${error}`);
|
|
2168
2324
|
}
|
|
2169
2325
|
const adb = await this.getAdb();
|
|
2170
2326
|
let screenshotBuffer;
|
|
@@ -2206,14 +2362,14 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
2206
2362
|
});
|
|
2207
2363
|
debugDevice('screenshotBase64 end (fallback)');
|
|
2208
2364
|
if (scrcpyDeviceInfo) adapter.recoverAfterAdbScreenshot(scrcpyDeviceInfo);
|
|
2209
|
-
return result;
|
|
2365
|
+
return adapter.prepareFallbackScreenshot(result);
|
|
2210
2366
|
}
|
|
2211
2367
|
if (!screenshotBuffer) throw new Error('Failed to capture screenshot: all methods failed');
|
|
2212
2368
|
debugDevice('Converting to base64');
|
|
2213
2369
|
const result = createImgBase64ByFormat('png', screenshotBuffer.toString('base64'));
|
|
2214
2370
|
debugDevice('screenshotBase64 end');
|
|
2215
2371
|
if (scrcpyDeviceInfo) adapter.recoverAfterAdbScreenshot(scrcpyDeviceInfo);
|
|
2216
|
-
return result;
|
|
2372
|
+
return adapter.prepareFallbackScreenshot(result);
|
|
2217
2373
|
}
|
|
2218
2374
|
async captureScreenshotBase64FromDeviceFile(label, capture) {
|
|
2219
2375
|
const adb = await this.getAdb();
|
|
@@ -3067,12 +3223,18 @@ function agent_tools_define_property(obj, key, value) {
|
|
|
3067
3223
|
const debug = (0, logger_.getDebug)('agent-tools:android');
|
|
3068
3224
|
function adaptAndroidInitArgs(extracted) {
|
|
3069
3225
|
if (!extracted) return;
|
|
3226
|
+
const scrcpyVideoBitRate = 'number' == typeof extracted.scrcpyVideoBitRate ? extracted.scrcpyVideoBitRate : void 0;
|
|
3227
|
+
const requestedUseScrcpy = 'boolean' == typeof extracted.useScrcpy ? extracted.useScrcpy : void 0;
|
|
3228
|
+
const useScrcpy = void 0 !== scrcpyVideoBitRate ? true : requestedUseScrcpy;
|
|
3070
3229
|
const initArgs = {
|
|
3071
3230
|
...'string' == typeof extracted.deviceId ? {
|
|
3072
3231
|
deviceId: extracted.deviceId
|
|
3073
3232
|
} : {},
|
|
3074
|
-
...
|
|
3075
|
-
useScrcpy
|
|
3233
|
+
...void 0 !== useScrcpy ? {
|
|
3234
|
+
useScrcpy
|
|
3235
|
+
} : {},
|
|
3236
|
+
...void 0 !== scrcpyVideoBitRate ? {
|
|
3237
|
+
scrcpyVideoBitRate
|
|
3076
3238
|
} : {},
|
|
3077
3239
|
...extractAgentBehaviorInitArgs(extracted) ?? {}
|
|
3078
3240
|
};
|
|
@@ -3104,7 +3266,10 @@ class AndroidMidsceneTools extends BaseMidsceneTools {
|
|
|
3104
3266
|
...extractAgentBehaviorInitArgs(initArgs) ?? {},
|
|
3105
3267
|
...initArgs?.useScrcpy ? {
|
|
3106
3268
|
scrcpyConfig: {
|
|
3107
|
-
enabled: true
|
|
3269
|
+
enabled: true,
|
|
3270
|
+
...void 0 !== initArgs.scrcpyVideoBitRate ? {
|
|
3271
|
+
videoBitRate: initArgs.scrcpyVideoBitRate
|
|
3272
|
+
} : {}
|
|
3108
3273
|
}
|
|
3109
3274
|
} : {},
|
|
3110
3275
|
...reportOptions ?? {}
|
|
@@ -3162,6 +3327,7 @@ class AndroidMidsceneTools extends BaseMidsceneTools {
|
|
|
3162
3327
|
shape: {
|
|
3163
3328
|
deviceId: z.string().optional().describe('Android device ID (from adb devices)'),
|
|
3164
3329
|
useScrcpy: z.boolean().optional().describe('Enable scrcpy accelerated screenshots'),
|
|
3330
|
+
scrcpyVideoBitRate: 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).'),
|
|
3165
3331
|
...agentBehaviorInitArgShape
|
|
3166
3332
|
},
|
|
3167
3333
|
cli: {
|
|
@@ -3174,7 +3340,7 @@ class AndroidMidsceneTools extends BaseMidsceneTools {
|
|
|
3174
3340
|
const tools = new AndroidMidsceneTools();
|
|
3175
3341
|
runToolsCLI(tools, 'midscene-android', {
|
|
3176
3342
|
stripPrefix: 'android_',
|
|
3177
|
-
version: "1.12.0",
|
|
3343
|
+
version: "1.12.1-beta-20260824081858.0",
|
|
3178
3344
|
extraCommands: createReportCliCommands()
|
|
3179
3345
|
}).catch((e)=>{
|
|
3180
3346
|
process.exit(reportCLIError(e));
|