@midscene/android 1.3.10-beta-20260210033532.0 → 1.3.10
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 +50 -46
- package/dist/es/mcp-server.mjs +50 -46
- package/dist/lib/index.js +50 -46
- package/dist/lib/mcp-server.js +50 -46
- package/package.json +4 -4
package/dist/es/index.mjs
CHANGED
|
@@ -32,12 +32,10 @@ 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
|
|
36
|
-
const
|
|
37
|
-
const
|
|
38
|
-
const
|
|
39
|
-
const H265_NAL_TYPE_SPS = 33;
|
|
40
|
-
const H265_NAL_TYPE_PPS = 34;
|
|
35
|
+
const NAL_TYPE_IDR = 5;
|
|
36
|
+
const NAL_TYPE_SPS = 7;
|
|
37
|
+
const NAL_TYPE_PPS = 8;
|
|
38
|
+
const NAL_TYPE_MASK = 0x1f;
|
|
41
39
|
const START_CODE_4_BYTE = Buffer.from([
|
|
42
40
|
0x00,
|
|
43
41
|
0x00,
|
|
@@ -45,7 +43,7 @@ var __webpack_modules__ = {
|
|
|
45
43
|
0x01
|
|
46
44
|
]);
|
|
47
45
|
const DEFAULT_MAX_SIZE = 0;
|
|
48
|
-
const DEFAULT_VIDEO_BIT_RATE =
|
|
46
|
+
const DEFAULT_VIDEO_BIT_RATE = 2000000;
|
|
49
47
|
const DEFAULT_IDLE_TIMEOUT_MS = 30000;
|
|
50
48
|
const MAX_KEYFRAME_WAIT_MS = 5000;
|
|
51
49
|
const FRESH_FRAME_TIMEOUT_MS = 300;
|
|
@@ -58,17 +56,17 @@ var __webpack_modules__ = {
|
|
|
58
56
|
idleTimeoutMs: DEFAULT_IDLE_TIMEOUT_MS,
|
|
59
57
|
videoBitRate: DEFAULT_VIDEO_BIT_RATE
|
|
60
58
|
};
|
|
61
|
-
function
|
|
62
|
-
return nalUnitType ===
|
|
59
|
+
function isKeyFrameNalType(nalUnitType) {
|
|
60
|
+
return nalUnitType === NAL_TYPE_IDR || nalUnitType === NAL_TYPE_SPS || nalUnitType === NAL_TYPE_PPS;
|
|
63
61
|
}
|
|
64
|
-
function
|
|
65
|
-
const scanLimit = Math.min(buffer.length -
|
|
62
|
+
function detectH264KeyFrame(buffer) {
|
|
63
|
+
const scanLimit = Math.min(buffer.length - 4, MAX_SCAN_BYTES);
|
|
66
64
|
for(let i = 0; i < scanLimit; i++)if (0x00 === buffer[i] && 0x00 === buffer[i + 1] && 0x00 === buffer[i + 2] && 0x01 === buffer[i + 3]) {
|
|
67
|
-
const nalUnitType = buffer[i + 4]
|
|
68
|
-
if (
|
|
65
|
+
const nalUnitType = buffer[i + 4] & NAL_TYPE_MASK;
|
|
66
|
+
if (isKeyFrameNalType(nalUnitType)) return true;
|
|
69
67
|
} else if (0x00 === buffer[i] && 0x00 === buffer[i + 1] && 0x01 === buffer[i + 2]) {
|
|
70
|
-
const nalUnitType = buffer[i + 3]
|
|
71
|
-
if (
|
|
68
|
+
const nalUnitType = buffer[i + 3] & NAL_TYPE_MASK;
|
|
69
|
+
if (isKeyFrameNalType(nalUnitType)) return true;
|
|
72
70
|
}
|
|
73
71
|
return false;
|
|
74
72
|
}
|
|
@@ -93,8 +91,8 @@ var __webpack_modules__ = {
|
|
|
93
91
|
debugScrcpy('Starting scrcpy connection...');
|
|
94
92
|
const { AdbScrcpyClient, AdbScrcpyOptions2_1 } = await import("@yume-chan/adb-scrcpy");
|
|
95
93
|
const { ReadableStream } = await import("@yume-chan/stream-extra");
|
|
96
|
-
const { ScrcpyOptions3_1, DefaultServerPath,
|
|
97
|
-
this.
|
|
94
|
+
const { ScrcpyOptions3_1, DefaultServerPath, h264SearchConfiguration } = await import("@yume-chan/scrcpy");
|
|
95
|
+
this.h264SearchConfigFn = h264SearchConfiguration;
|
|
98
96
|
const serverBinPath = this.resolveServerBinPath();
|
|
99
97
|
await AdbScrcpyClient.pushServer(this.adb, ReadableStream.from((0, node_fs__rspack_import_0.createReadStream)(serverBinPath)));
|
|
100
98
|
const scrcpyOptions = new ScrcpyOptions3_1({
|
|
@@ -103,7 +101,6 @@ var __webpack_modules__ = {
|
|
|
103
101
|
maxSize: this.options.maxSize,
|
|
104
102
|
videoBitRate: this.options.videoBitRate,
|
|
105
103
|
sendFrameMeta: false,
|
|
106
|
-
videoCodec: 'h265',
|
|
107
104
|
videoCodecOptions: 'i-frame-interval=0'
|
|
108
105
|
});
|
|
109
106
|
this.scrcpyClient = await AdbScrcpyClient.start(this.adb, DefaultServerPath, new AdbScrcpyOptions2_1(scrcpyOptions));
|
|
@@ -163,7 +160,7 @@ var __webpack_modules__ = {
|
|
|
163
160
|
}
|
|
164
161
|
processFrame(packet) {
|
|
165
162
|
const frameBuffer = Buffer.from(packet.data);
|
|
166
|
-
const actualKeyFrame =
|
|
163
|
+
const actualKeyFrame = detectH264KeyFrame(frameBuffer);
|
|
167
164
|
if (actualKeyFrame && !this.spsHeader) this.extractSpsHeader(frameBuffer);
|
|
168
165
|
if (actualKeyFrame && this.spsHeader) {
|
|
169
166
|
this.lastRawKeyframe = frameBuffer;
|
|
@@ -177,21 +174,19 @@ var __webpack_modules__ = {
|
|
|
177
174
|
}
|
|
178
175
|
}
|
|
179
176
|
extractSpsHeader(frameBuffer) {
|
|
180
|
-
if (!this.
|
|
177
|
+
if (!this.h264SearchConfigFn) return;
|
|
181
178
|
try {
|
|
182
|
-
const config = this.
|
|
183
|
-
if (!config.
|
|
179
|
+
const config = this.h264SearchConfigFn(new Uint8Array(frameBuffer));
|
|
180
|
+
if (!config.sequenceParameterSet || !config.pictureParameterSet) return;
|
|
184
181
|
this.spsHeader = Buffer.concat([
|
|
185
182
|
START_CODE_4_BYTE,
|
|
186
|
-
Buffer.from(config.
|
|
183
|
+
Buffer.from(config.sequenceParameterSet),
|
|
187
184
|
START_CODE_4_BYTE,
|
|
188
|
-
Buffer.from(config.
|
|
189
|
-
START_CODE_4_BYTE,
|
|
190
|
-
Buffer.from(config.pictureParameterSet.data)
|
|
185
|
+
Buffer.from(config.pictureParameterSet)
|
|
191
186
|
]);
|
|
192
|
-
debugScrcpy(`Extracted
|
|
187
|
+
debugScrcpy(`Extracted SPS/PPS: SPS=${config.sequenceParameterSet.length}B, PPS=${config.pictureParameterSet.length}B, total=${this.spsHeader.length}B`);
|
|
193
188
|
} catch (error) {
|
|
194
|
-
debugScrcpy(`Failed to extract
|
|
189
|
+
debugScrcpy(`Failed to extract SPS/PPS from keyframe: ${error}`);
|
|
195
190
|
}
|
|
196
191
|
}
|
|
197
192
|
async getScreenshotPng() {
|
|
@@ -222,9 +217,9 @@ var __webpack_modules__ = {
|
|
|
222
217
|
}
|
|
223
218
|
const frameWaitTime = Date.now() - t3;
|
|
224
219
|
this.resetIdleTimer();
|
|
225
|
-
debugScrcpy(`Decoding H.
|
|
220
|
+
debugScrcpy(`Decoding H.264 stream: ${keyframeBuffer.length} bytes (${frameSource})`);
|
|
226
221
|
const t4 = Date.now();
|
|
227
|
-
const result = await this.
|
|
222
|
+
const result = await this.decodeH264ToPng(keyframeBuffer);
|
|
228
223
|
const decodeTime = Date.now() - t4;
|
|
229
224
|
const totalTime = Date.now() - perfStart;
|
|
230
225
|
debugScrcpy(`Performance: total=${totalTime}ms (connect=${connectTime}ms, spsWait=${spsWaitTime}ms, frameWait=${frameWaitTime}ms[${frameSource}], decode=${decodeTime}ms)`);
|
|
@@ -266,7 +261,7 @@ var __webpack_modules__ = {
|
|
|
266
261
|
const startTime = Date.now();
|
|
267
262
|
while(!this.spsHeader && Date.now() - startTime < MAX_KEYFRAME_WAIT_MS){
|
|
268
263
|
const elapsed = Date.now() - startTime;
|
|
269
|
-
debugScrcpy(`Waiting for first keyframe (
|
|
264
|
+
debugScrcpy(`Waiting for first keyframe (SPS/PPS header)... ${elapsed}ms`);
|
|
270
265
|
await new Promise((resolve)=>setTimeout(resolve, KEYFRAME_POLL_INTERVAL_MS));
|
|
271
266
|
}
|
|
272
267
|
if (!this.spsHeader) throw new Error(`No keyframe received within ${MAX_KEYFRAME_WAIT_MS}ms. Device may have a long GOP interval or video encoding issues. Please retry.`);
|
|
@@ -287,12 +282,12 @@ var __webpack_modules__ = {
|
|
|
287
282
|
return false;
|
|
288
283
|
}
|
|
289
284
|
}
|
|
290
|
-
async
|
|
285
|
+
async decodeH264ToPng(h264Buffer) {
|
|
291
286
|
const { spawn } = await import("node:child_process");
|
|
292
287
|
return new Promise((resolve, reject)=>{
|
|
293
288
|
const ffmpegArgs = [
|
|
294
289
|
'-f',
|
|
295
|
-
'
|
|
290
|
+
'h264',
|
|
296
291
|
'-i',
|
|
297
292
|
'pipe:0',
|
|
298
293
|
'-vframes',
|
|
@@ -329,13 +324,13 @@ var __webpack_modules__ = {
|
|
|
329
324
|
} else {
|
|
330
325
|
const errorMsg = stderrOutput || `FFmpeg exited with code ${code}`;
|
|
331
326
|
debugScrcpy(`FFmpeg decode failed: ${errorMsg}`);
|
|
332
|
-
reject(new Error(`H.
|
|
327
|
+
reject(new Error(`H.264 to PNG decode failed: ${errorMsg}`));
|
|
333
328
|
}
|
|
334
329
|
});
|
|
335
330
|
ffmpeg.on('error', (error)=>{
|
|
336
331
|
reject(new Error(`Failed to spawn ffmpeg process: ${error.message}`));
|
|
337
332
|
});
|
|
338
|
-
ffmpeg.stdin.write(
|
|
333
|
+
ffmpeg.stdin.write(h264Buffer);
|
|
339
334
|
ffmpeg.stdin.end();
|
|
340
335
|
});
|
|
341
336
|
}
|
|
@@ -361,7 +356,7 @@ var __webpack_modules__ = {
|
|
|
361
356
|
this.spsHeader = null;
|
|
362
357
|
this.lastRawKeyframe = null;
|
|
363
358
|
this.isInitialized = false;
|
|
364
|
-
this.
|
|
359
|
+
this.h264SearchConfigFn = null;
|
|
365
360
|
this.keyframeResolvers = [];
|
|
366
361
|
if (reader) try {
|
|
367
362
|
reader.cancel();
|
|
@@ -389,7 +384,7 @@ var __webpack_modules__ = {
|
|
|
389
384
|
_define_property(this, "keyframeResolvers", []);
|
|
390
385
|
_define_property(this, "lastRawKeyframe", null);
|
|
391
386
|
_define_property(this, "videoResolution", null);
|
|
392
|
-
_define_property(this, "
|
|
387
|
+
_define_property(this, "h264SearchConfigFn", null);
|
|
393
388
|
_define_property(this, "streamReader", null);
|
|
394
389
|
this.adb = adb;
|
|
395
390
|
this.options = {
|
|
@@ -467,20 +462,18 @@ class ScrcpyDeviceAdapter {
|
|
|
467
462
|
resolveConfig(deviceInfo) {
|
|
468
463
|
if (this.resolvedConfig) return this.resolvedConfig;
|
|
469
464
|
const config = this.scrcpyConfig;
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
const
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
videoBitRate = Math.round(Math.max(scrcpy_manager.o.videoBitRate, scrcpy_manager.o.videoBitRate * ratio));
|
|
477
|
-
debugAdapter(`Auto-scaled videoBitRate: ${(videoBitRate / 1000000).toFixed(1)}Mbps (pixels=${physicalPixels}, ratio=${ratio.toFixed(2)})`);
|
|
465
|
+
let maxSize = config?.maxSize ?? scrcpy_manager.o.maxSize;
|
|
466
|
+
if (config?.maxSize === void 0) {
|
|
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'})`);
|
|
478
471
|
}
|
|
479
472
|
this.resolvedConfig = {
|
|
480
473
|
enabled: this.isEnabled(),
|
|
481
474
|
maxSize,
|
|
482
475
|
idleTimeoutMs: config?.idleTimeoutMs ?? scrcpy_manager.o.idleTimeoutMs,
|
|
483
|
-
videoBitRate
|
|
476
|
+
videoBitRate: config?.videoBitRate ?? scrcpy_manager.o.videoBitRate
|
|
484
477
|
};
|
|
485
478
|
return this.resolvedConfig;
|
|
486
479
|
}
|
|
@@ -1011,6 +1004,17 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1011
1004
|
}
|
|
1012
1005
|
async size() {
|
|
1013
1006
|
const deviceInfo = await this.getDevicePhysicalInfo();
|
|
1007
|
+
const adapter = this.getScrcpyAdapter();
|
|
1008
|
+
if (adapter.isEnabled()) {
|
|
1009
|
+
const scrcpySize = adapter.getSize(deviceInfo);
|
|
1010
|
+
if (scrcpySize) {
|
|
1011
|
+
const isLandscape = 1 === deviceInfo.orientation || 3 === deviceInfo.orientation;
|
|
1012
|
+
const shouldSwap = true !== deviceInfo.isCurrentOrientation && isLandscape;
|
|
1013
|
+
const physicalWidth = shouldSwap ? deviceInfo.physicalHeight : deviceInfo.physicalWidth;
|
|
1014
|
+
this.scalingRatio = adapter.getScalingRatio(physicalWidth) ?? this.scalingRatio;
|
|
1015
|
+
return scrcpySize;
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1014
1018
|
const isLandscape = 1 === deviceInfo.orientation || 3 === deviceInfo.orientation;
|
|
1015
1019
|
const shouldSwap = true !== deviceInfo.isCurrentOrientation && isLandscape;
|
|
1016
1020
|
const width = shouldSwap ? deviceInfo.physicalHeight : deviceInfo.physicalWidth;
|
package/dist/es/mcp-server.mjs
CHANGED
|
@@ -33,12 +33,10 @@ 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
|
|
37
|
-
const
|
|
38
|
-
const
|
|
39
|
-
const
|
|
40
|
-
const H265_NAL_TYPE_SPS = 33;
|
|
41
|
-
const H265_NAL_TYPE_PPS = 34;
|
|
36
|
+
const NAL_TYPE_IDR = 5;
|
|
37
|
+
const NAL_TYPE_SPS = 7;
|
|
38
|
+
const NAL_TYPE_PPS = 8;
|
|
39
|
+
const NAL_TYPE_MASK = 0x1f;
|
|
42
40
|
const START_CODE_4_BYTE = Buffer.from([
|
|
43
41
|
0x00,
|
|
44
42
|
0x00,
|
|
@@ -46,7 +44,7 @@ var __webpack_modules__ = {
|
|
|
46
44
|
0x01
|
|
47
45
|
]);
|
|
48
46
|
const DEFAULT_MAX_SIZE = 0;
|
|
49
|
-
const DEFAULT_VIDEO_BIT_RATE =
|
|
47
|
+
const DEFAULT_VIDEO_BIT_RATE = 2000000;
|
|
50
48
|
const DEFAULT_IDLE_TIMEOUT_MS = 30000;
|
|
51
49
|
const MAX_KEYFRAME_WAIT_MS = 5000;
|
|
52
50
|
const FRESH_FRAME_TIMEOUT_MS = 300;
|
|
@@ -59,17 +57,17 @@ var __webpack_modules__ = {
|
|
|
59
57
|
idleTimeoutMs: DEFAULT_IDLE_TIMEOUT_MS,
|
|
60
58
|
videoBitRate: DEFAULT_VIDEO_BIT_RATE
|
|
61
59
|
};
|
|
62
|
-
function
|
|
63
|
-
return nalUnitType ===
|
|
60
|
+
function isKeyFrameNalType(nalUnitType) {
|
|
61
|
+
return nalUnitType === NAL_TYPE_IDR || nalUnitType === NAL_TYPE_SPS || nalUnitType === NAL_TYPE_PPS;
|
|
64
62
|
}
|
|
65
|
-
function
|
|
66
|
-
const scanLimit = Math.min(buffer.length -
|
|
63
|
+
function detectH264KeyFrame(buffer) {
|
|
64
|
+
const scanLimit = Math.min(buffer.length - 4, MAX_SCAN_BYTES);
|
|
67
65
|
for(let i = 0; i < scanLimit; i++)if (0x00 === buffer[i] && 0x00 === buffer[i + 1] && 0x00 === buffer[i + 2] && 0x01 === buffer[i + 3]) {
|
|
68
|
-
const nalUnitType = buffer[i + 4]
|
|
69
|
-
if (
|
|
66
|
+
const nalUnitType = buffer[i + 4] & NAL_TYPE_MASK;
|
|
67
|
+
if (isKeyFrameNalType(nalUnitType)) return true;
|
|
70
68
|
} else if (0x00 === buffer[i] && 0x00 === buffer[i + 1] && 0x01 === buffer[i + 2]) {
|
|
71
|
-
const nalUnitType = buffer[i + 3]
|
|
72
|
-
if (
|
|
69
|
+
const nalUnitType = buffer[i + 3] & NAL_TYPE_MASK;
|
|
70
|
+
if (isKeyFrameNalType(nalUnitType)) return true;
|
|
73
71
|
}
|
|
74
72
|
return false;
|
|
75
73
|
}
|
|
@@ -94,8 +92,8 @@ var __webpack_modules__ = {
|
|
|
94
92
|
debugScrcpy('Starting scrcpy connection...');
|
|
95
93
|
const { AdbScrcpyClient, AdbScrcpyOptions2_1 } = await import("@yume-chan/adb-scrcpy");
|
|
96
94
|
const { ReadableStream } = await import("@yume-chan/stream-extra");
|
|
97
|
-
const { ScrcpyOptions3_1, DefaultServerPath,
|
|
98
|
-
this.
|
|
95
|
+
const { ScrcpyOptions3_1, DefaultServerPath, h264SearchConfiguration } = await import("@yume-chan/scrcpy");
|
|
96
|
+
this.h264SearchConfigFn = h264SearchConfiguration;
|
|
99
97
|
const serverBinPath = this.resolveServerBinPath();
|
|
100
98
|
await AdbScrcpyClient.pushServer(this.adb, ReadableStream.from((0, node_fs__rspack_import_0.createReadStream)(serverBinPath)));
|
|
101
99
|
const scrcpyOptions = new ScrcpyOptions3_1({
|
|
@@ -104,7 +102,6 @@ var __webpack_modules__ = {
|
|
|
104
102
|
maxSize: this.options.maxSize,
|
|
105
103
|
videoBitRate: this.options.videoBitRate,
|
|
106
104
|
sendFrameMeta: false,
|
|
107
|
-
videoCodec: 'h265',
|
|
108
105
|
videoCodecOptions: 'i-frame-interval=0'
|
|
109
106
|
});
|
|
110
107
|
this.scrcpyClient = await AdbScrcpyClient.start(this.adb, DefaultServerPath, new AdbScrcpyOptions2_1(scrcpyOptions));
|
|
@@ -164,7 +161,7 @@ var __webpack_modules__ = {
|
|
|
164
161
|
}
|
|
165
162
|
processFrame(packet) {
|
|
166
163
|
const frameBuffer = Buffer.from(packet.data);
|
|
167
|
-
const actualKeyFrame =
|
|
164
|
+
const actualKeyFrame = detectH264KeyFrame(frameBuffer);
|
|
168
165
|
if (actualKeyFrame && !this.spsHeader) this.extractSpsHeader(frameBuffer);
|
|
169
166
|
if (actualKeyFrame && this.spsHeader) {
|
|
170
167
|
this.lastRawKeyframe = frameBuffer;
|
|
@@ -178,21 +175,19 @@ var __webpack_modules__ = {
|
|
|
178
175
|
}
|
|
179
176
|
}
|
|
180
177
|
extractSpsHeader(frameBuffer) {
|
|
181
|
-
if (!this.
|
|
178
|
+
if (!this.h264SearchConfigFn) return;
|
|
182
179
|
try {
|
|
183
|
-
const config = this.
|
|
184
|
-
if (!config.
|
|
180
|
+
const config = this.h264SearchConfigFn(new Uint8Array(frameBuffer));
|
|
181
|
+
if (!config.sequenceParameterSet || !config.pictureParameterSet) return;
|
|
185
182
|
this.spsHeader = Buffer.concat([
|
|
186
183
|
START_CODE_4_BYTE,
|
|
187
|
-
Buffer.from(config.
|
|
184
|
+
Buffer.from(config.sequenceParameterSet),
|
|
188
185
|
START_CODE_4_BYTE,
|
|
189
|
-
Buffer.from(config.
|
|
190
|
-
START_CODE_4_BYTE,
|
|
191
|
-
Buffer.from(config.pictureParameterSet.data)
|
|
186
|
+
Buffer.from(config.pictureParameterSet)
|
|
192
187
|
]);
|
|
193
|
-
debugScrcpy(`Extracted
|
|
188
|
+
debugScrcpy(`Extracted SPS/PPS: SPS=${config.sequenceParameterSet.length}B, PPS=${config.pictureParameterSet.length}B, total=${this.spsHeader.length}B`);
|
|
194
189
|
} catch (error) {
|
|
195
|
-
debugScrcpy(`Failed to extract
|
|
190
|
+
debugScrcpy(`Failed to extract SPS/PPS from keyframe: ${error}`);
|
|
196
191
|
}
|
|
197
192
|
}
|
|
198
193
|
async getScreenshotPng() {
|
|
@@ -223,9 +218,9 @@ var __webpack_modules__ = {
|
|
|
223
218
|
}
|
|
224
219
|
const frameWaitTime = Date.now() - t3;
|
|
225
220
|
this.resetIdleTimer();
|
|
226
|
-
debugScrcpy(`Decoding H.
|
|
221
|
+
debugScrcpy(`Decoding H.264 stream: ${keyframeBuffer.length} bytes (${frameSource})`);
|
|
227
222
|
const t4 = Date.now();
|
|
228
|
-
const result = await this.
|
|
223
|
+
const result = await this.decodeH264ToPng(keyframeBuffer);
|
|
229
224
|
const decodeTime = Date.now() - t4;
|
|
230
225
|
const totalTime = Date.now() - perfStart;
|
|
231
226
|
debugScrcpy(`Performance: total=${totalTime}ms (connect=${connectTime}ms, spsWait=${spsWaitTime}ms, frameWait=${frameWaitTime}ms[${frameSource}], decode=${decodeTime}ms)`);
|
|
@@ -267,7 +262,7 @@ var __webpack_modules__ = {
|
|
|
267
262
|
const startTime = Date.now();
|
|
268
263
|
while(!this.spsHeader && Date.now() - startTime < MAX_KEYFRAME_WAIT_MS){
|
|
269
264
|
const elapsed = Date.now() - startTime;
|
|
270
|
-
debugScrcpy(`Waiting for first keyframe (
|
|
265
|
+
debugScrcpy(`Waiting for first keyframe (SPS/PPS header)... ${elapsed}ms`);
|
|
271
266
|
await new Promise((resolve)=>setTimeout(resolve, KEYFRAME_POLL_INTERVAL_MS));
|
|
272
267
|
}
|
|
273
268
|
if (!this.spsHeader) throw new Error(`No keyframe received within ${MAX_KEYFRAME_WAIT_MS}ms. Device may have a long GOP interval or video encoding issues. Please retry.`);
|
|
@@ -288,12 +283,12 @@ var __webpack_modules__ = {
|
|
|
288
283
|
return false;
|
|
289
284
|
}
|
|
290
285
|
}
|
|
291
|
-
async
|
|
286
|
+
async decodeH264ToPng(h264Buffer) {
|
|
292
287
|
const { spawn } = await import("node:child_process");
|
|
293
288
|
return new Promise((resolve, reject)=>{
|
|
294
289
|
const ffmpegArgs = [
|
|
295
290
|
'-f',
|
|
296
|
-
'
|
|
291
|
+
'h264',
|
|
297
292
|
'-i',
|
|
298
293
|
'pipe:0',
|
|
299
294
|
'-vframes',
|
|
@@ -330,13 +325,13 @@ var __webpack_modules__ = {
|
|
|
330
325
|
} else {
|
|
331
326
|
const errorMsg = stderrOutput || `FFmpeg exited with code ${code}`;
|
|
332
327
|
debugScrcpy(`FFmpeg decode failed: ${errorMsg}`);
|
|
333
|
-
reject(new Error(`H.
|
|
328
|
+
reject(new Error(`H.264 to PNG decode failed: ${errorMsg}`));
|
|
334
329
|
}
|
|
335
330
|
});
|
|
336
331
|
ffmpeg.on('error', (error)=>{
|
|
337
332
|
reject(new Error(`Failed to spawn ffmpeg process: ${error.message}`));
|
|
338
333
|
});
|
|
339
|
-
ffmpeg.stdin.write(
|
|
334
|
+
ffmpeg.stdin.write(h264Buffer);
|
|
340
335
|
ffmpeg.stdin.end();
|
|
341
336
|
});
|
|
342
337
|
}
|
|
@@ -362,7 +357,7 @@ var __webpack_modules__ = {
|
|
|
362
357
|
this.spsHeader = null;
|
|
363
358
|
this.lastRawKeyframe = null;
|
|
364
359
|
this.isInitialized = false;
|
|
365
|
-
this.
|
|
360
|
+
this.h264SearchConfigFn = null;
|
|
366
361
|
this.keyframeResolvers = [];
|
|
367
362
|
if (reader) try {
|
|
368
363
|
reader.cancel();
|
|
@@ -390,7 +385,7 @@ var __webpack_modules__ = {
|
|
|
390
385
|
_define_property(this, "keyframeResolvers", []);
|
|
391
386
|
_define_property(this, "lastRawKeyframe", null);
|
|
392
387
|
_define_property(this, "videoResolution", null);
|
|
393
|
-
_define_property(this, "
|
|
388
|
+
_define_property(this, "h264SearchConfigFn", null);
|
|
394
389
|
_define_property(this, "streamReader", null);
|
|
395
390
|
this.adb = adb;
|
|
396
391
|
this.options = {
|
|
@@ -564,20 +559,18 @@ class ScrcpyDeviceAdapter {
|
|
|
564
559
|
resolveConfig(deviceInfo) {
|
|
565
560
|
if (this.resolvedConfig) return this.resolvedConfig;
|
|
566
561
|
const config = this.scrcpyConfig;
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
const
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
videoBitRate = Math.round(Math.max(scrcpy_manager.o.videoBitRate, scrcpy_manager.o.videoBitRate * ratio));
|
|
574
|
-
debugAdapter(`Auto-scaled videoBitRate: ${(videoBitRate / 1000000).toFixed(1)}Mbps (pixels=${physicalPixels}, ratio=${ratio.toFixed(2)})`);
|
|
562
|
+
let maxSize = config?.maxSize ?? scrcpy_manager.o.maxSize;
|
|
563
|
+
if (config?.maxSize === void 0) {
|
|
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'})`);
|
|
575
568
|
}
|
|
576
569
|
this.resolvedConfig = {
|
|
577
570
|
enabled: this.isEnabled(),
|
|
578
571
|
maxSize,
|
|
579
572
|
idleTimeoutMs: config?.idleTimeoutMs ?? scrcpy_manager.o.idleTimeoutMs,
|
|
580
|
-
videoBitRate
|
|
573
|
+
videoBitRate: config?.videoBitRate ?? scrcpy_manager.o.videoBitRate
|
|
581
574
|
};
|
|
582
575
|
return this.resolvedConfig;
|
|
583
576
|
}
|
|
@@ -1108,6 +1101,17 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1108
1101
|
}
|
|
1109
1102
|
async size() {
|
|
1110
1103
|
const deviceInfo = await this.getDevicePhysicalInfo();
|
|
1104
|
+
const adapter = this.getScrcpyAdapter();
|
|
1105
|
+
if (adapter.isEnabled()) {
|
|
1106
|
+
const scrcpySize = adapter.getSize(deviceInfo);
|
|
1107
|
+
if (scrcpySize) {
|
|
1108
|
+
const isLandscape = 1 === deviceInfo.orientation || 3 === deviceInfo.orientation;
|
|
1109
|
+
const shouldSwap = true !== deviceInfo.isCurrentOrientation && isLandscape;
|
|
1110
|
+
const physicalWidth = shouldSwap ? deviceInfo.physicalHeight : deviceInfo.physicalWidth;
|
|
1111
|
+
this.scalingRatio = adapter.getScalingRatio(physicalWidth) ?? this.scalingRatio;
|
|
1112
|
+
return scrcpySize;
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1111
1115
|
const isLandscape = 1 === deviceInfo.orientation || 3 === deviceInfo.orientation;
|
|
1112
1116
|
const shouldSwap = true !== deviceInfo.isCurrentOrientation && isLandscape;
|
|
1113
1117
|
const width = shouldSwap ? deviceInfo.physicalHeight : deviceInfo.physicalWidth;
|
package/dist/lib/index.js
CHANGED
|
@@ -24,12 +24,10 @@ 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
|
|
28
|
-
const
|
|
29
|
-
const
|
|
30
|
-
const
|
|
31
|
-
const H265_NAL_TYPE_SPS = 33;
|
|
32
|
-
const H265_NAL_TYPE_PPS = 34;
|
|
27
|
+
const NAL_TYPE_IDR = 5;
|
|
28
|
+
const NAL_TYPE_SPS = 7;
|
|
29
|
+
const NAL_TYPE_PPS = 8;
|
|
30
|
+
const NAL_TYPE_MASK = 0x1f;
|
|
33
31
|
const START_CODE_4_BYTE = Buffer.from([
|
|
34
32
|
0x00,
|
|
35
33
|
0x00,
|
|
@@ -37,7 +35,7 @@ var __webpack_modules__ = {
|
|
|
37
35
|
0x01
|
|
38
36
|
]);
|
|
39
37
|
const DEFAULT_MAX_SIZE = 0;
|
|
40
|
-
const DEFAULT_VIDEO_BIT_RATE =
|
|
38
|
+
const DEFAULT_VIDEO_BIT_RATE = 2000000;
|
|
41
39
|
const DEFAULT_IDLE_TIMEOUT_MS = 30000;
|
|
42
40
|
const MAX_KEYFRAME_WAIT_MS = 5000;
|
|
43
41
|
const FRESH_FRAME_TIMEOUT_MS = 300;
|
|
@@ -50,17 +48,17 @@ var __webpack_modules__ = {
|
|
|
50
48
|
idleTimeoutMs: DEFAULT_IDLE_TIMEOUT_MS,
|
|
51
49
|
videoBitRate: DEFAULT_VIDEO_BIT_RATE
|
|
52
50
|
};
|
|
53
|
-
function
|
|
54
|
-
return nalUnitType ===
|
|
51
|
+
function isKeyFrameNalType(nalUnitType) {
|
|
52
|
+
return nalUnitType === NAL_TYPE_IDR || nalUnitType === NAL_TYPE_SPS || nalUnitType === NAL_TYPE_PPS;
|
|
55
53
|
}
|
|
56
|
-
function
|
|
57
|
-
const scanLimit = Math.min(buffer.length -
|
|
54
|
+
function detectH264KeyFrame(buffer) {
|
|
55
|
+
const scanLimit = Math.min(buffer.length - 4, MAX_SCAN_BYTES);
|
|
58
56
|
for(let i = 0; i < scanLimit; i++)if (0x00 === buffer[i] && 0x00 === buffer[i + 1] && 0x00 === buffer[i + 2] && 0x01 === buffer[i + 3]) {
|
|
59
|
-
const nalUnitType = buffer[i + 4]
|
|
60
|
-
if (
|
|
57
|
+
const nalUnitType = buffer[i + 4] & NAL_TYPE_MASK;
|
|
58
|
+
if (isKeyFrameNalType(nalUnitType)) return true;
|
|
61
59
|
} else if (0x00 === buffer[i] && 0x00 === buffer[i + 1] && 0x01 === buffer[i + 2]) {
|
|
62
|
-
const nalUnitType = buffer[i + 3]
|
|
63
|
-
if (
|
|
60
|
+
const nalUnitType = buffer[i + 3] & NAL_TYPE_MASK;
|
|
61
|
+
if (isKeyFrameNalType(nalUnitType)) return true;
|
|
64
62
|
}
|
|
65
63
|
return false;
|
|
66
64
|
}
|
|
@@ -85,8 +83,8 @@ var __webpack_modules__ = {
|
|
|
85
83
|
debugScrcpy('Starting scrcpy connection...');
|
|
86
84
|
const { AdbScrcpyClient, AdbScrcpyOptions2_1 } = await import("@yume-chan/adb-scrcpy");
|
|
87
85
|
const { ReadableStream } = await import("@yume-chan/stream-extra");
|
|
88
|
-
const { ScrcpyOptions3_1, DefaultServerPath,
|
|
89
|
-
this.
|
|
86
|
+
const { ScrcpyOptions3_1, DefaultServerPath, h264SearchConfiguration } = await import("@yume-chan/scrcpy");
|
|
87
|
+
this.h264SearchConfigFn = h264SearchConfiguration;
|
|
90
88
|
const serverBinPath = this.resolveServerBinPath();
|
|
91
89
|
await AdbScrcpyClient.pushServer(this.adb, ReadableStream.from((0, node_fs__rspack_import_0.createReadStream)(serverBinPath)));
|
|
92
90
|
const scrcpyOptions = new ScrcpyOptions3_1({
|
|
@@ -95,7 +93,6 @@ var __webpack_modules__ = {
|
|
|
95
93
|
maxSize: this.options.maxSize,
|
|
96
94
|
videoBitRate: this.options.videoBitRate,
|
|
97
95
|
sendFrameMeta: false,
|
|
98
|
-
videoCodec: 'h265',
|
|
99
96
|
videoCodecOptions: 'i-frame-interval=0'
|
|
100
97
|
});
|
|
101
98
|
this.scrcpyClient = await AdbScrcpyClient.start(this.adb, DefaultServerPath, new AdbScrcpyOptions2_1(scrcpyOptions));
|
|
@@ -155,7 +152,7 @@ var __webpack_modules__ = {
|
|
|
155
152
|
}
|
|
156
153
|
processFrame(packet) {
|
|
157
154
|
const frameBuffer = Buffer.from(packet.data);
|
|
158
|
-
const actualKeyFrame =
|
|
155
|
+
const actualKeyFrame = detectH264KeyFrame(frameBuffer);
|
|
159
156
|
if (actualKeyFrame && !this.spsHeader) this.extractSpsHeader(frameBuffer);
|
|
160
157
|
if (actualKeyFrame && this.spsHeader) {
|
|
161
158
|
this.lastRawKeyframe = frameBuffer;
|
|
@@ -169,21 +166,19 @@ var __webpack_modules__ = {
|
|
|
169
166
|
}
|
|
170
167
|
}
|
|
171
168
|
extractSpsHeader(frameBuffer) {
|
|
172
|
-
if (!this.
|
|
169
|
+
if (!this.h264SearchConfigFn) return;
|
|
173
170
|
try {
|
|
174
|
-
const config = this.
|
|
175
|
-
if (!config.
|
|
171
|
+
const config = this.h264SearchConfigFn(new Uint8Array(frameBuffer));
|
|
172
|
+
if (!config.sequenceParameterSet || !config.pictureParameterSet) return;
|
|
176
173
|
this.spsHeader = Buffer.concat([
|
|
177
174
|
START_CODE_4_BYTE,
|
|
178
|
-
Buffer.from(config.
|
|
175
|
+
Buffer.from(config.sequenceParameterSet),
|
|
179
176
|
START_CODE_4_BYTE,
|
|
180
|
-
Buffer.from(config.
|
|
181
|
-
START_CODE_4_BYTE,
|
|
182
|
-
Buffer.from(config.pictureParameterSet.data)
|
|
177
|
+
Buffer.from(config.pictureParameterSet)
|
|
183
178
|
]);
|
|
184
|
-
debugScrcpy(`Extracted
|
|
179
|
+
debugScrcpy(`Extracted SPS/PPS: SPS=${config.sequenceParameterSet.length}B, PPS=${config.pictureParameterSet.length}B, total=${this.spsHeader.length}B`);
|
|
185
180
|
} catch (error) {
|
|
186
|
-
debugScrcpy(`Failed to extract
|
|
181
|
+
debugScrcpy(`Failed to extract SPS/PPS from keyframe: ${error}`);
|
|
187
182
|
}
|
|
188
183
|
}
|
|
189
184
|
async getScreenshotPng() {
|
|
@@ -214,9 +209,9 @@ var __webpack_modules__ = {
|
|
|
214
209
|
}
|
|
215
210
|
const frameWaitTime = Date.now() - t3;
|
|
216
211
|
this.resetIdleTimer();
|
|
217
|
-
debugScrcpy(`Decoding H.
|
|
212
|
+
debugScrcpy(`Decoding H.264 stream: ${keyframeBuffer.length} bytes (${frameSource})`);
|
|
218
213
|
const t4 = Date.now();
|
|
219
|
-
const result = await this.
|
|
214
|
+
const result = await this.decodeH264ToPng(keyframeBuffer);
|
|
220
215
|
const decodeTime = Date.now() - t4;
|
|
221
216
|
const totalTime = Date.now() - perfStart;
|
|
222
217
|
debugScrcpy(`Performance: total=${totalTime}ms (connect=${connectTime}ms, spsWait=${spsWaitTime}ms, frameWait=${frameWaitTime}ms[${frameSource}], decode=${decodeTime}ms)`);
|
|
@@ -258,7 +253,7 @@ var __webpack_modules__ = {
|
|
|
258
253
|
const startTime = Date.now();
|
|
259
254
|
while(!this.spsHeader && Date.now() - startTime < MAX_KEYFRAME_WAIT_MS){
|
|
260
255
|
const elapsed = Date.now() - startTime;
|
|
261
|
-
debugScrcpy(`Waiting for first keyframe (
|
|
256
|
+
debugScrcpy(`Waiting for first keyframe (SPS/PPS header)... ${elapsed}ms`);
|
|
262
257
|
await new Promise((resolve)=>setTimeout(resolve, KEYFRAME_POLL_INTERVAL_MS));
|
|
263
258
|
}
|
|
264
259
|
if (!this.spsHeader) throw new Error(`No keyframe received within ${MAX_KEYFRAME_WAIT_MS}ms. Device may have a long GOP interval or video encoding issues. Please retry.`);
|
|
@@ -279,12 +274,12 @@ var __webpack_modules__ = {
|
|
|
279
274
|
return false;
|
|
280
275
|
}
|
|
281
276
|
}
|
|
282
|
-
async
|
|
277
|
+
async decodeH264ToPng(h264Buffer) {
|
|
283
278
|
const { spawn } = await import("node:child_process");
|
|
284
279
|
return new Promise((resolve, reject)=>{
|
|
285
280
|
const ffmpegArgs = [
|
|
286
281
|
'-f',
|
|
287
|
-
'
|
|
282
|
+
'h264',
|
|
288
283
|
'-i',
|
|
289
284
|
'pipe:0',
|
|
290
285
|
'-vframes',
|
|
@@ -321,13 +316,13 @@ var __webpack_modules__ = {
|
|
|
321
316
|
} else {
|
|
322
317
|
const errorMsg = stderrOutput || `FFmpeg exited with code ${code}`;
|
|
323
318
|
debugScrcpy(`FFmpeg decode failed: ${errorMsg}`);
|
|
324
|
-
reject(new Error(`H.
|
|
319
|
+
reject(new Error(`H.264 to PNG decode failed: ${errorMsg}`));
|
|
325
320
|
}
|
|
326
321
|
});
|
|
327
322
|
ffmpeg.on('error', (error)=>{
|
|
328
323
|
reject(new Error(`Failed to spawn ffmpeg process: ${error.message}`));
|
|
329
324
|
});
|
|
330
|
-
ffmpeg.stdin.write(
|
|
325
|
+
ffmpeg.stdin.write(h264Buffer);
|
|
331
326
|
ffmpeg.stdin.end();
|
|
332
327
|
});
|
|
333
328
|
}
|
|
@@ -353,7 +348,7 @@ var __webpack_modules__ = {
|
|
|
353
348
|
this.spsHeader = null;
|
|
354
349
|
this.lastRawKeyframe = null;
|
|
355
350
|
this.isInitialized = false;
|
|
356
|
-
this.
|
|
351
|
+
this.h264SearchConfigFn = null;
|
|
357
352
|
this.keyframeResolvers = [];
|
|
358
353
|
if (reader) try {
|
|
359
354
|
reader.cancel();
|
|
@@ -381,7 +376,7 @@ var __webpack_modules__ = {
|
|
|
381
376
|
_define_property(this, "keyframeResolvers", []);
|
|
382
377
|
_define_property(this, "lastRawKeyframe", null);
|
|
383
378
|
_define_property(this, "videoResolution", null);
|
|
384
|
-
_define_property(this, "
|
|
379
|
+
_define_property(this, "h264SearchConfigFn", null);
|
|
385
380
|
_define_property(this, "streamReader", null);
|
|
386
381
|
this.adb = adb;
|
|
387
382
|
this.options = {
|
|
@@ -499,20 +494,18 @@ var __webpack_exports__ = {};
|
|
|
499
494
|
resolveConfig(deviceInfo) {
|
|
500
495
|
if (this.resolvedConfig) return this.resolvedConfig;
|
|
501
496
|
const config = this.scrcpyConfig;
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
const
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
videoBitRate = Math.round(Math.max(scrcpy_manager.o.videoBitRate, scrcpy_manager.o.videoBitRate * ratio));
|
|
509
|
-
debugAdapter(`Auto-scaled videoBitRate: ${(videoBitRate / 1000000).toFixed(1)}Mbps (pixels=${physicalPixels}, ratio=${ratio.toFixed(2)})`);
|
|
497
|
+
let maxSize = config?.maxSize ?? scrcpy_manager.o.maxSize;
|
|
498
|
+
if (config?.maxSize === void 0) {
|
|
499
|
+
const physicalMax = Math.max(deviceInfo.physicalWidth, deviceInfo.physicalHeight);
|
|
500
|
+
const scale = this.screenshotResizeScale ?? 1 / deviceInfo.dpr;
|
|
501
|
+
maxSize = Math.round(physicalMax * scale);
|
|
502
|
+
debugAdapter(`Auto-calculated maxSize: ${maxSize} (physical=${physicalMax}, scale=${scale.toFixed(3)}, ${void 0 !== this.screenshotResizeScale ? 'from screenshotResizeScale' : 'from 1/dpr'})`);
|
|
510
503
|
}
|
|
511
504
|
this.resolvedConfig = {
|
|
512
505
|
enabled: this.isEnabled(),
|
|
513
506
|
maxSize,
|
|
514
507
|
idleTimeoutMs: config?.idleTimeoutMs ?? scrcpy_manager.o.idleTimeoutMs,
|
|
515
|
-
videoBitRate
|
|
508
|
+
videoBitRate: config?.videoBitRate ?? scrcpy_manager.o.videoBitRate
|
|
516
509
|
};
|
|
517
510
|
return this.resolvedConfig;
|
|
518
511
|
}
|
|
@@ -1043,6 +1036,17 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1043
1036
|
}
|
|
1044
1037
|
async size() {
|
|
1045
1038
|
const deviceInfo = await this.getDevicePhysicalInfo();
|
|
1039
|
+
const adapter = this.getScrcpyAdapter();
|
|
1040
|
+
if (adapter.isEnabled()) {
|
|
1041
|
+
const scrcpySize = adapter.getSize(deviceInfo);
|
|
1042
|
+
if (scrcpySize) {
|
|
1043
|
+
const isLandscape = 1 === deviceInfo.orientation || 3 === deviceInfo.orientation;
|
|
1044
|
+
const shouldSwap = true !== deviceInfo.isCurrentOrientation && isLandscape;
|
|
1045
|
+
const physicalWidth = shouldSwap ? deviceInfo.physicalHeight : deviceInfo.physicalWidth;
|
|
1046
|
+
this.scalingRatio = adapter.getScalingRatio(physicalWidth) ?? this.scalingRatio;
|
|
1047
|
+
return scrcpySize;
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1046
1050
|
const isLandscape = 1 === deviceInfo.orientation || 3 === deviceInfo.orientation;
|
|
1047
1051
|
const shouldSwap = true !== deviceInfo.isCurrentOrientation && isLandscape;
|
|
1048
1052
|
const width = shouldSwap ? deviceInfo.physicalHeight : deviceInfo.physicalWidth;
|
package/dist/lib/mcp-server.js
CHANGED
|
@@ -24,12 +24,10 @@ 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
|
|
28
|
-
const
|
|
29
|
-
const
|
|
30
|
-
const
|
|
31
|
-
const H265_NAL_TYPE_SPS = 33;
|
|
32
|
-
const H265_NAL_TYPE_PPS = 34;
|
|
27
|
+
const NAL_TYPE_IDR = 5;
|
|
28
|
+
const NAL_TYPE_SPS = 7;
|
|
29
|
+
const NAL_TYPE_PPS = 8;
|
|
30
|
+
const NAL_TYPE_MASK = 0x1f;
|
|
33
31
|
const START_CODE_4_BYTE = Buffer.from([
|
|
34
32
|
0x00,
|
|
35
33
|
0x00,
|
|
@@ -37,7 +35,7 @@ var __webpack_modules__ = {
|
|
|
37
35
|
0x01
|
|
38
36
|
]);
|
|
39
37
|
const DEFAULT_MAX_SIZE = 0;
|
|
40
|
-
const DEFAULT_VIDEO_BIT_RATE =
|
|
38
|
+
const DEFAULT_VIDEO_BIT_RATE = 2000000;
|
|
41
39
|
const DEFAULT_IDLE_TIMEOUT_MS = 30000;
|
|
42
40
|
const MAX_KEYFRAME_WAIT_MS = 5000;
|
|
43
41
|
const FRESH_FRAME_TIMEOUT_MS = 300;
|
|
@@ -50,17 +48,17 @@ var __webpack_modules__ = {
|
|
|
50
48
|
idleTimeoutMs: DEFAULT_IDLE_TIMEOUT_MS,
|
|
51
49
|
videoBitRate: DEFAULT_VIDEO_BIT_RATE
|
|
52
50
|
};
|
|
53
|
-
function
|
|
54
|
-
return nalUnitType ===
|
|
51
|
+
function isKeyFrameNalType(nalUnitType) {
|
|
52
|
+
return nalUnitType === NAL_TYPE_IDR || nalUnitType === NAL_TYPE_SPS || nalUnitType === NAL_TYPE_PPS;
|
|
55
53
|
}
|
|
56
|
-
function
|
|
57
|
-
const scanLimit = Math.min(buffer.length -
|
|
54
|
+
function detectH264KeyFrame(buffer) {
|
|
55
|
+
const scanLimit = Math.min(buffer.length - 4, MAX_SCAN_BYTES);
|
|
58
56
|
for(let i = 0; i < scanLimit; i++)if (0x00 === buffer[i] && 0x00 === buffer[i + 1] && 0x00 === buffer[i + 2] && 0x01 === buffer[i + 3]) {
|
|
59
|
-
const nalUnitType = buffer[i + 4]
|
|
60
|
-
if (
|
|
57
|
+
const nalUnitType = buffer[i + 4] & NAL_TYPE_MASK;
|
|
58
|
+
if (isKeyFrameNalType(nalUnitType)) return true;
|
|
61
59
|
} else if (0x00 === buffer[i] && 0x00 === buffer[i + 1] && 0x01 === buffer[i + 2]) {
|
|
62
|
-
const nalUnitType = buffer[i + 3]
|
|
63
|
-
if (
|
|
60
|
+
const nalUnitType = buffer[i + 3] & NAL_TYPE_MASK;
|
|
61
|
+
if (isKeyFrameNalType(nalUnitType)) return true;
|
|
64
62
|
}
|
|
65
63
|
return false;
|
|
66
64
|
}
|
|
@@ -85,8 +83,8 @@ var __webpack_modules__ = {
|
|
|
85
83
|
debugScrcpy('Starting scrcpy connection...');
|
|
86
84
|
const { AdbScrcpyClient, AdbScrcpyOptions2_1 } = await import("@yume-chan/adb-scrcpy");
|
|
87
85
|
const { ReadableStream } = await import("@yume-chan/stream-extra");
|
|
88
|
-
const { ScrcpyOptions3_1, DefaultServerPath,
|
|
89
|
-
this.
|
|
86
|
+
const { ScrcpyOptions3_1, DefaultServerPath, h264SearchConfiguration } = await import("@yume-chan/scrcpy");
|
|
87
|
+
this.h264SearchConfigFn = h264SearchConfiguration;
|
|
90
88
|
const serverBinPath = this.resolveServerBinPath();
|
|
91
89
|
await AdbScrcpyClient.pushServer(this.adb, ReadableStream.from((0, node_fs__rspack_import_0.createReadStream)(serverBinPath)));
|
|
92
90
|
const scrcpyOptions = new ScrcpyOptions3_1({
|
|
@@ -95,7 +93,6 @@ var __webpack_modules__ = {
|
|
|
95
93
|
maxSize: this.options.maxSize,
|
|
96
94
|
videoBitRate: this.options.videoBitRate,
|
|
97
95
|
sendFrameMeta: false,
|
|
98
|
-
videoCodec: 'h265',
|
|
99
96
|
videoCodecOptions: 'i-frame-interval=0'
|
|
100
97
|
});
|
|
101
98
|
this.scrcpyClient = await AdbScrcpyClient.start(this.adb, DefaultServerPath, new AdbScrcpyOptions2_1(scrcpyOptions));
|
|
@@ -155,7 +152,7 @@ var __webpack_modules__ = {
|
|
|
155
152
|
}
|
|
156
153
|
processFrame(packet) {
|
|
157
154
|
const frameBuffer = Buffer.from(packet.data);
|
|
158
|
-
const actualKeyFrame =
|
|
155
|
+
const actualKeyFrame = detectH264KeyFrame(frameBuffer);
|
|
159
156
|
if (actualKeyFrame && !this.spsHeader) this.extractSpsHeader(frameBuffer);
|
|
160
157
|
if (actualKeyFrame && this.spsHeader) {
|
|
161
158
|
this.lastRawKeyframe = frameBuffer;
|
|
@@ -169,21 +166,19 @@ var __webpack_modules__ = {
|
|
|
169
166
|
}
|
|
170
167
|
}
|
|
171
168
|
extractSpsHeader(frameBuffer) {
|
|
172
|
-
if (!this.
|
|
169
|
+
if (!this.h264SearchConfigFn) return;
|
|
173
170
|
try {
|
|
174
|
-
const config = this.
|
|
175
|
-
if (!config.
|
|
171
|
+
const config = this.h264SearchConfigFn(new Uint8Array(frameBuffer));
|
|
172
|
+
if (!config.sequenceParameterSet || !config.pictureParameterSet) return;
|
|
176
173
|
this.spsHeader = Buffer.concat([
|
|
177
174
|
START_CODE_4_BYTE,
|
|
178
|
-
Buffer.from(config.
|
|
175
|
+
Buffer.from(config.sequenceParameterSet),
|
|
179
176
|
START_CODE_4_BYTE,
|
|
180
|
-
Buffer.from(config.
|
|
181
|
-
START_CODE_4_BYTE,
|
|
182
|
-
Buffer.from(config.pictureParameterSet.data)
|
|
177
|
+
Buffer.from(config.pictureParameterSet)
|
|
183
178
|
]);
|
|
184
|
-
debugScrcpy(`Extracted
|
|
179
|
+
debugScrcpy(`Extracted SPS/PPS: SPS=${config.sequenceParameterSet.length}B, PPS=${config.pictureParameterSet.length}B, total=${this.spsHeader.length}B`);
|
|
185
180
|
} catch (error) {
|
|
186
|
-
debugScrcpy(`Failed to extract
|
|
181
|
+
debugScrcpy(`Failed to extract SPS/PPS from keyframe: ${error}`);
|
|
187
182
|
}
|
|
188
183
|
}
|
|
189
184
|
async getScreenshotPng() {
|
|
@@ -214,9 +209,9 @@ var __webpack_modules__ = {
|
|
|
214
209
|
}
|
|
215
210
|
const frameWaitTime = Date.now() - t3;
|
|
216
211
|
this.resetIdleTimer();
|
|
217
|
-
debugScrcpy(`Decoding H.
|
|
212
|
+
debugScrcpy(`Decoding H.264 stream: ${keyframeBuffer.length} bytes (${frameSource})`);
|
|
218
213
|
const t4 = Date.now();
|
|
219
|
-
const result = await this.
|
|
214
|
+
const result = await this.decodeH264ToPng(keyframeBuffer);
|
|
220
215
|
const decodeTime = Date.now() - t4;
|
|
221
216
|
const totalTime = Date.now() - perfStart;
|
|
222
217
|
debugScrcpy(`Performance: total=${totalTime}ms (connect=${connectTime}ms, spsWait=${spsWaitTime}ms, frameWait=${frameWaitTime}ms[${frameSource}], decode=${decodeTime}ms)`);
|
|
@@ -258,7 +253,7 @@ var __webpack_modules__ = {
|
|
|
258
253
|
const startTime = Date.now();
|
|
259
254
|
while(!this.spsHeader && Date.now() - startTime < MAX_KEYFRAME_WAIT_MS){
|
|
260
255
|
const elapsed = Date.now() - startTime;
|
|
261
|
-
debugScrcpy(`Waiting for first keyframe (
|
|
256
|
+
debugScrcpy(`Waiting for first keyframe (SPS/PPS header)... ${elapsed}ms`);
|
|
262
257
|
await new Promise((resolve)=>setTimeout(resolve, KEYFRAME_POLL_INTERVAL_MS));
|
|
263
258
|
}
|
|
264
259
|
if (!this.spsHeader) throw new Error(`No keyframe received within ${MAX_KEYFRAME_WAIT_MS}ms. Device may have a long GOP interval or video encoding issues. Please retry.`);
|
|
@@ -279,12 +274,12 @@ var __webpack_modules__ = {
|
|
|
279
274
|
return false;
|
|
280
275
|
}
|
|
281
276
|
}
|
|
282
|
-
async
|
|
277
|
+
async decodeH264ToPng(h264Buffer) {
|
|
283
278
|
const { spawn } = await import("node:child_process");
|
|
284
279
|
return new Promise((resolve, reject)=>{
|
|
285
280
|
const ffmpegArgs = [
|
|
286
281
|
'-f',
|
|
287
|
-
'
|
|
282
|
+
'h264',
|
|
288
283
|
'-i',
|
|
289
284
|
'pipe:0',
|
|
290
285
|
'-vframes',
|
|
@@ -321,13 +316,13 @@ var __webpack_modules__ = {
|
|
|
321
316
|
} else {
|
|
322
317
|
const errorMsg = stderrOutput || `FFmpeg exited with code ${code}`;
|
|
323
318
|
debugScrcpy(`FFmpeg decode failed: ${errorMsg}`);
|
|
324
|
-
reject(new Error(`H.
|
|
319
|
+
reject(new Error(`H.264 to PNG decode failed: ${errorMsg}`));
|
|
325
320
|
}
|
|
326
321
|
});
|
|
327
322
|
ffmpeg.on('error', (error)=>{
|
|
328
323
|
reject(new Error(`Failed to spawn ffmpeg process: ${error.message}`));
|
|
329
324
|
});
|
|
330
|
-
ffmpeg.stdin.write(
|
|
325
|
+
ffmpeg.stdin.write(h264Buffer);
|
|
331
326
|
ffmpeg.stdin.end();
|
|
332
327
|
});
|
|
333
328
|
}
|
|
@@ -353,7 +348,7 @@ var __webpack_modules__ = {
|
|
|
353
348
|
this.spsHeader = null;
|
|
354
349
|
this.lastRawKeyframe = null;
|
|
355
350
|
this.isInitialized = false;
|
|
356
|
-
this.
|
|
351
|
+
this.h264SearchConfigFn = null;
|
|
357
352
|
this.keyframeResolvers = [];
|
|
358
353
|
if (reader) try {
|
|
359
354
|
reader.cancel();
|
|
@@ -381,7 +376,7 @@ var __webpack_modules__ = {
|
|
|
381
376
|
_define_property(this, "keyframeResolvers", []);
|
|
382
377
|
_define_property(this, "lastRawKeyframe", null);
|
|
383
378
|
_define_property(this, "videoResolution", null);
|
|
384
|
-
_define_property(this, "
|
|
379
|
+
_define_property(this, "h264SearchConfigFn", null);
|
|
385
380
|
_define_property(this, "streamReader", null);
|
|
386
381
|
this.adb = adb;
|
|
387
382
|
this.options = {
|
|
@@ -595,20 +590,18 @@ var __webpack_exports__ = {};
|
|
|
595
590
|
resolveConfig(deviceInfo) {
|
|
596
591
|
if (this.resolvedConfig) return this.resolvedConfig;
|
|
597
592
|
const config = this.scrcpyConfig;
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
const
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
videoBitRate = Math.round(Math.max(scrcpy_manager.o.videoBitRate, scrcpy_manager.o.videoBitRate * ratio));
|
|
605
|
-
debugAdapter(`Auto-scaled videoBitRate: ${(videoBitRate / 1000000).toFixed(1)}Mbps (pixels=${physicalPixels}, ratio=${ratio.toFixed(2)})`);
|
|
593
|
+
let maxSize = config?.maxSize ?? scrcpy_manager.o.maxSize;
|
|
594
|
+
if (config?.maxSize === void 0) {
|
|
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'})`);
|
|
606
599
|
}
|
|
607
600
|
this.resolvedConfig = {
|
|
608
601
|
enabled: this.isEnabled(),
|
|
609
602
|
maxSize,
|
|
610
603
|
idleTimeoutMs: config?.idleTimeoutMs ?? scrcpy_manager.o.idleTimeoutMs,
|
|
611
|
-
videoBitRate
|
|
604
|
+
videoBitRate: config?.videoBitRate ?? scrcpy_manager.o.videoBitRate
|
|
612
605
|
};
|
|
613
606
|
return this.resolvedConfig;
|
|
614
607
|
}
|
|
@@ -1139,6 +1132,17 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1139
1132
|
}
|
|
1140
1133
|
async size() {
|
|
1141
1134
|
const deviceInfo = await this.getDevicePhysicalInfo();
|
|
1135
|
+
const adapter = this.getScrcpyAdapter();
|
|
1136
|
+
if (adapter.isEnabled()) {
|
|
1137
|
+
const scrcpySize = adapter.getSize(deviceInfo);
|
|
1138
|
+
if (scrcpySize) {
|
|
1139
|
+
const isLandscape = 1 === deviceInfo.orientation || 3 === deviceInfo.orientation;
|
|
1140
|
+
const shouldSwap = true !== deviceInfo.isCurrentOrientation && isLandscape;
|
|
1141
|
+
const physicalWidth = shouldSwap ? deviceInfo.physicalHeight : deviceInfo.physicalWidth;
|
|
1142
|
+
this.scalingRatio = adapter.getScalingRatio(physicalWidth) ?? this.scalingRatio;
|
|
1143
|
+
return scrcpySize;
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1142
1146
|
const isLandscape = 1 === deviceInfo.orientation || 3 === deviceInfo.orientation;
|
|
1143
1147
|
const shouldSwap = true !== deviceInfo.isCurrentOrientation && isLandscape;
|
|
1144
1148
|
const width = shouldSwap ? deviceInfo.physicalHeight : deviceInfo.physicalWidth;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@midscene/android",
|
|
3
|
-
"version": "1.3.10
|
|
3
|
+
"version": "1.3.10",
|
|
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/core": "1.3.10
|
|
42
|
-
"@midscene/shared": "1.3.10
|
|
41
|
+
"@midscene/core": "1.3.10",
|
|
42
|
+
"@midscene/shared": "1.3.10"
|
|
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.10
|
|
56
|
+
"@midscene/playground": "1.3.10"
|
|
57
57
|
},
|
|
58
58
|
"license": "MIT",
|
|
59
59
|
"scripts": {
|