@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/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,
|
|
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.
|
|
120
|
-
debugScrcpy('Connection already in progress,
|
|
121
|
-
await
|
|
122
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
576
|
+
await this.waitForPlanningFrame();
|
|
545
577
|
}
|
|
546
578
|
async waitForPlanningFrame() {
|
|
547
579
|
let planningBarrierArmed = false;
|
|
548
|
-
|
|
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.
|
|
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, "
|
|
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(
|
|
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
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
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
|
|
1167
|
+
return this.jpegBufferToBase64(screenshotBuffer);
|
|
1066
1168
|
} catch (error) {
|
|
1067
|
-
if ((0, scrcpy_manager.GJ)(error)) {
|
|
1068
|
-
this.
|
|
1169
|
+
if (!(0, scrcpy_manager.GJ)(error)) {
|
|
1170
|
+
this.recordFailure(error);
|
|
1069
1171
|
throw error;
|
|
1070
1172
|
}
|
|
1071
|
-
this.
|
|
1072
|
-
|
|
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(
|
|
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
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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, "
|
|
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.
|
|
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
|
|
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
|
-
...
|
|
3178
|
-
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: {
|