Skip to content

Commit b9fb159

Browse files
committed
feat: add support for scrcpy 2.x with audio and socket id support scrcpy defaut version is 2.7
1 parent dfa03c7 commit b9fb159

File tree

4 files changed

+181
-49
lines changed

4 files changed

+181
-49
lines changed

.vscode/launch.json

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,24 +23,20 @@
2323
}
2424
},
2525

26-
27-
2826
{
29-
"name": "realTest",
27+
"name": "realTest (tsx)",
3028
"type": "node",
3129
"request": "launch",
32-
"runtimeArgs": [ "--trace-warnings", "--unhandled-rejections", "warn", "--trace-sync-io", "--nolazy", "-r", "ts-node/register"],
33-
"args": ["./tasks/realTest.ts", "--transpile-only"],
30+
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/tsx",
31+
"args": ["./tasks/realTest.ts"],
3432
"cwd": "${workspaceFolder}",
3533
"internalConsoleOptions": "openOnSessionStart",
36-
// "skipFiles": ["<node_internals>/**", "node_modules/**"],
37-
"skipFiles": ["<node_internals>/**" ],
34+
"skipFiles": ["<node_internals>/**"],
3835
"env": {
39-
"TS_NODE_PROJECT": "${workspaceFolder}/tsconfig.json",
4036
"DEBUG": "adb:minicap, adb:scrcpy",
4137
"DEBUG_COLORS": "1"
4238
}
43-
},
39+
},
4440
{
4541
"name": "Mocha Tests",
4642
"cwd": "${workspaceFolder}",

src/adb/thirdparty/scrcpy/Scrcpy.ts

Lines changed: 159 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import { parse_sequence_parameter_set } from './sps.js';
1616
import { Point, ScrcpyOptions, H264Configuration, VideoStreamFramePacket } from './ScrcpyModels.js';
1717
import prebuilds from "@u4/minicap-prebuilt";
1818

19+
const SC_SOCKET_NAME_PREFIX = "scrcpy_";
20+
1921
const debug = Utils.debug('adb:scrcpy');
2022

2123
// const KEYFRAME_PTS = BigInt(1) << BigInt(62);
@@ -24,6 +26,11 @@ const debug = Utils.debug('adb:scrcpy');
2426
const PACKET_FLAG_CONFIG = BigInt(1) << BigInt(63);
2527
const PACKET_FLAG_KEY_FRAME = BigInt(1) << BigInt(62);
2628

29+
/**
30+
* usage reference fron app/src/server.c in scrcpy
31+
* https://github.com/Genymobile/scrcpy/blob/master/app/src/server.c
32+
*/
33+
2734
/**
2835
* by hand start:
2936
*
@@ -71,6 +78,7 @@ interface IEmissions {
7178
export default class Scrcpy extends EventEmitter {
7279
private config: ScrcpyOptions;
7380
private videoSocket: PromiseDuplex<Duplex> | undefined;
81+
private audioSocket: PromiseDuplex<Duplex> | undefined;
7482
private controlSocket: PromiseDuplex<Duplex> | undefined;
7583
/**
7684
* used to recive Process Error
@@ -103,7 +111,9 @@ export default class Scrcpy extends EventEmitter {
103111
constructor(private client: DeviceClient, config = {} as Partial<ScrcpyOptions>) {
104112
super();
105113
this.config = {
106-
version: 24,
114+
scid: '0' + Math.random().toString(16).substring(2, 9),
115+
noAudio: true, // disable audio TMP
116+
version: "2.7",
107117
// port: 8099,
108118
maxSize: 600,
109119
maxFps: 0,
@@ -161,7 +171,7 @@ export default class Scrcpy extends EventEmitter {
161171
try {
162172
const errors = [];
163173
for (; ;) {
164-
await Utils.waitforReadable(duplex, 0, 'wait for error');
174+
await Utils.waitforReadable(duplex, 0, 'wait for error from ScrcpyServer');
165175
const data = await duplex.read();
166176
if (data) {
167177
const msg = data.toString().trim();
@@ -173,7 +183,9 @@ export default class Scrcpy extends EventEmitter {
173183
// emit Error but to not want to Quit Yet
174184
}
175185
} else {
176-
this._setFatalError(errors.join('\n'));
186+
if (errors.length > 0)
187+
this._setFatalError(errors.join('\n'));
188+
// else no error
177189
break;
178190
}
179191
}
@@ -227,6 +239,13 @@ export default class Scrcpy extends EventEmitter {
227239
throw Error(`Unsupported message type:${type}`);
228240
}
229241
}
242+
get strVersion(): string {
243+
let versionSplit = this.config.version.split(".").map(Number);
244+
if (versionSplit.length === 2) {
245+
versionSplit = [...versionSplit, 0];
246+
}
247+
return `${versionSplit[0].toString().padStart(2, '0')}.${versionSplit[1].toString().padStart(2, '0')}.${versionSplit[2].toString().padStart(2, '0')}`;
248+
}
230249

231250
private _getStartupLine(jarDest: string): string {
232251
const args: Array<string | number | boolean> = [];
@@ -239,10 +258,19 @@ export default class Scrcpy extends EventEmitter {
239258
args.push('app_process');
240259
args.push('/');
241260
args.push('com.genymobile.scrcpy.Server');
261+
const versionStr = this.strVersion;
242262

243-
if (this.config.version <= 20) {
244-
// Version 11 => 20
245-
args.push(`1.${this.config.version}`); // arg 0 Scrcpy server version
263+
// first args is the expected version number
264+
if (versionStr === "02.02.00") {
265+
// V2.2 is the only version that expect a v prefix
266+
args.push("v" + this.config.version);
267+
} else {
268+
args.push(this.config.version);
269+
}
270+
// args.push(this.config.version); // arg 0 Scrcpy server version
271+
//if (this.config.version <= 20) {
272+
if (versionStr <= "02.00.00") {
273+
// Version 11 => 20
246274
args.push("info"); // Log level: info, verbose...
247275
args.push(maxSize); // Max screen width (long side)
248276
args.push(bitrate); // Bitrate of video
@@ -259,10 +287,26 @@ export default class Scrcpy extends EventEmitter {
259287
args.push(encoderName || '-'); // Encoder name
260288
args.push(powerOffScreenOnClose); // Power off screen after server closed
261289
} else {
262-
args.push(`1.${this.config.version}`); // arg 0 Scrcpy server version
290+
if (versionStr >= "02.00.00") {
291+
args.push(`scid=${this.config.scid}`);
292+
if (this.config.noAudio)
293+
args.push(`audio=false`);
294+
// if (this.config.noControl)
295+
// args.push(`no_control=${this.config.noControl}`);
296+
}
297+
298+
if (versionStr >= "02.04.00") {
299+
args.push(`video_source=display`);
300+
}
301+
263302
args.push("log_level=info");
264303
args.push(`max_size=${maxSize}`);
265-
args.push(`bit_rate=${bitrate}`);
304+
args.push("clipboard_autosync=false"); // cause crash on some newer phone and we do not use that feature.
305+
if (versionStr >= "02.00.00") {
306+
args.push(`video_bit_rate=${bitrate}`);
307+
} else {
308+
args.push(`bit_rate=${bitrate}`);
309+
}
266310
args.push(`max_fps=${maxFps}`);
267311
args.push(`lock_video_orientation=${lockedVideoOrientation}`);
268312
args.push(`tunnel_forward=${tunnelForward}`); // Tunnel forward
@@ -281,8 +325,9 @@ export default class Scrcpy extends EventEmitter {
281325
// args.push(`clipboard_autosync=${clipboardAutosync}`); // default is True
282326
if (clipboardAutosync !== undefined)
283327
args.push(`clipboard_autosync=${clipboardAutosync}`); // default is True
284-
if (this.config.version >= 22) {
285-
const {
328+
//if (this.config.version >= 22) {
329+
if (versionStr >= "01.22.00") {
330+
const {
286331
downsizeOnError, sendDeviceMeta, sendDummyByte, rawVideoStream
287332
} = this.config;
288333
if (downsizeOnError !== undefined)
@@ -294,7 +339,7 @@ export default class Scrcpy extends EventEmitter {
294339
if (rawVideoStream !== undefined)
295340
args.push(`raw_video_stream=${rawVideoStream}`);
296341
}
297-
if (this.config.version >= 22) {
342+
if (versionStr >= "01.22.00") {
298343
const { cleanup } = this.config;
299344
if (cleanup !== undefined)
300345
args.push(`raw_video_stream=${cleanup}`);
@@ -311,10 +356,19 @@ export default class Scrcpy extends EventEmitter {
311356
async start(): Promise<this> {
312357
if (this.closed) // can not start once stop called
313358
return this;
314-
const jarDest = '/data/local/tmp/scrcpy-server.jar';
315-
// Transfer server...
316-
const jar = prebuilds.getScrcpyJar(`1.${this.config.version}`);
317-
// ThirdUtils.getResourcePath(`scrcpy-server-v1.${this.config.version}.jar`);
359+
360+
let dstFolder = '/data/local/tmp';
361+
let dstFolderStat = await this.client.stat(dstFolder).catch((e) => {console.log(e); return null;});
362+
if (!dstFolderStat) {
363+
dstFolder = '/tmp';
364+
dstFolderStat = await this.client.stat(dstFolder).catch(() => null);
365+
}
366+
if (!dstFolderStat) {
367+
throw Error("can not find a writable tmp dest folder in device");
368+
}
369+
const jarDest = `${dstFolder}/scrcpy-server-v${this.config.version}.jar`;
370+
// Transfer server jar to device...
371+
const jar = prebuilds.getScrcpyJar(this.config.version);
318372

319373
const srcStat: fs.Stats | null = await fs.promises.stat(jar).catch(() => null);
320374
const dstStat: Stats | null = await this.client.stat(jarDest).catch(() => null);
@@ -332,9 +386,15 @@ export default class Scrcpy extends EventEmitter {
332386
} else {
333387
debug(`scrcpy-server.jar already present in ${this.client.serial}, keep it`);
334388
}
335-
// Start server
389+
///////
390+
// Build the commandline to start the server
336391
try {
337392
const cmdLine = this._getStartupLine(jarDest);
393+
394+
// console.log("starting scrcpy server with cmdLine:");
395+
// console.log(cmdLine);
396+
// console.log("");
397+
338398
if (this.closed) // can not start once stop called
339399
return this;
340400
const duplex = await this.client.shell(cmdLine);
@@ -350,37 +410,67 @@ export default class Scrcpy extends EventEmitter {
350410
throw e;
351411
}
352412

353-
let info = '';
413+
let stdoutContent = '';
354414
for (; ;) {
355415
if (!await Utils.waitforReadable(this.scrcpyServer, this.config.tunnelDelay, 'scrcpyServer stdout loading')) {
356416
// const msg = `First line should be '[server] // INFO: Device: Name (Version), reveived:\n\n${info}`
357-
const error = `Starting scrcpyServer failed, scrcpy stdout:${info}`;
417+
if (!stdoutContent)
418+
stdoutContent = "no stdout content";
419+
const error = `Starting scrcpyServer failed, scrcpy stdout:${stdoutContent}`;
358420
this._setFatalError(error);
359421
this.stop();
360422
throw Error(error);
361423
}
362424
const srvOut = await this.scrcpyServer.read();
363-
info += (srvOut) ? srvOut.toString() : '';
364-
if (info.includes('[server] INFO: Device: '))
425+
stdoutContent += (srvOut) ? srvOut.toString() : '';
426+
// the server may crash within the first message
427+
const errorIndex = stdoutContent.indexOf("[server] ERROR:");
428+
if (errorIndex >= 0) {
429+
const error = stdoutContent.substring(errorIndex)
430+
this._setFatalError(error);
431+
this.stop();
432+
throw Error(error);
433+
}
434+
if (stdoutContent.includes('[server] INFO: Device: '))
365435
break;
366436
}
367437

368438
this.throwsErrors(this.scrcpyServer);
369439

440+
// from V2.0 SC_SOCKET_NAME name can be change
441+
const strVersion = this.strVersion;
442+
let SC_SOCKET_NAME = 'scrcpy';
443+
if (strVersion >= "02.00.00") {
444+
SC_SOCKET_NAME = SC_SOCKET_NAME_PREFIX + this.config.scid;
445+
assert(this.config.scid.length == 8, `scid length should be 8`);
446+
}
447+
370448
// Wait 1 sec to forward to work
371449
// await Util.delay(this.config.tunnelDelay);
372450

373451
if (this.closed) // can not start once stop called
374452
return this;
453+
375454
// Connect videoSocket
376455
await Utils.delay(100);
377-
this.videoSocket = await this.client.openLocal2('localabstract:scrcpy', 'first connection to scrcpy for video');
378-
// Connect controlSocket
456+
this.videoSocket = await this.client.openLocal2(`localabstract:${SC_SOCKET_NAME}`, 'first connection to scrcpy for video');
457+
379458
if (this.closed) {
380459
this.stop();
381460
return this;
382461
}
383-
this.controlSocket = await this.client.openLocal2('localabstract:scrcpy', 'second connection to scrcpy for control');
462+
463+
if (strVersion >= "02.00.00" && !this.config.noAudio) {
464+
// Connect audioSocket
465+
this.audioSocket = await this.client.openLocal2(`localabstract:${SC_SOCKET_NAME}`, 'first connection to scrcpy for audio');
466+
// Connect controlSocket
467+
if (this.closed) {
468+
this.stop();
469+
return this;
470+
}
471+
}
472+
473+
this.controlSocket = await this.client.openLocal2(`localabstract:${SC_SOCKET_NAME}`, 'second connection to scrcpy for control');
384474
if (this.closed) {
385475
this.stop();
386476
return this;
@@ -406,7 +496,10 @@ export default class Scrcpy extends EventEmitter {
406496
}
407497

408498
if (this.config.sendFrameMeta) {
409-
void this.startStreamWithMeta().catch(() => this.stop());
499+
void this.startStreamWithMeta().catch((e) => {
500+
this._setFatalError(e);
501+
this.stop();
502+
});
410503
} else {
411504
this.startStreamRaw();
412505
}
@@ -423,6 +516,11 @@ export default class Scrcpy extends EventEmitter {
423516
this.videoSocket = undefined;
424517
close = true;
425518
}
519+
if (this.audioSocket) {
520+
this.audioSocket.destroy();
521+
this.audioSocket = undefined;
522+
close = true;
523+
}
426524
if (this.controlSocket) {
427525
this.controlSocket.destroy();
428526
this.controlSocket = undefined;
@@ -450,17 +548,32 @@ export default class Scrcpy extends EventEmitter {
450548
* get resolve once capture stop
451549
*/
452550
private async startStreamWithMeta(): Promise<void> {
551+
const strVersion = this.strVersion;
453552
assert(this.videoSocket);
454553
this.videoSocket.stream.pause();
455554
await Utils.waitforReadable(this.videoSocket, 0, 'videoSocket header');
456-
const chunk = this.videoSocket.stream.read(68) as Buffer;
457-
const name = chunk.toString('utf8', 0, 64).trim();
458-
this.setName(name);
459-
const width = chunk.readUint16BE(64);
460-
this.setWidth(width);
461-
const height = chunk.readUint16BE(66);
462-
this.setHeight(height);
463-
555+
if (strVersion >= "02.00.00") {
556+
const chunk = this.videoSocket.stream.read(64) as Buffer;
557+
if (!chunk)
558+
throw Error('fail to read firstChunk, inclease tunnelDelay for this device.');
559+
const name = chunk.toString('utf8', 0, 64).trim();
560+
this.setName(name);
561+
// const width = chunk.readUint16BE(64);
562+
// this.setWidth(width);
563+
// const height = chunk.readUint16BE(66);
564+
// this.setHeight(height);
565+
} else {
566+
const chunk = this.videoSocket.stream.read(68) as Buffer;
567+
if (!chunk)
568+
throw Error('fail to read firstChunk, inclease tunnelDelay for this device.');
569+
const name = chunk.toString('utf8', 0, 64).trim();
570+
this.setName(name);
571+
const width = chunk.readUint16BE(64);
572+
this.setWidth(width);
573+
const height = chunk.readUint16BE(66);
574+
this.setHeight(height);
575+
}
576+
464577
// let header: Uint8Array | undefined;
465578

466579
let pts = BigInt(0);// Buffer.alloc(0);
@@ -475,12 +588,20 @@ export default class Scrcpy extends EventEmitter {
475588
// regular end condition
476589
return;
477590
}
478-
// console.log(frameMeta.toString('hex').replace(/(........)/g, '$1 '))
479-
pts = frameMeta.readBigUint64BE();
480-
len = frameMeta.readUInt32BE(8);
481-
// else {bufferInfo.presentationTimeUs - ptsOrigin}
482-
// debug(`\tHeader:PTS =`, pts);
483-
// debug(`\tHeader:len =`, len);
591+
if (strVersion >= "02.00.00") {
592+
const codecId = frameMeta.readUInt32BE(0);
593+
// Read width (4 bytes)
594+
const width = frameMeta.readUInt32BE(4);
595+
// Read height (4 bytes)
596+
const height = frameMeta.readUInt32BE(8);
597+
this.setWidth(width);
598+
this.setHeight(height);
599+
} else {
600+
pts = frameMeta.readBigUint64BE();
601+
len = frameMeta.readUInt32BE(8);
602+
// debug(`\tHeader:PTS =`, pts);
603+
// debug(`\tHeader:len =`, len);
604+
}
484605
}
485606

486607
const config = !!(pts & PACKET_FLAG_CONFIG);

0 commit comments

Comments
 (0)