@midscene/android 1.3.11-beta-20260211054343.0 → 1.3.11
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/index.mjs +38 -65
- package/dist/es/mcp-server.mjs +38 -65
- package/dist/lib/index.js +37 -64
- package/dist/lib/mcp-server.js +37 -64
- package/dist/types/index.d.ts +15 -14
- package/package.json +4 -4
package/dist/es/index.mjs
CHANGED
|
@@ -7,7 +7,7 @@ import { getMidsceneLocationSchema, z } from "@midscene/core";
|
|
|
7
7
|
import { defineAction, defineActionClearInput, defineActionCursorMove, defineActionDoubleClick, defineActionDragAndDrop, defineActionKeyboardPress, defineActionScroll, defineActionSwipe, defineActionTap, normalizeMobileSwipeParam } from "@midscene/core/device";
|
|
8
8
|
import { getTmpFile, sleep } from "@midscene/core/utils";
|
|
9
9
|
import { MIDSCENE_ADB_PATH, MIDSCENE_ADB_REMOTE_HOST, MIDSCENE_ADB_REMOTE_PORT, MIDSCENE_ANDROID_IME_STRATEGY, globalConfigManager, overrideAIConfig } from "@midscene/shared/env";
|
|
10
|
-
import { createImgBase64ByFormat,
|
|
10
|
+
import { createImgBase64ByFormat, isValidImageBuffer } from "@midscene/shared/img";
|
|
11
11
|
import { mergeAndNormalizeAppNameMapping, normalizeForComparison, repeat } from "@midscene/shared/utils";
|
|
12
12
|
import { ADB } from "appium-adb";
|
|
13
13
|
import { Agent } from "@midscene/core/agent";
|
|
@@ -32,18 +32,16 @@ var __webpack_modules__ = {
|
|
|
32
32
|
return obj;
|
|
33
33
|
}
|
|
34
34
|
const debugScrcpy = (0, _midscene_shared_logger__rspack_import_3.getDebug)('android:scrcpy');
|
|
35
|
+
const warnScrcpy = (0, _midscene_shared_logger__rspack_import_3.getDebug)('android:scrcpy', {
|
|
36
|
+
console: true
|
|
37
|
+
});
|
|
35
38
|
const NAL_TYPE_IDR = 5;
|
|
36
39
|
const NAL_TYPE_SPS = 7;
|
|
37
40
|
const NAL_TYPE_PPS = 8;
|
|
38
41
|
const NAL_TYPE_MASK = 0x1f;
|
|
39
|
-
const START_CODE_4_BYTE = Buffer.from([
|
|
40
|
-
0x00,
|
|
41
|
-
0x00,
|
|
42
|
-
0x00,
|
|
43
|
-
0x01
|
|
44
|
-
]);
|
|
45
42
|
const DEFAULT_MAX_SIZE = 0;
|
|
46
|
-
const DEFAULT_VIDEO_BIT_RATE =
|
|
43
|
+
const DEFAULT_VIDEO_BIT_RATE = 100000000;
|
|
44
|
+
const MAX_VIDEO_BIT_RATE = 100000000;
|
|
47
45
|
const DEFAULT_IDLE_TIMEOUT_MS = 30000;
|
|
48
46
|
const MAX_KEYFRAME_WAIT_MS = 5000;
|
|
49
47
|
const FRESH_FRAME_TIMEOUT_MS = 300;
|
|
@@ -91,8 +89,7 @@ var __webpack_modules__ = {
|
|
|
91
89
|
debugScrcpy('Starting scrcpy connection...');
|
|
92
90
|
const { AdbScrcpyClient, AdbScrcpyOptions2_1 } = await import("@yume-chan/adb-scrcpy");
|
|
93
91
|
const { ReadableStream } = await import("@yume-chan/stream-extra");
|
|
94
|
-
const { ScrcpyOptions3_1, DefaultServerPath
|
|
95
|
-
this.h264SearchConfigFn = h264SearchConfiguration;
|
|
92
|
+
const { ScrcpyOptions3_1, DefaultServerPath } = await import("@yume-chan/scrcpy");
|
|
96
93
|
const serverBinPath = this.resolveServerBinPath();
|
|
97
94
|
await AdbScrcpyClient.pushServer(this.adb, ReadableStream.from((0, node_fs__rspack_import_0.createReadStream)(serverBinPath)));
|
|
98
95
|
const scrcpyOptions = new ScrcpyOptions3_1({
|
|
@@ -100,8 +97,9 @@ var __webpack_modules__ = {
|
|
|
100
97
|
control: false,
|
|
101
98
|
maxSize: this.options.maxSize,
|
|
102
99
|
videoBitRate: this.options.videoBitRate,
|
|
103
|
-
|
|
104
|
-
|
|
100
|
+
maxFps: 10,
|
|
101
|
+
sendFrameMeta: true,
|
|
102
|
+
videoCodecOptions: 'i-frame-interval=0,bitrate-mode=2'
|
|
105
103
|
});
|
|
106
104
|
this.scrcpyClient = await AdbScrcpyClient.start(this.adb, DefaultServerPath, new AdbScrcpyOptions2_1(scrcpyOptions));
|
|
107
105
|
const videoStreamPromise = this.scrcpyClient.videoStream;
|
|
@@ -159,10 +157,14 @@ var __webpack_modules__ = {
|
|
|
159
157
|
}
|
|
160
158
|
}
|
|
161
159
|
processFrame(packet) {
|
|
160
|
+
if ('configuration' === packet.type) {
|
|
161
|
+
this.spsHeader = Buffer.from(packet.data);
|
|
162
|
+
debugScrcpy(`Received SPS/PPS configuration: ${this.spsHeader.length}B`);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
162
165
|
const frameBuffer = Buffer.from(packet.data);
|
|
163
|
-
const
|
|
164
|
-
if (
|
|
165
|
-
if (actualKeyFrame && this.spsHeader) {
|
|
166
|
+
const isKeyFrame = detectH264KeyFrame(frameBuffer);
|
|
167
|
+
if (isKeyFrame && this.spsHeader) {
|
|
166
168
|
this.lastRawKeyframe = frameBuffer;
|
|
167
169
|
if (this.keyframeResolvers.length > 0) {
|
|
168
170
|
const combined = Buffer.concat([
|
|
@@ -173,23 +175,7 @@ var __webpack_modules__ = {
|
|
|
173
175
|
}
|
|
174
176
|
}
|
|
175
177
|
}
|
|
176
|
-
|
|
177
|
-
if (!this.h264SearchConfigFn) return;
|
|
178
|
-
try {
|
|
179
|
-
const config = this.h264SearchConfigFn(new Uint8Array(frameBuffer));
|
|
180
|
-
if (!config.sequenceParameterSet || !config.pictureParameterSet) return;
|
|
181
|
-
this.spsHeader = Buffer.concat([
|
|
182
|
-
START_CODE_4_BYTE,
|
|
183
|
-
Buffer.from(config.sequenceParameterSet),
|
|
184
|
-
START_CODE_4_BYTE,
|
|
185
|
-
Buffer.from(config.pictureParameterSet)
|
|
186
|
-
]);
|
|
187
|
-
debugScrcpy(`Extracted SPS/PPS: SPS=${config.sequenceParameterSet.length}B, PPS=${config.pictureParameterSet.length}B, total=${this.spsHeader.length}B`);
|
|
188
|
-
} catch (error) {
|
|
189
|
-
debugScrcpy(`Failed to extract SPS/PPS from keyframe: ${error}`);
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
async getScreenshotPng() {
|
|
178
|
+
async getScreenshotJpeg() {
|
|
193
179
|
const perfStart = Date.now();
|
|
194
180
|
const t1 = Date.now();
|
|
195
181
|
await this.ensureConnected();
|
|
@@ -219,7 +205,7 @@ var __webpack_modules__ = {
|
|
|
219
205
|
this.resetIdleTimer();
|
|
220
206
|
debugScrcpy(`Decoding H.264 stream: ${keyframeBuffer.length} bytes (${frameSource})`);
|
|
221
207
|
const t4 = Date.now();
|
|
222
|
-
const result = await this.
|
|
208
|
+
const result = await this.decodeH264ToJpeg(keyframeBuffer);
|
|
223
209
|
const decodeTime = Date.now() - t4;
|
|
224
210
|
const totalTime = Date.now() - perfStart;
|
|
225
211
|
debugScrcpy(`Performance: total=${totalTime}ms (connect=${connectTime}ms, spsWait=${spsWaitTime}ms, frameWait=${frameWaitTime}ms[${frameSource}], decode=${decodeTime}ms)`);
|
|
@@ -282,7 +268,7 @@ var __webpack_modules__ = {
|
|
|
282
268
|
return false;
|
|
283
269
|
}
|
|
284
270
|
}
|
|
285
|
-
async
|
|
271
|
+
async decodeH264ToJpeg(h264Buffer) {
|
|
286
272
|
const { spawn } = await import("node:child_process");
|
|
287
273
|
return new Promise((resolve, reject)=>{
|
|
288
274
|
const ffmpegArgs = [
|
|
@@ -295,7 +281,9 @@ var __webpack_modules__ = {
|
|
|
295
281
|
'-f',
|
|
296
282
|
'image2pipe',
|
|
297
283
|
'-vcodec',
|
|
298
|
-
'
|
|
284
|
+
'mjpeg',
|
|
285
|
+
'-q:v',
|
|
286
|
+
'5',
|
|
299
287
|
'-loglevel',
|
|
300
288
|
'error',
|
|
301
289
|
'pipe:1'
|
|
@@ -318,13 +306,13 @@ var __webpack_modules__ = {
|
|
|
318
306
|
});
|
|
319
307
|
ffmpeg.on('close', (code)=>{
|
|
320
308
|
if (0 === code && chunks.length > 0) {
|
|
321
|
-
const
|
|
322
|
-
debugScrcpy(`FFmpeg decode successful,
|
|
323
|
-
resolve(
|
|
309
|
+
const jpegBuffer = Buffer.concat(chunks);
|
|
310
|
+
debugScrcpy(`FFmpeg decode successful, JPEG size: ${jpegBuffer.length} bytes`);
|
|
311
|
+
resolve(jpegBuffer);
|
|
324
312
|
} else {
|
|
325
313
|
const errorMsg = stderrOutput || `FFmpeg exited with code ${code}`;
|
|
326
314
|
debugScrcpy(`FFmpeg decode failed: ${errorMsg}`);
|
|
327
|
-
reject(new Error(`H.264 to
|
|
315
|
+
reject(new Error(`H.264 to JPEG decode failed: ${errorMsg}`));
|
|
328
316
|
}
|
|
329
317
|
});
|
|
330
318
|
ffmpeg.on('error', (error)=>{
|
|
@@ -356,7 +344,6 @@ var __webpack_modules__ = {
|
|
|
356
344
|
this.spsHeader = null;
|
|
357
345
|
this.lastRawKeyframe = null;
|
|
358
346
|
this.isInitialized = false;
|
|
359
|
-
this.h264SearchConfigFn = null;
|
|
360
347
|
this.keyframeResolvers = [];
|
|
361
348
|
if (reader) try {
|
|
362
349
|
reader.cancel();
|
|
@@ -384,12 +371,14 @@ var __webpack_modules__ = {
|
|
|
384
371
|
_define_property(this, "keyframeResolvers", []);
|
|
385
372
|
_define_property(this, "lastRawKeyframe", null);
|
|
386
373
|
_define_property(this, "videoResolution", null);
|
|
387
|
-
_define_property(this, "h264SearchConfigFn", null);
|
|
388
374
|
_define_property(this, "streamReader", null);
|
|
389
375
|
this.adb = adb;
|
|
376
|
+
const requestedBitRate = options.videoBitRate ?? DEFAULT_VIDEO_BIT_RATE;
|
|
377
|
+
const clampedBitRate = Math.min(requestedBitRate, MAX_VIDEO_BIT_RATE);
|
|
378
|
+
if (requestedBitRate > MAX_VIDEO_BIT_RATE) warnScrcpy(`videoBitRate ${requestedBitRate} exceeds maximum ${MAX_VIDEO_BIT_RATE}, clamped to ${clampedBitRate}`);
|
|
390
379
|
this.options = {
|
|
391
380
|
maxSize: options.maxSize ?? DEFAULT_MAX_SIZE,
|
|
392
|
-
videoBitRate:
|
|
381
|
+
videoBitRate: clampedBitRate,
|
|
393
382
|
idleTimeoutMs: options.idleTimeoutMs ?? DEFAULT_IDLE_TIMEOUT_MS
|
|
394
383
|
};
|
|
395
384
|
}
|
|
@@ -462,18 +451,13 @@ class ScrcpyDeviceAdapter {
|
|
|
462
451
|
resolveConfig(deviceInfo) {
|
|
463
452
|
if (this.resolvedConfig) return this.resolvedConfig;
|
|
464
453
|
const config = this.scrcpyConfig;
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
const physicalMax = Math.max(deviceInfo.physicalWidth, deviceInfo.physicalHeight);
|
|
468
|
-
const scale = this.screenshotResizeScale ?? 1 / deviceInfo.dpr;
|
|
469
|
-
maxSize = Math.round(physicalMax * scale);
|
|
470
|
-
debugAdapter(`Auto-calculated maxSize: ${maxSize} (physical=${physicalMax}, scale=${scale.toFixed(3)}, ${void 0 !== this.screenshotResizeScale ? 'from screenshotResizeScale' : 'from 1/dpr'})`);
|
|
471
|
-
}
|
|
454
|
+
const maxSize = config?.maxSize ?? scrcpy_manager.o.maxSize;
|
|
455
|
+
const videoBitRate = config?.videoBitRate ?? scrcpy_manager.o.videoBitRate;
|
|
472
456
|
this.resolvedConfig = {
|
|
473
457
|
enabled: this.isEnabled(),
|
|
474
458
|
maxSize,
|
|
475
459
|
idleTimeoutMs: config?.idleTimeoutMs ?? scrcpy_manager.o.idleTimeoutMs,
|
|
476
|
-
videoBitRate
|
|
460
|
+
videoBitRate
|
|
477
461
|
};
|
|
478
462
|
return this.resolvedConfig;
|
|
479
463
|
}
|
|
@@ -508,8 +492,8 @@ class ScrcpyDeviceAdapter {
|
|
|
508
492
|
}
|
|
509
493
|
async screenshotBase64(deviceInfo) {
|
|
510
494
|
const manager = await this.ensureManager(deviceInfo);
|
|
511
|
-
const screenshotBuffer = await manager.
|
|
512
|
-
return createImgBase64ByFormat('
|
|
495
|
+
const screenshotBuffer = await manager.getScreenshotJpeg();
|
|
496
|
+
return createImgBase64ByFormat('jpeg', screenshotBuffer.toString('base64'));
|
|
513
497
|
}
|
|
514
498
|
getResolution() {
|
|
515
499
|
return this.manager?.getResolution() ?? null;
|
|
@@ -1016,17 +1000,6 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1016
1000
|
}
|
|
1017
1001
|
async size() {
|
|
1018
1002
|
const deviceInfo = await this.getDevicePhysicalInfo();
|
|
1019
|
-
const adapter = this.getScrcpyAdapter();
|
|
1020
|
-
if (adapter.isEnabled()) {
|
|
1021
|
-
const scrcpySize = adapter.getSize(deviceInfo);
|
|
1022
|
-
if (scrcpySize) {
|
|
1023
|
-
const isLandscape = 1 === deviceInfo.orientation || 3 === deviceInfo.orientation;
|
|
1024
|
-
const shouldSwap = true !== deviceInfo.isCurrentOrientation && isLandscape;
|
|
1025
|
-
const physicalWidth = shouldSwap ? deviceInfo.physicalHeight : deviceInfo.physicalWidth;
|
|
1026
|
-
this.scalingRatio = adapter.getScalingRatio(physicalWidth) ?? this.scalingRatio;
|
|
1027
|
-
return scrcpySize;
|
|
1028
|
-
}
|
|
1029
|
-
}
|
|
1030
1003
|
const isLandscape = 1 === deviceInfo.orientation || 3 === deviceInfo.orientation;
|
|
1031
1004
|
const shouldSwap = true !== deviceInfo.isCurrentOrientation && isLandscape;
|
|
1032
1005
|
const width = shouldSwap ? deviceInfo.physicalHeight : deviceInfo.physicalWidth;
|
|
@@ -1095,7 +1068,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1095
1068
|
screenshotBuffer = await adb.takeScreenshot(null);
|
|
1096
1069
|
debugDevice('adb.takeScreenshot completed');
|
|
1097
1070
|
if (!screenshotBuffer) throw new Error('Failed to capture screenshot: screenshotBuffer is null');
|
|
1098
|
-
if (!
|
|
1071
|
+
if (!isValidImageBuffer(screenshotBuffer)) {
|
|
1099
1072
|
debugDevice('Invalid image buffer detected: not a valid image format');
|
|
1100
1073
|
throw new Error('Screenshot buffer has invalid format: could not find valid image signature');
|
|
1101
1074
|
}
|
|
@@ -1126,7 +1099,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1126
1099
|
screenshotBuffer = await external_node_fs_["default"].promises.readFile(screenshotPath);
|
|
1127
1100
|
const validScreenshotBufferSize = this.options?.minScreenshotBufferSize ?? 10240;
|
|
1128
1101
|
if (!screenshotBuffer || validScreenshotBufferSize > 0 && screenshotBuffer.length < validScreenshotBufferSize) throw new Error(`Fallback screenshot validation failed: buffer size ${screenshotBuffer?.length || 0} bytes (minimum: ${validScreenshotBufferSize})`);
|
|
1129
|
-
if (!
|
|
1102
|
+
if (!isValidImageBuffer(screenshotBuffer)) throw new Error('Fallback screenshot buffer has invalid PNG format');
|
|
1130
1103
|
debugDevice(`Fallback screenshot validated successfully: ${screenshotBuffer.length} bytes`);
|
|
1131
1104
|
} finally{
|
|
1132
1105
|
Promise.resolve().then(()=>adb.shell(`rm ${androidScreenshotPath}`)).catch((error)=>{
|
package/dist/es/mcp-server.mjs
CHANGED
|
@@ -10,7 +10,7 @@ import { getMidsceneLocationSchema, z } from "@midscene/core";
|
|
|
10
10
|
import { defineAction, defineActionClearInput, defineActionCursorMove, defineActionDoubleClick, defineActionDragAndDrop, defineActionKeyboardPress, defineActionScroll, defineActionSwipe, defineActionTap, normalizeMobileSwipeParam } from "@midscene/core/device";
|
|
11
11
|
import { getTmpFile, sleep } from "@midscene/core/utils";
|
|
12
12
|
import { MIDSCENE_ADB_PATH, MIDSCENE_ADB_REMOTE_HOST, MIDSCENE_ADB_REMOTE_PORT, MIDSCENE_ANDROID_IME_STRATEGY, globalConfigManager } from "@midscene/shared/env";
|
|
13
|
-
import { createImgBase64ByFormat,
|
|
13
|
+
import { createImgBase64ByFormat, isValidImageBuffer } from "@midscene/shared/img";
|
|
14
14
|
import { ADB } from "appium-adb";
|
|
15
15
|
var __webpack_modules__ = {
|
|
16
16
|
"./src/scrcpy-manager.ts" (__unused_rspack_module, __webpack_exports__, __webpack_require__) {
|
|
@@ -33,18 +33,16 @@ var __webpack_modules__ = {
|
|
|
33
33
|
return obj;
|
|
34
34
|
}
|
|
35
35
|
const debugScrcpy = (0, _midscene_shared_logger__rspack_import_3.getDebug)('android:scrcpy');
|
|
36
|
+
const warnScrcpy = (0, _midscene_shared_logger__rspack_import_3.getDebug)('android:scrcpy', {
|
|
37
|
+
console: true
|
|
38
|
+
});
|
|
36
39
|
const NAL_TYPE_IDR = 5;
|
|
37
40
|
const NAL_TYPE_SPS = 7;
|
|
38
41
|
const NAL_TYPE_PPS = 8;
|
|
39
42
|
const NAL_TYPE_MASK = 0x1f;
|
|
40
|
-
const START_CODE_4_BYTE = Buffer.from([
|
|
41
|
-
0x00,
|
|
42
|
-
0x00,
|
|
43
|
-
0x00,
|
|
44
|
-
0x01
|
|
45
|
-
]);
|
|
46
43
|
const DEFAULT_MAX_SIZE = 0;
|
|
47
|
-
const DEFAULT_VIDEO_BIT_RATE =
|
|
44
|
+
const DEFAULT_VIDEO_BIT_RATE = 100000000;
|
|
45
|
+
const MAX_VIDEO_BIT_RATE = 100000000;
|
|
48
46
|
const DEFAULT_IDLE_TIMEOUT_MS = 30000;
|
|
49
47
|
const MAX_KEYFRAME_WAIT_MS = 5000;
|
|
50
48
|
const FRESH_FRAME_TIMEOUT_MS = 300;
|
|
@@ -92,8 +90,7 @@ var __webpack_modules__ = {
|
|
|
92
90
|
debugScrcpy('Starting scrcpy connection...');
|
|
93
91
|
const { AdbScrcpyClient, AdbScrcpyOptions2_1 } = await import("@yume-chan/adb-scrcpy");
|
|
94
92
|
const { ReadableStream } = await import("@yume-chan/stream-extra");
|
|
95
|
-
const { ScrcpyOptions3_1, DefaultServerPath
|
|
96
|
-
this.h264SearchConfigFn = h264SearchConfiguration;
|
|
93
|
+
const { ScrcpyOptions3_1, DefaultServerPath } = await import("@yume-chan/scrcpy");
|
|
97
94
|
const serverBinPath = this.resolveServerBinPath();
|
|
98
95
|
await AdbScrcpyClient.pushServer(this.adb, ReadableStream.from((0, node_fs__rspack_import_0.createReadStream)(serverBinPath)));
|
|
99
96
|
const scrcpyOptions = new ScrcpyOptions3_1({
|
|
@@ -101,8 +98,9 @@ var __webpack_modules__ = {
|
|
|
101
98
|
control: false,
|
|
102
99
|
maxSize: this.options.maxSize,
|
|
103
100
|
videoBitRate: this.options.videoBitRate,
|
|
104
|
-
|
|
105
|
-
|
|
101
|
+
maxFps: 10,
|
|
102
|
+
sendFrameMeta: true,
|
|
103
|
+
videoCodecOptions: 'i-frame-interval=0,bitrate-mode=2'
|
|
106
104
|
});
|
|
107
105
|
this.scrcpyClient = await AdbScrcpyClient.start(this.adb, DefaultServerPath, new AdbScrcpyOptions2_1(scrcpyOptions));
|
|
108
106
|
const videoStreamPromise = this.scrcpyClient.videoStream;
|
|
@@ -160,10 +158,14 @@ var __webpack_modules__ = {
|
|
|
160
158
|
}
|
|
161
159
|
}
|
|
162
160
|
processFrame(packet) {
|
|
161
|
+
if ('configuration' === packet.type) {
|
|
162
|
+
this.spsHeader = Buffer.from(packet.data);
|
|
163
|
+
debugScrcpy(`Received SPS/PPS configuration: ${this.spsHeader.length}B`);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
163
166
|
const frameBuffer = Buffer.from(packet.data);
|
|
164
|
-
const
|
|
165
|
-
if (
|
|
166
|
-
if (actualKeyFrame && this.spsHeader) {
|
|
167
|
+
const isKeyFrame = detectH264KeyFrame(frameBuffer);
|
|
168
|
+
if (isKeyFrame && this.spsHeader) {
|
|
167
169
|
this.lastRawKeyframe = frameBuffer;
|
|
168
170
|
if (this.keyframeResolvers.length > 0) {
|
|
169
171
|
const combined = Buffer.concat([
|
|
@@ -174,23 +176,7 @@ var __webpack_modules__ = {
|
|
|
174
176
|
}
|
|
175
177
|
}
|
|
176
178
|
}
|
|
177
|
-
|
|
178
|
-
if (!this.h264SearchConfigFn) return;
|
|
179
|
-
try {
|
|
180
|
-
const config = this.h264SearchConfigFn(new Uint8Array(frameBuffer));
|
|
181
|
-
if (!config.sequenceParameterSet || !config.pictureParameterSet) return;
|
|
182
|
-
this.spsHeader = Buffer.concat([
|
|
183
|
-
START_CODE_4_BYTE,
|
|
184
|
-
Buffer.from(config.sequenceParameterSet),
|
|
185
|
-
START_CODE_4_BYTE,
|
|
186
|
-
Buffer.from(config.pictureParameterSet)
|
|
187
|
-
]);
|
|
188
|
-
debugScrcpy(`Extracted SPS/PPS: SPS=${config.sequenceParameterSet.length}B, PPS=${config.pictureParameterSet.length}B, total=${this.spsHeader.length}B`);
|
|
189
|
-
} catch (error) {
|
|
190
|
-
debugScrcpy(`Failed to extract SPS/PPS from keyframe: ${error}`);
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
async getScreenshotPng() {
|
|
179
|
+
async getScreenshotJpeg() {
|
|
194
180
|
const perfStart = Date.now();
|
|
195
181
|
const t1 = Date.now();
|
|
196
182
|
await this.ensureConnected();
|
|
@@ -220,7 +206,7 @@ var __webpack_modules__ = {
|
|
|
220
206
|
this.resetIdleTimer();
|
|
221
207
|
debugScrcpy(`Decoding H.264 stream: ${keyframeBuffer.length} bytes (${frameSource})`);
|
|
222
208
|
const t4 = Date.now();
|
|
223
|
-
const result = await this.
|
|
209
|
+
const result = await this.decodeH264ToJpeg(keyframeBuffer);
|
|
224
210
|
const decodeTime = Date.now() - t4;
|
|
225
211
|
const totalTime = Date.now() - perfStart;
|
|
226
212
|
debugScrcpy(`Performance: total=${totalTime}ms (connect=${connectTime}ms, spsWait=${spsWaitTime}ms, frameWait=${frameWaitTime}ms[${frameSource}], decode=${decodeTime}ms)`);
|
|
@@ -283,7 +269,7 @@ var __webpack_modules__ = {
|
|
|
283
269
|
return false;
|
|
284
270
|
}
|
|
285
271
|
}
|
|
286
|
-
async
|
|
272
|
+
async decodeH264ToJpeg(h264Buffer) {
|
|
287
273
|
const { spawn } = await import("node:child_process");
|
|
288
274
|
return new Promise((resolve, reject)=>{
|
|
289
275
|
const ffmpegArgs = [
|
|
@@ -296,7 +282,9 @@ var __webpack_modules__ = {
|
|
|
296
282
|
'-f',
|
|
297
283
|
'image2pipe',
|
|
298
284
|
'-vcodec',
|
|
299
|
-
'
|
|
285
|
+
'mjpeg',
|
|
286
|
+
'-q:v',
|
|
287
|
+
'5',
|
|
300
288
|
'-loglevel',
|
|
301
289
|
'error',
|
|
302
290
|
'pipe:1'
|
|
@@ -319,13 +307,13 @@ var __webpack_modules__ = {
|
|
|
319
307
|
});
|
|
320
308
|
ffmpeg.on('close', (code)=>{
|
|
321
309
|
if (0 === code && chunks.length > 0) {
|
|
322
|
-
const
|
|
323
|
-
debugScrcpy(`FFmpeg decode successful,
|
|
324
|
-
resolve(
|
|
310
|
+
const jpegBuffer = Buffer.concat(chunks);
|
|
311
|
+
debugScrcpy(`FFmpeg decode successful, JPEG size: ${jpegBuffer.length} bytes`);
|
|
312
|
+
resolve(jpegBuffer);
|
|
325
313
|
} else {
|
|
326
314
|
const errorMsg = stderrOutput || `FFmpeg exited with code ${code}`;
|
|
327
315
|
debugScrcpy(`FFmpeg decode failed: ${errorMsg}`);
|
|
328
|
-
reject(new Error(`H.264 to
|
|
316
|
+
reject(new Error(`H.264 to JPEG decode failed: ${errorMsg}`));
|
|
329
317
|
}
|
|
330
318
|
});
|
|
331
319
|
ffmpeg.on('error', (error)=>{
|
|
@@ -357,7 +345,6 @@ var __webpack_modules__ = {
|
|
|
357
345
|
this.spsHeader = null;
|
|
358
346
|
this.lastRawKeyframe = null;
|
|
359
347
|
this.isInitialized = false;
|
|
360
|
-
this.h264SearchConfigFn = null;
|
|
361
348
|
this.keyframeResolvers = [];
|
|
362
349
|
if (reader) try {
|
|
363
350
|
reader.cancel();
|
|
@@ -385,12 +372,14 @@ var __webpack_modules__ = {
|
|
|
385
372
|
_define_property(this, "keyframeResolvers", []);
|
|
386
373
|
_define_property(this, "lastRawKeyframe", null);
|
|
387
374
|
_define_property(this, "videoResolution", null);
|
|
388
|
-
_define_property(this, "h264SearchConfigFn", null);
|
|
389
375
|
_define_property(this, "streamReader", null);
|
|
390
376
|
this.adb = adb;
|
|
377
|
+
const requestedBitRate = options.videoBitRate ?? DEFAULT_VIDEO_BIT_RATE;
|
|
378
|
+
const clampedBitRate = Math.min(requestedBitRate, MAX_VIDEO_BIT_RATE);
|
|
379
|
+
if (requestedBitRate > MAX_VIDEO_BIT_RATE) warnScrcpy(`videoBitRate ${requestedBitRate} exceeds maximum ${MAX_VIDEO_BIT_RATE}, clamped to ${clampedBitRate}`);
|
|
391
380
|
this.options = {
|
|
392
381
|
maxSize: options.maxSize ?? DEFAULT_MAX_SIZE,
|
|
393
|
-
videoBitRate:
|
|
382
|
+
videoBitRate: clampedBitRate,
|
|
394
383
|
idleTimeoutMs: options.idleTimeoutMs ?? DEFAULT_IDLE_TIMEOUT_MS
|
|
395
384
|
};
|
|
396
385
|
}
|
|
@@ -559,18 +548,13 @@ class ScrcpyDeviceAdapter {
|
|
|
559
548
|
resolveConfig(deviceInfo) {
|
|
560
549
|
if (this.resolvedConfig) return this.resolvedConfig;
|
|
561
550
|
const config = this.scrcpyConfig;
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
const physicalMax = Math.max(deviceInfo.physicalWidth, deviceInfo.physicalHeight);
|
|
565
|
-
const scale = this.screenshotResizeScale ?? 1 / deviceInfo.dpr;
|
|
566
|
-
maxSize = Math.round(physicalMax * scale);
|
|
567
|
-
debugAdapter(`Auto-calculated maxSize: ${maxSize} (physical=${physicalMax}, scale=${scale.toFixed(3)}, ${void 0 !== this.screenshotResizeScale ? 'from screenshotResizeScale' : 'from 1/dpr'})`);
|
|
568
|
-
}
|
|
551
|
+
const maxSize = config?.maxSize ?? scrcpy_manager.o.maxSize;
|
|
552
|
+
const videoBitRate = config?.videoBitRate ?? scrcpy_manager.o.videoBitRate;
|
|
569
553
|
this.resolvedConfig = {
|
|
570
554
|
enabled: this.isEnabled(),
|
|
571
555
|
maxSize,
|
|
572
556
|
idleTimeoutMs: config?.idleTimeoutMs ?? scrcpy_manager.o.idleTimeoutMs,
|
|
573
|
-
videoBitRate
|
|
557
|
+
videoBitRate
|
|
574
558
|
};
|
|
575
559
|
return this.resolvedConfig;
|
|
576
560
|
}
|
|
@@ -605,8 +589,8 @@ class ScrcpyDeviceAdapter {
|
|
|
605
589
|
}
|
|
606
590
|
async screenshotBase64(deviceInfo) {
|
|
607
591
|
const manager = await this.ensureManager(deviceInfo);
|
|
608
|
-
const screenshotBuffer = await manager.
|
|
609
|
-
return createImgBase64ByFormat('
|
|
592
|
+
const screenshotBuffer = await manager.getScreenshotJpeg();
|
|
593
|
+
return createImgBase64ByFormat('jpeg', screenshotBuffer.toString('base64'));
|
|
610
594
|
}
|
|
611
595
|
getResolution() {
|
|
612
596
|
return this.manager?.getResolution() ?? null;
|
|
@@ -1113,17 +1097,6 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1113
1097
|
}
|
|
1114
1098
|
async size() {
|
|
1115
1099
|
const deviceInfo = await this.getDevicePhysicalInfo();
|
|
1116
|
-
const adapter = this.getScrcpyAdapter();
|
|
1117
|
-
if (adapter.isEnabled()) {
|
|
1118
|
-
const scrcpySize = adapter.getSize(deviceInfo);
|
|
1119
|
-
if (scrcpySize) {
|
|
1120
|
-
const isLandscape = 1 === deviceInfo.orientation || 3 === deviceInfo.orientation;
|
|
1121
|
-
const shouldSwap = true !== deviceInfo.isCurrentOrientation && isLandscape;
|
|
1122
|
-
const physicalWidth = shouldSwap ? deviceInfo.physicalHeight : deviceInfo.physicalWidth;
|
|
1123
|
-
this.scalingRatio = adapter.getScalingRatio(physicalWidth) ?? this.scalingRatio;
|
|
1124
|
-
return scrcpySize;
|
|
1125
|
-
}
|
|
1126
|
-
}
|
|
1127
1100
|
const isLandscape = 1 === deviceInfo.orientation || 3 === deviceInfo.orientation;
|
|
1128
1101
|
const shouldSwap = true !== deviceInfo.isCurrentOrientation && isLandscape;
|
|
1129
1102
|
const width = shouldSwap ? deviceInfo.physicalHeight : deviceInfo.physicalWidth;
|
|
@@ -1192,7 +1165,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1192
1165
|
screenshotBuffer = await adb.takeScreenshot(null);
|
|
1193
1166
|
debugDevice('adb.takeScreenshot completed');
|
|
1194
1167
|
if (!screenshotBuffer) throw new Error('Failed to capture screenshot: screenshotBuffer is null');
|
|
1195
|
-
if (!
|
|
1168
|
+
if (!isValidImageBuffer(screenshotBuffer)) {
|
|
1196
1169
|
debugDevice('Invalid image buffer detected: not a valid image format');
|
|
1197
1170
|
throw new Error('Screenshot buffer has invalid format: could not find valid image signature');
|
|
1198
1171
|
}
|
|
@@ -1223,7 +1196,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1223
1196
|
screenshotBuffer = await external_node_fs_["default"].promises.readFile(screenshotPath);
|
|
1224
1197
|
const validScreenshotBufferSize = this.options?.minScreenshotBufferSize ?? 10240;
|
|
1225
1198
|
if (!screenshotBuffer || validScreenshotBufferSize > 0 && screenshotBuffer.length < validScreenshotBufferSize) throw new Error(`Fallback screenshot validation failed: buffer size ${screenshotBuffer?.length || 0} bytes (minimum: ${validScreenshotBufferSize})`);
|
|
1226
|
-
if (!
|
|
1199
|
+
if (!isValidImageBuffer(screenshotBuffer)) throw new Error('Fallback screenshot buffer has invalid PNG format');
|
|
1227
1200
|
debugDevice(`Fallback screenshot validated successfully: ${screenshotBuffer.length} bytes`);
|
|
1228
1201
|
} finally{
|
|
1229
1202
|
Promise.resolve().then(()=>adb.shell(`rm ${androidScreenshotPath}`)).catch((error)=>{
|
package/dist/lib/index.js
CHANGED
|
@@ -24,18 +24,16 @@ var __webpack_modules__ = {
|
|
|
24
24
|
return obj;
|
|
25
25
|
}
|
|
26
26
|
const debugScrcpy = (0, _midscene_shared_logger__rspack_import_3.getDebug)('android:scrcpy');
|
|
27
|
+
const warnScrcpy = (0, _midscene_shared_logger__rspack_import_3.getDebug)('android:scrcpy', {
|
|
28
|
+
console: true
|
|
29
|
+
});
|
|
27
30
|
const NAL_TYPE_IDR = 5;
|
|
28
31
|
const NAL_TYPE_SPS = 7;
|
|
29
32
|
const NAL_TYPE_PPS = 8;
|
|
30
33
|
const NAL_TYPE_MASK = 0x1f;
|
|
31
|
-
const START_CODE_4_BYTE = Buffer.from([
|
|
32
|
-
0x00,
|
|
33
|
-
0x00,
|
|
34
|
-
0x00,
|
|
35
|
-
0x01
|
|
36
|
-
]);
|
|
37
34
|
const DEFAULT_MAX_SIZE = 0;
|
|
38
|
-
const DEFAULT_VIDEO_BIT_RATE =
|
|
35
|
+
const DEFAULT_VIDEO_BIT_RATE = 100000000;
|
|
36
|
+
const MAX_VIDEO_BIT_RATE = 100000000;
|
|
39
37
|
const DEFAULT_IDLE_TIMEOUT_MS = 30000;
|
|
40
38
|
const MAX_KEYFRAME_WAIT_MS = 5000;
|
|
41
39
|
const FRESH_FRAME_TIMEOUT_MS = 300;
|
|
@@ -83,8 +81,7 @@ var __webpack_modules__ = {
|
|
|
83
81
|
debugScrcpy('Starting scrcpy connection...');
|
|
84
82
|
const { AdbScrcpyClient, AdbScrcpyOptions2_1 } = await import("@yume-chan/adb-scrcpy");
|
|
85
83
|
const { ReadableStream } = await import("@yume-chan/stream-extra");
|
|
86
|
-
const { ScrcpyOptions3_1, DefaultServerPath
|
|
87
|
-
this.h264SearchConfigFn = h264SearchConfiguration;
|
|
84
|
+
const { ScrcpyOptions3_1, DefaultServerPath } = await import("@yume-chan/scrcpy");
|
|
88
85
|
const serverBinPath = this.resolveServerBinPath();
|
|
89
86
|
await AdbScrcpyClient.pushServer(this.adb, ReadableStream.from((0, node_fs__rspack_import_0.createReadStream)(serverBinPath)));
|
|
90
87
|
const scrcpyOptions = new ScrcpyOptions3_1({
|
|
@@ -92,8 +89,9 @@ var __webpack_modules__ = {
|
|
|
92
89
|
control: false,
|
|
93
90
|
maxSize: this.options.maxSize,
|
|
94
91
|
videoBitRate: this.options.videoBitRate,
|
|
95
|
-
|
|
96
|
-
|
|
92
|
+
maxFps: 10,
|
|
93
|
+
sendFrameMeta: true,
|
|
94
|
+
videoCodecOptions: 'i-frame-interval=0,bitrate-mode=2'
|
|
97
95
|
});
|
|
98
96
|
this.scrcpyClient = await AdbScrcpyClient.start(this.adb, DefaultServerPath, new AdbScrcpyOptions2_1(scrcpyOptions));
|
|
99
97
|
const videoStreamPromise = this.scrcpyClient.videoStream;
|
|
@@ -151,10 +149,14 @@ var __webpack_modules__ = {
|
|
|
151
149
|
}
|
|
152
150
|
}
|
|
153
151
|
processFrame(packet) {
|
|
152
|
+
if ('configuration' === packet.type) {
|
|
153
|
+
this.spsHeader = Buffer.from(packet.data);
|
|
154
|
+
debugScrcpy(`Received SPS/PPS configuration: ${this.spsHeader.length}B`);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
154
157
|
const frameBuffer = Buffer.from(packet.data);
|
|
155
|
-
const
|
|
156
|
-
if (
|
|
157
|
-
if (actualKeyFrame && this.spsHeader) {
|
|
158
|
+
const isKeyFrame = detectH264KeyFrame(frameBuffer);
|
|
159
|
+
if (isKeyFrame && this.spsHeader) {
|
|
158
160
|
this.lastRawKeyframe = frameBuffer;
|
|
159
161
|
if (this.keyframeResolvers.length > 0) {
|
|
160
162
|
const combined = Buffer.concat([
|
|
@@ -165,23 +167,7 @@ var __webpack_modules__ = {
|
|
|
165
167
|
}
|
|
166
168
|
}
|
|
167
169
|
}
|
|
168
|
-
|
|
169
|
-
if (!this.h264SearchConfigFn) return;
|
|
170
|
-
try {
|
|
171
|
-
const config = this.h264SearchConfigFn(new Uint8Array(frameBuffer));
|
|
172
|
-
if (!config.sequenceParameterSet || !config.pictureParameterSet) return;
|
|
173
|
-
this.spsHeader = Buffer.concat([
|
|
174
|
-
START_CODE_4_BYTE,
|
|
175
|
-
Buffer.from(config.sequenceParameterSet),
|
|
176
|
-
START_CODE_4_BYTE,
|
|
177
|
-
Buffer.from(config.pictureParameterSet)
|
|
178
|
-
]);
|
|
179
|
-
debugScrcpy(`Extracted SPS/PPS: SPS=${config.sequenceParameterSet.length}B, PPS=${config.pictureParameterSet.length}B, total=${this.spsHeader.length}B`);
|
|
180
|
-
} catch (error) {
|
|
181
|
-
debugScrcpy(`Failed to extract SPS/PPS from keyframe: ${error}`);
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
async getScreenshotPng() {
|
|
170
|
+
async getScreenshotJpeg() {
|
|
185
171
|
const perfStart = Date.now();
|
|
186
172
|
const t1 = Date.now();
|
|
187
173
|
await this.ensureConnected();
|
|
@@ -211,7 +197,7 @@ var __webpack_modules__ = {
|
|
|
211
197
|
this.resetIdleTimer();
|
|
212
198
|
debugScrcpy(`Decoding H.264 stream: ${keyframeBuffer.length} bytes (${frameSource})`);
|
|
213
199
|
const t4 = Date.now();
|
|
214
|
-
const result = await this.
|
|
200
|
+
const result = await this.decodeH264ToJpeg(keyframeBuffer);
|
|
215
201
|
const decodeTime = Date.now() - t4;
|
|
216
202
|
const totalTime = Date.now() - perfStart;
|
|
217
203
|
debugScrcpy(`Performance: total=${totalTime}ms (connect=${connectTime}ms, spsWait=${spsWaitTime}ms, frameWait=${frameWaitTime}ms[${frameSource}], decode=${decodeTime}ms)`);
|
|
@@ -274,7 +260,7 @@ var __webpack_modules__ = {
|
|
|
274
260
|
return false;
|
|
275
261
|
}
|
|
276
262
|
}
|
|
277
|
-
async
|
|
263
|
+
async decodeH264ToJpeg(h264Buffer) {
|
|
278
264
|
const { spawn } = await import("node:child_process");
|
|
279
265
|
return new Promise((resolve, reject)=>{
|
|
280
266
|
const ffmpegArgs = [
|
|
@@ -287,7 +273,9 @@ var __webpack_modules__ = {
|
|
|
287
273
|
'-f',
|
|
288
274
|
'image2pipe',
|
|
289
275
|
'-vcodec',
|
|
290
|
-
'
|
|
276
|
+
'mjpeg',
|
|
277
|
+
'-q:v',
|
|
278
|
+
'5',
|
|
291
279
|
'-loglevel',
|
|
292
280
|
'error',
|
|
293
281
|
'pipe:1'
|
|
@@ -310,13 +298,13 @@ var __webpack_modules__ = {
|
|
|
310
298
|
});
|
|
311
299
|
ffmpeg.on('close', (code)=>{
|
|
312
300
|
if (0 === code && chunks.length > 0) {
|
|
313
|
-
const
|
|
314
|
-
debugScrcpy(`FFmpeg decode successful,
|
|
315
|
-
resolve(
|
|
301
|
+
const jpegBuffer = Buffer.concat(chunks);
|
|
302
|
+
debugScrcpy(`FFmpeg decode successful, JPEG size: ${jpegBuffer.length} bytes`);
|
|
303
|
+
resolve(jpegBuffer);
|
|
316
304
|
} else {
|
|
317
305
|
const errorMsg = stderrOutput || `FFmpeg exited with code ${code}`;
|
|
318
306
|
debugScrcpy(`FFmpeg decode failed: ${errorMsg}`);
|
|
319
|
-
reject(new Error(`H.264 to
|
|
307
|
+
reject(new Error(`H.264 to JPEG decode failed: ${errorMsg}`));
|
|
320
308
|
}
|
|
321
309
|
});
|
|
322
310
|
ffmpeg.on('error', (error)=>{
|
|
@@ -348,7 +336,6 @@ var __webpack_modules__ = {
|
|
|
348
336
|
this.spsHeader = null;
|
|
349
337
|
this.lastRawKeyframe = null;
|
|
350
338
|
this.isInitialized = false;
|
|
351
|
-
this.h264SearchConfigFn = null;
|
|
352
339
|
this.keyframeResolvers = [];
|
|
353
340
|
if (reader) try {
|
|
354
341
|
reader.cancel();
|
|
@@ -376,12 +363,14 @@ var __webpack_modules__ = {
|
|
|
376
363
|
_define_property(this, "keyframeResolvers", []);
|
|
377
364
|
_define_property(this, "lastRawKeyframe", null);
|
|
378
365
|
_define_property(this, "videoResolution", null);
|
|
379
|
-
_define_property(this, "h264SearchConfigFn", null);
|
|
380
366
|
_define_property(this, "streamReader", null);
|
|
381
367
|
this.adb = adb;
|
|
368
|
+
const requestedBitRate = options.videoBitRate ?? DEFAULT_VIDEO_BIT_RATE;
|
|
369
|
+
const clampedBitRate = Math.min(requestedBitRate, MAX_VIDEO_BIT_RATE);
|
|
370
|
+
if (requestedBitRate > MAX_VIDEO_BIT_RATE) warnScrcpy(`videoBitRate ${requestedBitRate} exceeds maximum ${MAX_VIDEO_BIT_RATE}, clamped to ${clampedBitRate}`);
|
|
382
371
|
this.options = {
|
|
383
372
|
maxSize: options.maxSize ?? DEFAULT_MAX_SIZE,
|
|
384
|
-
videoBitRate:
|
|
373
|
+
videoBitRate: clampedBitRate,
|
|
385
374
|
idleTimeoutMs: options.idleTimeoutMs ?? DEFAULT_IDLE_TIMEOUT_MS
|
|
386
375
|
};
|
|
387
376
|
}
|
|
@@ -495,18 +484,13 @@ var __webpack_exports__ = {};
|
|
|
495
484
|
resolveConfig(deviceInfo) {
|
|
496
485
|
if (this.resolvedConfig) return this.resolvedConfig;
|
|
497
486
|
const config = this.scrcpyConfig;
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
const physicalMax = Math.max(deviceInfo.physicalWidth, deviceInfo.physicalHeight);
|
|
501
|
-
const scale = this.screenshotResizeScale ?? 1 / deviceInfo.dpr;
|
|
502
|
-
maxSize = Math.round(physicalMax * scale);
|
|
503
|
-
debugAdapter(`Auto-calculated maxSize: ${maxSize} (physical=${physicalMax}, scale=${scale.toFixed(3)}, ${void 0 !== this.screenshotResizeScale ? 'from screenshotResizeScale' : 'from 1/dpr'})`);
|
|
504
|
-
}
|
|
487
|
+
const maxSize = config?.maxSize ?? scrcpy_manager.o.maxSize;
|
|
488
|
+
const videoBitRate = config?.videoBitRate ?? scrcpy_manager.o.videoBitRate;
|
|
505
489
|
this.resolvedConfig = {
|
|
506
490
|
enabled: this.isEnabled(),
|
|
507
491
|
maxSize,
|
|
508
492
|
idleTimeoutMs: config?.idleTimeoutMs ?? scrcpy_manager.o.idleTimeoutMs,
|
|
509
|
-
videoBitRate
|
|
493
|
+
videoBitRate
|
|
510
494
|
};
|
|
511
495
|
return this.resolvedConfig;
|
|
512
496
|
}
|
|
@@ -541,8 +525,8 @@ var __webpack_exports__ = {};
|
|
|
541
525
|
}
|
|
542
526
|
async screenshotBase64(deviceInfo) {
|
|
543
527
|
const manager = await this.ensureManager(deviceInfo);
|
|
544
|
-
const screenshotBuffer = await manager.
|
|
545
|
-
return (0, img_namespaceObject.createImgBase64ByFormat)('
|
|
528
|
+
const screenshotBuffer = await manager.getScreenshotJpeg();
|
|
529
|
+
return (0, img_namespaceObject.createImgBase64ByFormat)('jpeg', screenshotBuffer.toString('base64'));
|
|
546
530
|
}
|
|
547
531
|
getResolution() {
|
|
548
532
|
return this.manager?.getResolution() ?? null;
|
|
@@ -1049,17 +1033,6 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1049
1033
|
}
|
|
1050
1034
|
async size() {
|
|
1051
1035
|
const deviceInfo = await this.getDevicePhysicalInfo();
|
|
1052
|
-
const adapter = this.getScrcpyAdapter();
|
|
1053
|
-
if (adapter.isEnabled()) {
|
|
1054
|
-
const scrcpySize = adapter.getSize(deviceInfo);
|
|
1055
|
-
if (scrcpySize) {
|
|
1056
|
-
const isLandscape = 1 === deviceInfo.orientation || 3 === deviceInfo.orientation;
|
|
1057
|
-
const shouldSwap = true !== deviceInfo.isCurrentOrientation && isLandscape;
|
|
1058
|
-
const physicalWidth = shouldSwap ? deviceInfo.physicalHeight : deviceInfo.physicalWidth;
|
|
1059
|
-
this.scalingRatio = adapter.getScalingRatio(physicalWidth) ?? this.scalingRatio;
|
|
1060
|
-
return scrcpySize;
|
|
1061
|
-
}
|
|
1062
|
-
}
|
|
1063
1036
|
const isLandscape = 1 === deviceInfo.orientation || 3 === deviceInfo.orientation;
|
|
1064
1037
|
const shouldSwap = true !== deviceInfo.isCurrentOrientation && isLandscape;
|
|
1065
1038
|
const width = shouldSwap ? deviceInfo.physicalHeight : deviceInfo.physicalWidth;
|
|
@@ -1128,7 +1101,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1128
1101
|
screenshotBuffer = await adb.takeScreenshot(null);
|
|
1129
1102
|
debugDevice('adb.takeScreenshot completed');
|
|
1130
1103
|
if (!screenshotBuffer) throw new Error('Failed to capture screenshot: screenshotBuffer is null');
|
|
1131
|
-
if (!(0, img_namespaceObject.
|
|
1104
|
+
if (!(0, img_namespaceObject.isValidImageBuffer)(screenshotBuffer)) {
|
|
1132
1105
|
debugDevice('Invalid image buffer detected: not a valid image format');
|
|
1133
1106
|
throw new Error('Screenshot buffer has invalid format: could not find valid image signature');
|
|
1134
1107
|
}
|
|
@@ -1159,7 +1132,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1159
1132
|
screenshotBuffer = await external_node_fs_default().promises.readFile(screenshotPath);
|
|
1160
1133
|
const validScreenshotBufferSize = this.options?.minScreenshotBufferSize ?? 10240;
|
|
1161
1134
|
if (!screenshotBuffer || validScreenshotBufferSize > 0 && screenshotBuffer.length < validScreenshotBufferSize) throw new Error(`Fallback screenshot validation failed: buffer size ${screenshotBuffer?.length || 0} bytes (minimum: ${validScreenshotBufferSize})`);
|
|
1162
|
-
if (!(0, img_namespaceObject.
|
|
1135
|
+
if (!(0, img_namespaceObject.isValidImageBuffer)(screenshotBuffer)) throw new Error('Fallback screenshot buffer has invalid PNG format');
|
|
1163
1136
|
debugDevice(`Fallback screenshot validated successfully: ${screenshotBuffer.length} bytes`);
|
|
1164
1137
|
} finally{
|
|
1165
1138
|
Promise.resolve().then(()=>adb.shell(`rm ${androidScreenshotPath}`)).catch((error)=>{
|
package/dist/lib/mcp-server.js
CHANGED
|
@@ -24,18 +24,16 @@ var __webpack_modules__ = {
|
|
|
24
24
|
return obj;
|
|
25
25
|
}
|
|
26
26
|
const debugScrcpy = (0, _midscene_shared_logger__rspack_import_3.getDebug)('android:scrcpy');
|
|
27
|
+
const warnScrcpy = (0, _midscene_shared_logger__rspack_import_3.getDebug)('android:scrcpy', {
|
|
28
|
+
console: true
|
|
29
|
+
});
|
|
27
30
|
const NAL_TYPE_IDR = 5;
|
|
28
31
|
const NAL_TYPE_SPS = 7;
|
|
29
32
|
const NAL_TYPE_PPS = 8;
|
|
30
33
|
const NAL_TYPE_MASK = 0x1f;
|
|
31
|
-
const START_CODE_4_BYTE = Buffer.from([
|
|
32
|
-
0x00,
|
|
33
|
-
0x00,
|
|
34
|
-
0x00,
|
|
35
|
-
0x01
|
|
36
|
-
]);
|
|
37
34
|
const DEFAULT_MAX_SIZE = 0;
|
|
38
|
-
const DEFAULT_VIDEO_BIT_RATE =
|
|
35
|
+
const DEFAULT_VIDEO_BIT_RATE = 100000000;
|
|
36
|
+
const MAX_VIDEO_BIT_RATE = 100000000;
|
|
39
37
|
const DEFAULT_IDLE_TIMEOUT_MS = 30000;
|
|
40
38
|
const MAX_KEYFRAME_WAIT_MS = 5000;
|
|
41
39
|
const FRESH_FRAME_TIMEOUT_MS = 300;
|
|
@@ -83,8 +81,7 @@ var __webpack_modules__ = {
|
|
|
83
81
|
debugScrcpy('Starting scrcpy connection...');
|
|
84
82
|
const { AdbScrcpyClient, AdbScrcpyOptions2_1 } = await import("@yume-chan/adb-scrcpy");
|
|
85
83
|
const { ReadableStream } = await import("@yume-chan/stream-extra");
|
|
86
|
-
const { ScrcpyOptions3_1, DefaultServerPath
|
|
87
|
-
this.h264SearchConfigFn = h264SearchConfiguration;
|
|
84
|
+
const { ScrcpyOptions3_1, DefaultServerPath } = await import("@yume-chan/scrcpy");
|
|
88
85
|
const serverBinPath = this.resolveServerBinPath();
|
|
89
86
|
await AdbScrcpyClient.pushServer(this.adb, ReadableStream.from((0, node_fs__rspack_import_0.createReadStream)(serverBinPath)));
|
|
90
87
|
const scrcpyOptions = new ScrcpyOptions3_1({
|
|
@@ -92,8 +89,9 @@ var __webpack_modules__ = {
|
|
|
92
89
|
control: false,
|
|
93
90
|
maxSize: this.options.maxSize,
|
|
94
91
|
videoBitRate: this.options.videoBitRate,
|
|
95
|
-
|
|
96
|
-
|
|
92
|
+
maxFps: 10,
|
|
93
|
+
sendFrameMeta: true,
|
|
94
|
+
videoCodecOptions: 'i-frame-interval=0,bitrate-mode=2'
|
|
97
95
|
});
|
|
98
96
|
this.scrcpyClient = await AdbScrcpyClient.start(this.adb, DefaultServerPath, new AdbScrcpyOptions2_1(scrcpyOptions));
|
|
99
97
|
const videoStreamPromise = this.scrcpyClient.videoStream;
|
|
@@ -151,10 +149,14 @@ var __webpack_modules__ = {
|
|
|
151
149
|
}
|
|
152
150
|
}
|
|
153
151
|
processFrame(packet) {
|
|
152
|
+
if ('configuration' === packet.type) {
|
|
153
|
+
this.spsHeader = Buffer.from(packet.data);
|
|
154
|
+
debugScrcpy(`Received SPS/PPS configuration: ${this.spsHeader.length}B`);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
154
157
|
const frameBuffer = Buffer.from(packet.data);
|
|
155
|
-
const
|
|
156
|
-
if (
|
|
157
|
-
if (actualKeyFrame && this.spsHeader) {
|
|
158
|
+
const isKeyFrame = detectH264KeyFrame(frameBuffer);
|
|
159
|
+
if (isKeyFrame && this.spsHeader) {
|
|
158
160
|
this.lastRawKeyframe = frameBuffer;
|
|
159
161
|
if (this.keyframeResolvers.length > 0) {
|
|
160
162
|
const combined = Buffer.concat([
|
|
@@ -165,23 +167,7 @@ var __webpack_modules__ = {
|
|
|
165
167
|
}
|
|
166
168
|
}
|
|
167
169
|
}
|
|
168
|
-
|
|
169
|
-
if (!this.h264SearchConfigFn) return;
|
|
170
|
-
try {
|
|
171
|
-
const config = this.h264SearchConfigFn(new Uint8Array(frameBuffer));
|
|
172
|
-
if (!config.sequenceParameterSet || !config.pictureParameterSet) return;
|
|
173
|
-
this.spsHeader = Buffer.concat([
|
|
174
|
-
START_CODE_4_BYTE,
|
|
175
|
-
Buffer.from(config.sequenceParameterSet),
|
|
176
|
-
START_CODE_4_BYTE,
|
|
177
|
-
Buffer.from(config.pictureParameterSet)
|
|
178
|
-
]);
|
|
179
|
-
debugScrcpy(`Extracted SPS/PPS: SPS=${config.sequenceParameterSet.length}B, PPS=${config.pictureParameterSet.length}B, total=${this.spsHeader.length}B`);
|
|
180
|
-
} catch (error) {
|
|
181
|
-
debugScrcpy(`Failed to extract SPS/PPS from keyframe: ${error}`);
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
async getScreenshotPng() {
|
|
170
|
+
async getScreenshotJpeg() {
|
|
185
171
|
const perfStart = Date.now();
|
|
186
172
|
const t1 = Date.now();
|
|
187
173
|
await this.ensureConnected();
|
|
@@ -211,7 +197,7 @@ var __webpack_modules__ = {
|
|
|
211
197
|
this.resetIdleTimer();
|
|
212
198
|
debugScrcpy(`Decoding H.264 stream: ${keyframeBuffer.length} bytes (${frameSource})`);
|
|
213
199
|
const t4 = Date.now();
|
|
214
|
-
const result = await this.
|
|
200
|
+
const result = await this.decodeH264ToJpeg(keyframeBuffer);
|
|
215
201
|
const decodeTime = Date.now() - t4;
|
|
216
202
|
const totalTime = Date.now() - perfStart;
|
|
217
203
|
debugScrcpy(`Performance: total=${totalTime}ms (connect=${connectTime}ms, spsWait=${spsWaitTime}ms, frameWait=${frameWaitTime}ms[${frameSource}], decode=${decodeTime}ms)`);
|
|
@@ -274,7 +260,7 @@ var __webpack_modules__ = {
|
|
|
274
260
|
return false;
|
|
275
261
|
}
|
|
276
262
|
}
|
|
277
|
-
async
|
|
263
|
+
async decodeH264ToJpeg(h264Buffer) {
|
|
278
264
|
const { spawn } = await import("node:child_process");
|
|
279
265
|
return new Promise((resolve, reject)=>{
|
|
280
266
|
const ffmpegArgs = [
|
|
@@ -287,7 +273,9 @@ var __webpack_modules__ = {
|
|
|
287
273
|
'-f',
|
|
288
274
|
'image2pipe',
|
|
289
275
|
'-vcodec',
|
|
290
|
-
'
|
|
276
|
+
'mjpeg',
|
|
277
|
+
'-q:v',
|
|
278
|
+
'5',
|
|
291
279
|
'-loglevel',
|
|
292
280
|
'error',
|
|
293
281
|
'pipe:1'
|
|
@@ -310,13 +298,13 @@ var __webpack_modules__ = {
|
|
|
310
298
|
});
|
|
311
299
|
ffmpeg.on('close', (code)=>{
|
|
312
300
|
if (0 === code && chunks.length > 0) {
|
|
313
|
-
const
|
|
314
|
-
debugScrcpy(`FFmpeg decode successful,
|
|
315
|
-
resolve(
|
|
301
|
+
const jpegBuffer = Buffer.concat(chunks);
|
|
302
|
+
debugScrcpy(`FFmpeg decode successful, JPEG size: ${jpegBuffer.length} bytes`);
|
|
303
|
+
resolve(jpegBuffer);
|
|
316
304
|
} else {
|
|
317
305
|
const errorMsg = stderrOutput || `FFmpeg exited with code ${code}`;
|
|
318
306
|
debugScrcpy(`FFmpeg decode failed: ${errorMsg}`);
|
|
319
|
-
reject(new Error(`H.264 to
|
|
307
|
+
reject(new Error(`H.264 to JPEG decode failed: ${errorMsg}`));
|
|
320
308
|
}
|
|
321
309
|
});
|
|
322
310
|
ffmpeg.on('error', (error)=>{
|
|
@@ -348,7 +336,6 @@ var __webpack_modules__ = {
|
|
|
348
336
|
this.spsHeader = null;
|
|
349
337
|
this.lastRawKeyframe = null;
|
|
350
338
|
this.isInitialized = false;
|
|
351
|
-
this.h264SearchConfigFn = null;
|
|
352
339
|
this.keyframeResolvers = [];
|
|
353
340
|
if (reader) try {
|
|
354
341
|
reader.cancel();
|
|
@@ -376,12 +363,14 @@ var __webpack_modules__ = {
|
|
|
376
363
|
_define_property(this, "keyframeResolvers", []);
|
|
377
364
|
_define_property(this, "lastRawKeyframe", null);
|
|
378
365
|
_define_property(this, "videoResolution", null);
|
|
379
|
-
_define_property(this, "h264SearchConfigFn", null);
|
|
380
366
|
_define_property(this, "streamReader", null);
|
|
381
367
|
this.adb = adb;
|
|
368
|
+
const requestedBitRate = options.videoBitRate ?? DEFAULT_VIDEO_BIT_RATE;
|
|
369
|
+
const clampedBitRate = Math.min(requestedBitRate, MAX_VIDEO_BIT_RATE);
|
|
370
|
+
if (requestedBitRate > MAX_VIDEO_BIT_RATE) warnScrcpy(`videoBitRate ${requestedBitRate} exceeds maximum ${MAX_VIDEO_BIT_RATE}, clamped to ${clampedBitRate}`);
|
|
382
371
|
this.options = {
|
|
383
372
|
maxSize: options.maxSize ?? DEFAULT_MAX_SIZE,
|
|
384
|
-
videoBitRate:
|
|
373
|
+
videoBitRate: clampedBitRate,
|
|
385
374
|
idleTimeoutMs: options.idleTimeoutMs ?? DEFAULT_IDLE_TIMEOUT_MS
|
|
386
375
|
};
|
|
387
376
|
}
|
|
@@ -590,18 +579,13 @@ var __webpack_exports__ = {};
|
|
|
590
579
|
resolveConfig(deviceInfo) {
|
|
591
580
|
if (this.resolvedConfig) return this.resolvedConfig;
|
|
592
581
|
const config = this.scrcpyConfig;
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
const physicalMax = Math.max(deviceInfo.physicalWidth, deviceInfo.physicalHeight);
|
|
596
|
-
const scale = this.screenshotResizeScale ?? 1 / deviceInfo.dpr;
|
|
597
|
-
maxSize = Math.round(physicalMax * scale);
|
|
598
|
-
debugAdapter(`Auto-calculated maxSize: ${maxSize} (physical=${physicalMax}, scale=${scale.toFixed(3)}, ${void 0 !== this.screenshotResizeScale ? 'from screenshotResizeScale' : 'from 1/dpr'})`);
|
|
599
|
-
}
|
|
582
|
+
const maxSize = config?.maxSize ?? scrcpy_manager.o.maxSize;
|
|
583
|
+
const videoBitRate = config?.videoBitRate ?? scrcpy_manager.o.videoBitRate;
|
|
600
584
|
this.resolvedConfig = {
|
|
601
585
|
enabled: this.isEnabled(),
|
|
602
586
|
maxSize,
|
|
603
587
|
idleTimeoutMs: config?.idleTimeoutMs ?? scrcpy_manager.o.idleTimeoutMs,
|
|
604
|
-
videoBitRate
|
|
588
|
+
videoBitRate
|
|
605
589
|
};
|
|
606
590
|
return this.resolvedConfig;
|
|
607
591
|
}
|
|
@@ -636,8 +620,8 @@ var __webpack_exports__ = {};
|
|
|
636
620
|
}
|
|
637
621
|
async screenshotBase64(deviceInfo) {
|
|
638
622
|
const manager = await this.ensureManager(deviceInfo);
|
|
639
|
-
const screenshotBuffer = await manager.
|
|
640
|
-
return (0, img_namespaceObject.createImgBase64ByFormat)('
|
|
623
|
+
const screenshotBuffer = await manager.getScreenshotJpeg();
|
|
624
|
+
return (0, img_namespaceObject.createImgBase64ByFormat)('jpeg', screenshotBuffer.toString('base64'));
|
|
641
625
|
}
|
|
642
626
|
getResolution() {
|
|
643
627
|
return this.manager?.getResolution() ?? null;
|
|
@@ -1144,17 +1128,6 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1144
1128
|
}
|
|
1145
1129
|
async size() {
|
|
1146
1130
|
const deviceInfo = await this.getDevicePhysicalInfo();
|
|
1147
|
-
const adapter = this.getScrcpyAdapter();
|
|
1148
|
-
if (adapter.isEnabled()) {
|
|
1149
|
-
const scrcpySize = adapter.getSize(deviceInfo);
|
|
1150
|
-
if (scrcpySize) {
|
|
1151
|
-
const isLandscape = 1 === deviceInfo.orientation || 3 === deviceInfo.orientation;
|
|
1152
|
-
const shouldSwap = true !== deviceInfo.isCurrentOrientation && isLandscape;
|
|
1153
|
-
const physicalWidth = shouldSwap ? deviceInfo.physicalHeight : deviceInfo.physicalWidth;
|
|
1154
|
-
this.scalingRatio = adapter.getScalingRatio(physicalWidth) ?? this.scalingRatio;
|
|
1155
|
-
return scrcpySize;
|
|
1156
|
-
}
|
|
1157
|
-
}
|
|
1158
1131
|
const isLandscape = 1 === deviceInfo.orientation || 3 === deviceInfo.orientation;
|
|
1159
1132
|
const shouldSwap = true !== deviceInfo.isCurrentOrientation && isLandscape;
|
|
1160
1133
|
const width = shouldSwap ? deviceInfo.physicalHeight : deviceInfo.physicalWidth;
|
|
@@ -1223,7 +1196,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1223
1196
|
screenshotBuffer = await adb.takeScreenshot(null);
|
|
1224
1197
|
debugDevice('adb.takeScreenshot completed');
|
|
1225
1198
|
if (!screenshotBuffer) throw new Error('Failed to capture screenshot: screenshotBuffer is null');
|
|
1226
|
-
if (!(0, img_namespaceObject.
|
|
1199
|
+
if (!(0, img_namespaceObject.isValidImageBuffer)(screenshotBuffer)) {
|
|
1227
1200
|
debugDevice('Invalid image buffer detected: not a valid image format');
|
|
1228
1201
|
throw new Error('Screenshot buffer has invalid format: could not find valid image signature');
|
|
1229
1202
|
}
|
|
@@ -1254,7 +1227,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1254
1227
|
screenshotBuffer = await external_node_fs_default().promises.readFile(screenshotPath);
|
|
1255
1228
|
const validScreenshotBufferSize = this.options?.minScreenshotBufferSize ?? 10240;
|
|
1256
1229
|
if (!screenshotBuffer || validScreenshotBufferSize > 0 && screenshotBuffer.length < validScreenshotBufferSize) throw new Error(`Fallback screenshot validation failed: buffer size ${screenshotBuffer?.length || 0} bytes (minimum: ${validScreenshotBufferSize})`);
|
|
1257
|
-
if (!(0, img_namespaceObject.
|
|
1230
|
+
if (!(0, img_namespaceObject.isValidImageBuffer)(screenshotBuffer)) throw new Error('Fallback screenshot buffer has invalid PNG format');
|
|
1258
1231
|
debugDevice(`Fallback screenshot validated successfully: ${screenshotBuffer.length} bytes`);
|
|
1259
1232
|
} finally{
|
|
1260
1233
|
Promise.resolve().then(()=>adb.shell(`rm ${androidScreenshotPath}`)).catch((error)=>{
|
package/dist/types/index.d.ts
CHANGED
|
@@ -242,8 +242,11 @@ export declare class ScrcpyDeviceAdapter {
|
|
|
242
242
|
*/
|
|
243
243
|
initialize(deviceInfo: DevicePhysicalInfo): Promise<void>;
|
|
244
244
|
/**
|
|
245
|
-
* Resolve scrcpy config
|
|
246
|
-
*
|
|
245
|
+
* Resolve scrcpy config.
|
|
246
|
+
* maxSize defaults to 0 (no scaling, full physical resolution) so the Agent layer
|
|
247
|
+
* receives the highest quality image for AI processing.
|
|
248
|
+
* videoBitRate is auto-scaled based on physical pixel count to ensure
|
|
249
|
+
* sufficient quality for all-I-frame H.264 encoding.
|
|
247
250
|
*/
|
|
248
251
|
resolveConfig(deviceInfo: DevicePhysicalInfo): ResolvedScrcpyConfig;
|
|
249
252
|
/**
|
|
@@ -289,7 +292,6 @@ declare class ScrcpyScreenshotManager {
|
|
|
289
292
|
private keyframeResolvers;
|
|
290
293
|
private lastRawKeyframe;
|
|
291
294
|
private videoResolution;
|
|
292
|
-
private h264SearchConfigFn;
|
|
293
295
|
private streamReader;
|
|
294
296
|
constructor(adb: Adb, options?: ScrcpyScreenshotOptions);
|
|
295
297
|
/**
|
|
@@ -320,21 +322,20 @@ declare class ScrcpyScreenshotManager {
|
|
|
320
322
|
*/
|
|
321
323
|
private consumeFramesLoop;
|
|
322
324
|
/**
|
|
323
|
-
* Process a single video
|
|
324
|
-
*
|
|
325
|
-
*
|
|
325
|
+
* Process a single video packet from the scrcpy stream.
|
|
326
|
+
* With sendFrameMeta: true, the stream emits properly framed packets:
|
|
327
|
+
* - "configuration" packets contain SPS/PPS header data
|
|
328
|
+
* - "data" packets contain complete video frames with correct boundaries
|
|
329
|
+
* This avoids the frame-splitting issue that occurs with sendFrameMeta: false
|
|
330
|
+
* at high resolutions where raw chunks may not align with frame boundaries.
|
|
326
331
|
*/
|
|
327
332
|
private processFrame;
|
|
328
333
|
/**
|
|
329
|
-
*
|
|
330
|
-
*/
|
|
331
|
-
private extractSpsHeader;
|
|
332
|
-
/**
|
|
333
|
-
* Get screenshot as PNG.
|
|
334
|
+
* Get screenshot as JPEG.
|
|
334
335
|
* Tries to get a fresh frame within a short timeout. If the screen is static
|
|
335
336
|
* (no new frames arrive), falls back to the latest cached keyframe.
|
|
336
337
|
*/
|
|
337
|
-
|
|
338
|
+
getScreenshotJpeg(): Promise<Buffer>;
|
|
338
339
|
/**
|
|
339
340
|
* Get the actual video stream resolution
|
|
340
341
|
* Returns null if scrcpy is not connected yet
|
|
@@ -364,9 +365,9 @@ declare class ScrcpyScreenshotManager {
|
|
|
364
365
|
*/
|
|
365
366
|
private checkFfmpegAvailable;
|
|
366
367
|
/**
|
|
367
|
-
* Decode H.264 data to
|
|
368
|
+
* Decode H.264 data to JPEG using ffmpeg
|
|
368
369
|
*/
|
|
369
|
-
private
|
|
370
|
+
private decodeH264ToJpeg;
|
|
370
371
|
/**
|
|
371
372
|
* Reset idle timeout timer
|
|
372
373
|
*/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@midscene/android",
|
|
3
|
-
"version": "1.3.11
|
|
3
|
+
"version": "1.3.11",
|
|
4
4
|
"description": "Android automation library for Midscene",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"Android UI automation",
|
|
@@ -38,8 +38,8 @@
|
|
|
38
38
|
"@yume-chan/stream-extra": "^1.0.0",
|
|
39
39
|
"appium-adb": "12.12.1",
|
|
40
40
|
"sharp": "^0.34.3",
|
|
41
|
-
"@midscene/
|
|
42
|
-
"@midscene/
|
|
41
|
+
"@midscene/shared": "1.3.11",
|
|
42
|
+
"@midscene/core": "1.3.11"
|
|
43
43
|
},
|
|
44
44
|
"optionalDependencies": {
|
|
45
45
|
"@ffmpeg-installer/ffmpeg": "^1.1.0"
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"tsx": "^4.19.2",
|
|
54
54
|
"vitest": "3.0.5",
|
|
55
55
|
"zod": "3.24.3",
|
|
56
|
-
"@midscene/playground": "1.3.11
|
|
56
|
+
"@midscene/playground": "1.3.11"
|
|
57
57
|
},
|
|
58
58
|
"license": "MIT",
|
|
59
59
|
"scripts": {
|