@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 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, isValidPNGImageBuffer } from "@midscene/shared/img";
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 = 2000000;
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, h264SearchConfiguration } = await import("@yume-chan/scrcpy");
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
- sendFrameMeta: false,
104
- videoCodecOptions: 'i-frame-interval=0'
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 actualKeyFrame = detectH264KeyFrame(frameBuffer);
164
- if (actualKeyFrame && !this.spsHeader) this.extractSpsHeader(frameBuffer);
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
- extractSpsHeader(frameBuffer) {
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.decodeH264ToPng(keyframeBuffer);
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 decodeH264ToPng(h264Buffer) {
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
- 'png',
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 pngBuffer = Buffer.concat(chunks);
322
- debugScrcpy(`FFmpeg decode successful, PNG size: ${pngBuffer.length} bytes`);
323
- resolve(pngBuffer);
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 PNG decode failed: ${errorMsg}`));
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: options.videoBitRate ?? DEFAULT_VIDEO_BIT_RATE,
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
- 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'})`);
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: config?.videoBitRate ?? scrcpy_manager.o.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.getScreenshotPng();
512
- return createImgBase64ByFormat('png', screenshotBuffer.toString('base64'));
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 (!isValidPNGImageBuffer(screenshotBuffer)) {
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 (!isValidPNGImageBuffer(screenshotBuffer)) throw new Error('Fallback screenshot buffer has invalid PNG format');
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)=>{
@@ -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, isValidPNGImageBuffer } from "@midscene/shared/img";
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 = 2000000;
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, h264SearchConfiguration } = await import("@yume-chan/scrcpy");
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
- sendFrameMeta: false,
105
- videoCodecOptions: 'i-frame-interval=0'
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 actualKeyFrame = detectH264KeyFrame(frameBuffer);
165
- if (actualKeyFrame && !this.spsHeader) this.extractSpsHeader(frameBuffer);
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
- extractSpsHeader(frameBuffer) {
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.decodeH264ToPng(keyframeBuffer);
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 decodeH264ToPng(h264Buffer) {
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
- 'png',
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 pngBuffer = Buffer.concat(chunks);
323
- debugScrcpy(`FFmpeg decode successful, PNG size: ${pngBuffer.length} bytes`);
324
- resolve(pngBuffer);
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 PNG decode failed: ${errorMsg}`));
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: options.videoBitRate ?? DEFAULT_VIDEO_BIT_RATE,
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
- 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'})`);
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: config?.videoBitRate ?? scrcpy_manager.o.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.getScreenshotPng();
609
- return createImgBase64ByFormat('png', screenshotBuffer.toString('base64'));
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 (!isValidPNGImageBuffer(screenshotBuffer)) {
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 (!isValidPNGImageBuffer(screenshotBuffer)) throw new Error('Fallback screenshot buffer has invalid PNG format');
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 = 2000000;
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, h264SearchConfiguration } = await import("@yume-chan/scrcpy");
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
- sendFrameMeta: false,
96
- videoCodecOptions: 'i-frame-interval=0'
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 actualKeyFrame = detectH264KeyFrame(frameBuffer);
156
- if (actualKeyFrame && !this.spsHeader) this.extractSpsHeader(frameBuffer);
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
- extractSpsHeader(frameBuffer) {
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.decodeH264ToPng(keyframeBuffer);
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 decodeH264ToPng(h264Buffer) {
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
- 'png',
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 pngBuffer = Buffer.concat(chunks);
314
- debugScrcpy(`FFmpeg decode successful, PNG size: ${pngBuffer.length} bytes`);
315
- resolve(pngBuffer);
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 PNG decode failed: ${errorMsg}`));
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: options.videoBitRate ?? DEFAULT_VIDEO_BIT_RATE,
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
- let maxSize = config?.maxSize ?? scrcpy_manager.o.maxSize;
499
- if (config?.maxSize === void 0) {
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: config?.videoBitRate ?? scrcpy_manager.o.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.getScreenshotPng();
545
- return (0, img_namespaceObject.createImgBase64ByFormat)('png', screenshotBuffer.toString('base64'));
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.isValidPNGImageBuffer)(screenshotBuffer)) {
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.isValidPNGImageBuffer)(screenshotBuffer)) throw new Error('Fallback screenshot buffer has invalid PNG format');
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)=>{
@@ -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 = 2000000;
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, h264SearchConfiguration } = await import("@yume-chan/scrcpy");
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
- sendFrameMeta: false,
96
- videoCodecOptions: 'i-frame-interval=0'
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 actualKeyFrame = detectH264KeyFrame(frameBuffer);
156
- if (actualKeyFrame && !this.spsHeader) this.extractSpsHeader(frameBuffer);
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
- extractSpsHeader(frameBuffer) {
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.decodeH264ToPng(keyframeBuffer);
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 decodeH264ToPng(h264Buffer) {
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
- 'png',
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 pngBuffer = Buffer.concat(chunks);
314
- debugScrcpy(`FFmpeg decode successful, PNG size: ${pngBuffer.length} bytes`);
315
- resolve(pngBuffer);
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 PNG decode failed: ${errorMsg}`));
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: options.videoBitRate ?? DEFAULT_VIDEO_BIT_RATE,
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
- 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'})`);
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: config?.videoBitRate ?? scrcpy_manager.o.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.getScreenshotPng();
640
- return (0, img_namespaceObject.createImgBase64ByFormat)('png', screenshotBuffer.toString('base64'));
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.isValidPNGImageBuffer)(screenshotBuffer)) {
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.isValidPNGImageBuffer)(screenshotBuffer)) throw new Error('Fallback screenshot buffer has invalid PNG format');
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)=>{
@@ -242,8 +242,11 @@ export declare class ScrcpyDeviceAdapter {
242
242
  */
243
243
  initialize(deviceInfo: DevicePhysicalInfo): Promise<void>;
244
244
  /**
245
- * Resolve scrcpy config with auto-calculated maxSize.
246
- * Auto-calculation uses 1/DPR or screenshotResizeScale to match Agent layer's logical size.
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 frame.
324
- * Caches the raw keyframe buffer (without SPS concat) to minimize per-frame overhead.
325
- * Buffer.concat with SPS header is deferred to when the frame is actually consumed.
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
- * Extract SPS/PPS header from keyframe
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
- getScreenshotPng(): Promise<Buffer>;
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 PNG using ffmpeg
368
+ * Decode H.264 data to JPEG using ffmpeg
368
369
  */
369
- private decodeH264ToPng;
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-beta-20260211054343.0",
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/core": "1.3.11-beta-20260211054343.0",
42
- "@midscene/shared": "1.3.11-beta-20260211054343.0"
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-beta-20260211054343.0"
56
+ "@midscene/playground": "1.3.11"
57
57
  },
58
58
  "license": "MIT",
59
59
  "scripts": {