Skip to content

Commit 9f191f6

Browse files
committed
feat: enhance Scrcpy class with codec handling and version parsing, update control message types
1 parent cccc7f2 commit 9f191f6

File tree

3 files changed

+159
-52
lines changed

3 files changed

+159
-52
lines changed

src/adb/thirdparty/scrcpy/Scrcpy.ts

Lines changed: 119 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import PromiseDuplex from 'promise-duplex';
77
import DeviceClient from '../../DeviceClient.js';
88
import Utils from '../../utils.js';
99
import { Duplex } from 'node:stream';
10-
import { type MotionEvent, MotionEventMap, OrientationMap, ControlMessageMap } from './ScrcpyConst.js';
10+
import { type MotionEvent, MotionEventMap, OrientationMap, ControlMessageMap, codexMap } from './ScrcpyConst.js';
1111
import { KeyCodes } from '../../keycode.js';
1212
import { BufWrite } from '../minicap/BufWrite.js';
1313
// import ThirdUtils from '../ThirdUtils.js';
@@ -88,6 +88,7 @@ export default class Scrcpy extends EventEmitter {
8888
///////
8989
// promise holders
9090
private _name: Promise<string>;
91+
private _codec: Promise<string>;
9192
private _width: Promise<number>;
9293
private _height: Promise<number>;
9394
private _onTermination: Promise<string>;
@@ -96,6 +97,7 @@ export default class Scrcpy extends EventEmitter {
9697
////////
9798
// promise resolve calls
9899

100+
private setCodec!: (name: string) => void;
99101
private setName!: (name: string) => void;
100102
private setWidth!: (width: number) => void;
101103
private setHeight!: (height: number) => void;
@@ -137,10 +139,25 @@ export default class Scrcpy extends EventEmitter {
137139
this._name = new Promise<string>((resolve) => this.setName = resolve);
138140
this._width = new Promise<number>((resolve) => this.setWidth = resolve);
139141
this._height = new Promise<number>((resolve) => this.setHeight = resolve);
142+
this._codec = new Promise<string>((resolve) => this.setCodec = resolve);
140143
this._onTermination = new Promise<string>((resolve) => this.setFatalError = resolve);
141144
this._firstFrame = new Promise<void>((resolve) => this.setFirstFrame = resolve);
145+
146+
let versionSplit = this.config.version.split(".").map(Number);
147+
if (versionSplit.length === 2) {
148+
versionSplit = [...versionSplit, 0];
149+
}
150+
this.major = versionSplit[0];
151+
this.minor = versionSplit[1];
152+
this.patch = versionSplit[2];
153+
this.strVersion = `${this.major.toString().padStart(2, '0')}.${this.minor.toString().padStart(2, '0')}.${this.patch.toString().padStart(2, '0')}`;
142154
}
143155

156+
readonly strVersion: string;
157+
readonly major: number;
158+
readonly minor: number;
159+
readonly patch: number;
160+
144161
public override on = <K extends keyof IEmissions>(event: K, listener: IEmissions[K]): this => super.on(event, listener)
145162
public override off = <K extends keyof IEmissions>(event: K, listener: IEmissions[K]): this => super.off(event, listener)
146163
public override once = <K extends keyof IEmissions>(event: K, listener: IEmissions[K]): this => super.once(event, listener)
@@ -162,15 +179,22 @@ export default class Scrcpy extends EventEmitter {
162179
*/
163180
get firstFrame(): Promise<void> { return this._firstFrame; }
164181

182+
/**
183+
* return the used codex can be "H264", "H265", "AV1", "AAC" or "OPUS"
184+
*/
185+
get codec(): Promise<string> { return this._codec; }
186+
165187
/**
166188
* emit scrcpyServer output as Error
167189
* @param duplex
168190
* @returns
169191
*/
170-
async throwsErrors(duplex: PromiseDuplex<Duplex>) {
192+
async ListenErrors(duplex: PromiseDuplex<Duplex>) {
171193
try {
172194
const errors = [];
173195
for (; ;) {
196+
if (!duplex.readable) // the server is stoped
197+
break;
174198
await Utils.waitforReadable(duplex, 0, 'wait for error from ScrcpyServer');
175199
const data = await duplex.read();
176200
if (data) {
@@ -191,6 +215,7 @@ export default class Scrcpy extends EventEmitter {
191215
}
192216
// eslint-disable-next-line @typescript-eslint/no-unused-vars
193217
} catch (e: unknown) {
218+
// must never throw
194219
//this.emit('error', e as Error);
195220
//this.setError((e as Error).message);
196221
}
@@ -239,13 +264,6 @@ export default class Scrcpy extends EventEmitter {
239264
throw Error(`Unsupported message type:${type}`);
240265
}
241266
}
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-
}
249267

250268
private _getStartupLine(jarDest: string): string {
251269
const args: Array<string | number | boolean> = [];
@@ -269,7 +287,7 @@ export default class Scrcpy extends EventEmitter {
269287
}
270288
// args.push(this.config.version); // arg 0 Scrcpy server version
271289
//if (this.config.version <= 20) {
272-
if (versionStr <= "02.00.00") {
290+
if (this.major < 2) {
273291
// Version 11 => 20
274292
args.push("info"); // Log level: info, verbose...
275293
args.push(maxSize); // Max screen width (long side)
@@ -287,7 +305,7 @@ export default class Scrcpy extends EventEmitter {
287305
args.push(encoderName || '-'); // Encoder name
288306
args.push(powerOffScreenOnClose); // Power off screen after server closed
289307
} else {
290-
if (versionStr >= "02.00.00") {
308+
if (this.major >= 2) {
291309
args.push(`scid=${this.config.scid}`);
292310
if (this.config.noAudio)
293311
args.push(`audio=false`);
@@ -302,7 +320,7 @@ export default class Scrcpy extends EventEmitter {
302320
args.push("log_level=info");
303321
args.push(`max_size=${maxSize}`);
304322
args.push("clipboard_autosync=false"); // cause crash on some newer phone and we do not use that feature.
305-
if (versionStr >= "02.00.00") {
323+
if (this.major >= 2) {
306324
args.push(`video_bit_rate=${bitrate}`);
307325
} else {
308326
args.push(`bit_rate=${bitrate}`);
@@ -433,14 +451,14 @@ export default class Scrcpy extends EventEmitter {
433451
}
434452
if (stdoutContent.includes('[server] INFO: Device: '))
435453
break;
454+
// console.log('stdoutContent:', stdoutContent);
436455
}
437456

438-
this.throwsErrors(this.scrcpyServer);
457+
this.ListenErrors(this.scrcpyServer).then(() => {}, () => {});
439458

440459
// from V2.0 SC_SOCKET_NAME name can be change
441-
const strVersion = this.strVersion;
442460
let SC_SOCKET_NAME = 'scrcpy';
443-
if (strVersion >= "02.00.00") {
461+
if (this.major >= 2) {
444462
SC_SOCKET_NAME = SC_SOCKET_NAME_PREFIX + this.config.scid;
445463
assert(this.config.scid.length == 8, `scid length should be 8`);
446464
}
@@ -454,13 +472,16 @@ export default class Scrcpy extends EventEmitter {
454472
// Connect videoSocket
455473
await Utils.delay(100);
456474
this.videoSocket = await this.client.openLocal2(`localabstract:${SC_SOCKET_NAME}`, 'first connection to scrcpy for video');
475+
this.videoSocket.stream.on('error', (e) => {
476+
console.error('videoSocket error', e);
477+
});
457478

458479
if (this.closed) {
459480
this.stop();
460481
return this;
461482
}
462483

463-
if (strVersion >= "02.00.00" && !this.config.noAudio) {
484+
if (this.major >= 2 && !this.config.noAudio) {
464485
// Connect audioSocket
465486
this.audioSocket = await this.client.openLocal2(`localabstract:${SC_SOCKET_NAME}`, 'first connection to scrcpy for audio');
466487
// Connect controlSocket
@@ -478,14 +499,14 @@ export default class Scrcpy extends EventEmitter {
478499
// First chunk is 69 bytes length -> 1 dummy byte, 64 bytes for deviceName, 2 bytes for width & 2 bytes for height
479500
try {
480501
await Utils.waitforReadable(this.videoSocket, 0, 'videoSocket 1st 1 bit chunk');
481-
const firstChunk = await this.videoSocket.read(1) as Buffer;
502+
const firstChunk = await this.videoSocket.read(1) as Uint8Array;
482503
if (!firstChunk) {
483504
throw Error('fail to read firstChunk, inclease tunnelDelay for this device.');
484505
}
485506

486507
// old protocol
487-
const control = (firstChunk as unknown as Uint8Array).at(0);
488-
if ((firstChunk as unknown as Uint8Array).at(0) !== 0) {
508+
const control = firstChunk.at(0);
509+
if (firstChunk.at(0) !== 0) {
489510
if (control)
490511
throw Error(`Control code should be 0x00, receves: 0x${control.toString(16).padStart(2, '0')}`);
491512
throw Error(`Control code should be 0x00, receves nothing.`);
@@ -552,7 +573,7 @@ export default class Scrcpy extends EventEmitter {
552573
assert(this.videoSocket);
553574
this.videoSocket.stream.pause();
554575
await Utils.waitforReadable(this.videoSocket, 0, 'videoSocket header');
555-
if (strVersion >= "02.00.00") {
576+
if (this.major >= 2) {
556577
const chunk = this.videoSocket.stream.read(64) as Buffer;
557578
if (!chunk)
558579
throw Error('fail to read firstChunk, inclease tunnelDelay for this device.');
@@ -574,7 +595,41 @@ export default class Scrcpy extends EventEmitter {
574595
this.setHeight(height);
575596
}
576597

598+
let codec = "H264";
577599
// let header: Uint8Array | undefined;
600+
if (this.major >= 2) {
601+
const frameMeta = this.videoSocket.stream.read(12) as Buffer;
602+
const codecId = frameMeta.readUInt32BE(0);
603+
// Read width (4 bytes)
604+
const width = frameMeta.readUInt32BE(4);
605+
// Read height (4 bytes)
606+
const height = frameMeta.readUInt32BE(8);
607+
switch (codecId) {
608+
case codexMap.H264:
609+
codec = "H264";
610+
break;
611+
case codexMap.H265:
612+
codec = "H265";
613+
break;
614+
case codexMap.AV1:
615+
codec = "AV1";
616+
break;
617+
case codexMap.OPUS:
618+
codec = "OPUS";
619+
break;
620+
case codexMap.AAC:
621+
codec = "AAC";
622+
break;
623+
case codexMap.RAW:
624+
codec = "RAW";
625+
break;
626+
default:
627+
codec = "UNKNOWN";
628+
}
629+
this.setCodec(codec);
630+
this.setWidth(width);
631+
this.setHeight(height);
632+
}
578633

579634
let pts = BigInt(0);// Buffer.alloc(0);
580635
for (; ;) {
@@ -588,34 +643,29 @@ export default class Scrcpy extends EventEmitter {
588643
// regular end condition
589644
return;
590645
}
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-
}
646+
pts = frameMeta.readBigUint64BE();
647+
len = frameMeta.readUInt32BE(8);
648+
// debug(`\tHeader:PTS =`, pts);
649+
// debug(`\tHeader:len =`, len);
605650
}
606651

607652
const config = !!(pts & PACKET_FLAG_CONFIG);
608653

609-
let streamChunk: Buffer | null = null;
654+
let streamChunk: Uint8Array | null = null; // Buffer
610655
while (streamChunk === null) {
656+
if (!this.videoSocket) // the server is stoped
657+
break;
658+
if (!this.videoSocket.stream.readable) // the server is stoped
659+
break;
611660
await Utils.waitforReadable(this.videoSocket, 0, 'videoSocket streamChunk');
612-
streamChunk = this.videoSocket.stream.read(len) as Buffer;
661+
streamChunk = this.videoSocket.stream.read(len) as Uint8Array;
613662
if (streamChunk) {
614-
if (config) { // non-media data packet len: 33
663+
// const chunk_Uint8Array = streamChunk as unknown as Uint8Array;
664+
if (config) { // non-media data packet len: 30 .. 33
615665
/**
616666
* is a config package pts have PACKET_FLAG_CONFIG flag
617667
*/
618-
const sequenceParameterSet = parse_sequence_parameter_set(streamChunk as unknown as ArrayBuffer);
668+
const sequenceParameterSet = parse_sequence_parameter_set(streamChunk, codec);
619669
const {
620670
profile_idc: profileIndex,
621671
constraint_set: constraintSet,
@@ -640,7 +690,7 @@ export default class Scrcpy extends EventEmitter {
640690
const videoConf: H264Configuration = {
641691
profileIndex, constraintSet, levelIndex, encodedWidth, encodedHeight,
642692
cropLeft, cropRight, cropTop, cropBottom, croppedWidth, croppedHeight,
643-
data: streamChunk as unknown as Uint8Array,
693+
data: streamChunk,
644694
};
645695
this.lastConf = videoConf;
646696
this.emit('config', videoConf);
@@ -652,7 +702,7 @@ export default class Scrcpy extends EventEmitter {
652702
if (keyframe) {
653703
pts &= ~PACKET_FLAG_KEY_FRAME;
654704
}
655-
const frame = { keyframe, pts, data: streamChunk as unknown as Uint8Array, config: this.lastConf };
705+
const frame = { keyframe, pts, data: streamChunk, config: this.lastConf };
656706
if (this.setFirstFrame) {
657707
this.setFirstFrame();
658708
this.setFirstFrame = undefined;
@@ -707,12 +757,19 @@ export default class Scrcpy extends EventEmitter {
707757
* @param position
708758
* @param screenSize
709759
* @param pressure
760+
*
761+
* see parseInjectTouchEvent()
710762
*/
711763
// usb.data_len == 28
712-
async injectTouchEvent(action: MotionEvent, pointerId: bigint, position: Point, screenSize: Point, pressure?: number): Promise<void> {
713-
const chunk = new BufWrite(28);
764+
async injectTouchEvent(action: MotionEvent, pointerId: bigint, position: Point, screenSize: Point, pressure?: number): Promise<boolean> {
765+
let size = 28;
766+
if (this.major >= 2) {
767+
size += 4;
768+
}
769+
const chunk = new BufWrite(size);
714770
chunk.writeUint8(ControlMessageMap.TYPE_INJECT_TOUCH_EVENT);
715-
chunk.writeUint8(action);
771+
chunk.writeUint8(action); // action readUnsignedByte
772+
716773
if (pressure === undefined) {
717774
if (action == MotionEventMap.ACTION_UP)
718775
pressure = 0x0
@@ -722,15 +779,26 @@ export default class Scrcpy extends EventEmitter {
722779
pressure = 0xffff
723780
}
724781
// Writes a long to the underlying output stream as eight bytes, high byte first.
725-
chunk.writeBigUint64BE(pointerId);
726-
chunk.writeUint32BE(position.x | 0);
727-
chunk.writeUint32BE(position.y | 0);
728-
chunk.writeUint16BE(screenSize.x | 0);
729-
chunk.writeUint16BE(screenSize.y | 0);
730-
chunk.writeUint16BE(pressure);
731-
chunk.writeUint32BE(MotionEventMap.BUTTON_PRIMARY);
782+
chunk.writeBigUint64BE(pointerId); // long pointerId = dis.readLong();
783+
// Position position = parsePosition();
784+
chunk.writeUint32BE(position.x | 0); // int x = dis.readInt();
785+
chunk.writeUint32BE(position.y | 0); // int y = dis.readInt();
786+
chunk.writeUint16BE(screenSize.x | 0); // int screenWidth = dis.readUnsignedShort();
787+
chunk.writeUint16BE(screenSize.y | 0); // int screenHeight = dis.readUnsignedShort();
788+
chunk.writeUint16BE(pressure); // Binary.u16FixedPointToFloat(dis.readShort());
789+
chunk.writeUint32BE(MotionEventMap.BUTTON_PRIMARY); // int actionButton = dis.readInt();
790+
if (this.major >= 2) {
791+
chunk.writeUint32BE(MotionEventMap.BUTTON_PRIMARY); // int buttons = dis.readInt();
792+
}
732793
assert(this.controlSocket);
733-
await this.controlSocket.write(chunk.buffer);
794+
try {
795+
await this.controlSocket.write(chunk.buffer);
796+
return true;
797+
} catch (e) {
798+
debug(`injectTouchEvent failed:`, e);
799+
return false;
800+
// if the device is not connected anymore, we can not write to the controlSocket
801+
}
734802
// console.log(chunk.buffer.toString('hex'))
735803
}
736804

0 commit comments

Comments
 (0)