feat(scrcpy): support 1.22 new server options

fixes #374
This commit is contained in:
Simon Chan 2022-02-07 11:19:33 +08:00
parent 6750bbc367
commit 4d0f1a11cb
6 changed files with 135 additions and 59 deletions

View file

@ -417,6 +417,8 @@ class ScrcpyPageState {
logLevel: ScrcpyLogLevel.Debug, logLevel: ScrcpyLogLevel.Debug,
bitRate: 4_000_000, bitRate: 4_000_000,
tunnelForward: this.tunnelForward, tunnelForward: this.tunnelForward,
sendDeviceMeta: false,
sendDummyByte: false,
}) })
); );
if (encoders.length === 0) { if (encoders.length === 0) {
@ -468,6 +470,8 @@ class ScrcpyPageState {
lockVideoOrientation: ScrcpyScreenOrientation.Unlocked, lockVideoOrientation: ScrcpyScreenOrientation.Unlocked,
tunnelForward: this.tunnelForward, tunnelForward: this.tunnelForward,
encoderName: this.selectedEncoder ?? encoders[0], encoderName: this.selectedEncoder ?? encoders[0],
sendDeviceMeta: false,
sendDummyByte: false,
codecOptions: new CodecOptions({ codecOptions: new CodecOptions({
profile: decoder.maxProfile, profile: decoder.maxProfile,
level: decoder.maxLevel, level: decoder.maxLevel,

0
libraries/scrcpy/scripts/fetch-server.cjs Normal file → Executable file
View file

View file

@ -25,11 +25,6 @@ function* splitLines(text: string): Generator<string, void, void> {
} }
} }
const Size =
new Struct()
.uint16('width')
.uint16('height');
const VideoPacket = const VideoPacket =
new Struct() new Struct()
.int64('pts') .int64('pts')
@ -95,6 +90,8 @@ export class ScrcpyClient {
// Provide an invalid encoder name // Provide an invalid encoder name
// So the server will return all available encoders // So the server will return all available encoders
options.value.encoderName = '_'; options.value.encoderName = '_';
// Disable control for faster connection in 1.22+
options.value.control = false;
// Scrcpy server will open connections, before initializing encoder // Scrcpy server will open connections, before initializing encoder
// Thus although an invalid encoder name is given, the start process will success // Thus although an invalid encoder name is given, the start process will success
@ -227,14 +224,6 @@ export class ScrcpyClient {
} }
try { try {
// Device name, we don't need it
await this.videoStream.read(64);
// Initial video size
const { width, height } = await Size.deserialize(this.videoStream);
this._screenWidth = width;
this._screenHeight = height;
let buffer: ArrayBuffer | undefined; let buffer: ArrayBuffer | undefined;
while (this._running) { while (this._running) {
const { pts, data } = await VideoPacket.deserialize(this.videoStream); const { pts, data } = await VideoPacket.deserialize(this.videoStream);
@ -307,7 +296,8 @@ export class ScrcpyClient {
private async receiveControl() { private async receiveControl() {
if (!this.controlStream) { if (!this.controlStream) {
throw new Error('receiveControl started before initialization'); // control disabled
return;
} }
try { try {
@ -329,32 +319,38 @@ export class ScrcpyClient {
} }
} }
public async injectKeyCode(message: Omit<ScrcpyInjectKeyCodeControlMessage, 'type'>) { private checkControlStream(caller: string) {
if (!this.controlStream) { if (!this._running) {
throw new Error('injectKeyCode called before initialization'); throw new Error(`${caller} called before start`);
} }
await this.controlStream.write(ScrcpyInjectKeyCodeControlMessage.serialize({ if (!this.controlStream) {
throw new Error(`${caller} called with control disabled`);
}
return this.controlStream;
}
public async injectKeyCode(message: Omit<ScrcpyInjectKeyCodeControlMessage, 'type'>) {
const controlStream = this.checkControlStream('injectKeyCode');
await controlStream.write(ScrcpyInjectKeyCodeControlMessage.serialize({
...message, ...message,
type: ScrcpyControlMessageType.InjectKeycode, type: ScrcpyControlMessageType.InjectKeycode,
})); }));
} }
public async injectText(text: string) { public async injectText(text: string) {
if (!this.controlStream) { const controlStream = this.checkControlStream('injectText');
throw new Error('injectText called before initialization');
}
await this.controlStream.write(ScrcpyInjectTextControlMessage.serialize({ await controlStream.write(ScrcpyInjectTextControlMessage.serialize({
type: ScrcpyControlMessageType.InjectText, type: ScrcpyControlMessageType.InjectText,
text, text,
})); }));
} }
public async injectTouch(message: Omit<ScrcpyInjectTouchControlMessage, 'type' | 'screenWidth' | 'screenHeight'>) { public async injectTouch(message: Omit<ScrcpyInjectTouchControlMessage, 'type' | 'screenWidth' | 'screenHeight'>) {
if (!this.controlStream) { const controlStream = this.checkControlStream('injectTouch');
throw new Error('injectTouch called before initialization');
}
if (!this.screenWidth || !this.screenHeight) { if (!this.screenWidth || !this.screenHeight) {
return; return;
@ -369,20 +365,17 @@ export class ScrcpyClient {
} }
this.sendingTouchMessage = true; this.sendingTouchMessage = true;
const buffer = ScrcpyInjectTouchControlMessage.serialize({ await controlStream.write(ScrcpyInjectTouchControlMessage.serialize({
...message, ...message,
type: ScrcpyControlMessageType.InjectTouch, type: ScrcpyControlMessageType.InjectTouch,
screenWidth: this.screenWidth, screenWidth: this.screenWidth,
screenHeight: this.screenHeight, screenHeight: this.screenHeight,
}); }));
await this.controlStream.write(buffer);
this.sendingTouchMessage = false; this.sendingTouchMessage = false;
} }
public async injectScroll(message: Omit<ScrcpyInjectScrollControlMessage1_22, 'type' | 'screenWidth' | 'screenHeight'>) { public async injectScroll(message: Omit<ScrcpyInjectScrollControlMessage1_22, 'type' | 'screenWidth' | 'screenHeight'>) {
if (!this.controlStream) { const controlStream = this.checkControlStream('injectScroll');
throw new Error('injectScroll called before initialization');
}
if (!this.screenWidth || !this.screenHeight) { if (!this.screenWidth || !this.screenHeight) {
return; return;
@ -394,17 +387,15 @@ export class ScrcpyClient {
screenWidth: this.screenWidth, screenWidth: this.screenWidth,
screenHeight: this.screenHeight, screenHeight: this.screenHeight,
}); });
await this.controlStream.write(buffer); await controlStream.write(buffer);
} }
public async pressBackOrTurnOnScreen(action: AndroidKeyEventAction) { public async pressBackOrTurnOnScreen(action: AndroidKeyEventAction) {
if (!this.controlStream) { const controlStream = this.checkControlStream('pressBackOrTurnOnScreen');
throw new Error('pressBackOrTurnOnScreen called before initialization');
}
const buffer = this.options!.serializeBackOrScreenOnControlMessage(action, this.device); const buffer = this.options!.serializeBackOrScreenOnControlMessage(action, this.device);
if (buffer) { if (buffer) {
await this.controlStream.write(buffer); await controlStream.write(buffer);
} }
} }
@ -414,8 +405,13 @@ export class ScrcpyClient {
} }
this._running = false; this._running = false;
this.videoStream?.close(); this.videoStream?.close();
this.videoStream = undefined;
this.controlStream?.close(); this.controlStream?.close();
this.controlStream = undefined;
await this.process?.kill(); await this.process?.kill();
} }
} }

View file

@ -3,16 +3,33 @@ import { Disposable } from "@yume-chan/event";
import { ValueOrPromise } from "@yume-chan/struct"; import { ValueOrPromise } from "@yume-chan/struct";
import { delay } from "./utils"; import { delay } from "./utils";
export interface ScrcpyClientConnectionOptions {
control: boolean;
/**
* Write a byte on start to detect connection issues
*/
sendDummyByte: boolean;
/**
* Send device name and size
*/
sendDeviceMeta: boolean;
}
export abstract class ScrcpyClientConnection implements Disposable { export abstract class ScrcpyClientConnection implements Disposable {
protected device: Adb; protected device: Adb;
public constructor(device: Adb) { protected options: ScrcpyClientConnectionOptions;
public constructor(device: Adb, options: ScrcpyClientConnectionOptions) {
this.device = device; this.device = device;
this.options = options;
} }
public initialize(): ValueOrPromise<void> { } public initialize(): ValueOrPromise<void> { }
public abstract getStreams(): ValueOrPromise<[videoSteam: AdbBufferedStream, controlStream: AdbBufferedStream]>; public abstract getStreams(): ValueOrPromise<[videoSteam: AdbBufferedStream, controlStream: AdbBufferedStream | undefined]>;
public dispose(): void { } public dispose(): void { }
} }
@ -33,18 +50,26 @@ export class ScrcpyClientForwardConnection extends ScrcpyClientConnection {
throw new Error(`Can't connect to server after 100 retries`); throw new Error(`Can't connect to server after 100 retries`);
} }
private async connectAndReadByte(): Promise<AdbBufferedStream> { private async connectVideoStream(): Promise<AdbBufferedStream> {
const stream = await this.connectAndRetry(); const stream = await this.connectAndRetry();
// server will write a `0` to signal connection success if (this.options.sendDummyByte) {
await stream.read(1); // server will write a `0` to signal connection success
await stream.read(1);
}
return stream; return stream;
} }
public async getStreams(): Promise<[videoSteam: AdbBufferedStream, controlStream: AdbBufferedStream]> { public async getStreams(): Promise<[videoSteam: AdbBufferedStream, controlStream: AdbBufferedStream | undefined]> {
return [ const videoStream = await this.connectVideoStream();
await this.connectAndReadByte(), let controlStream: AdbBufferedStream | undefined;
await this.connectAndRetry() if (this.options.control) {
]; controlStream = await this.connectAndRetry();
}
if (this.options.sendDeviceMeta) {
// 64 bytes device name + 2 bytes video width + 2 bytes video height
await videoStream.read(64 + 2 + 2);
}
return [videoStream, controlStream];
} }
} }
@ -73,11 +98,17 @@ export class ScrcpyClientReverseConnection extends ScrcpyClientConnection {
return new AdbBufferedStream(await this.streams.dequeue()); return new AdbBufferedStream(await this.streams.dequeue());
} }
public async getStreams(): Promise<[videoSteam: AdbBufferedStream, controlStream: AdbBufferedStream]> { public async getStreams(): Promise<[videoSteam: AdbBufferedStream, controlStream: AdbBufferedStream | undefined]> {
return [ const videoStream = await this.accept();
await this.accept(), let controlStream: AdbBufferedStream | undefined;
await this.accept(), if (this.options.control) {
]; controlStream = await this.accept();
}
if (this.options.sendDeviceMeta) {
// 64 bytes device name + 2 bytes video width + 2 bytes video height
await videoStream.read(64 + 2 + 2);
}
return [videoStream, controlStream];
} }
public override dispose() { public override dispose() {

View file

@ -1,7 +1,7 @@
import type { Adb } from "@yume-chan/adb"; import type { Adb } from "@yume-chan/adb";
import Struct, { placeholder } from "@yume-chan/struct"; import Struct, { placeholder } from "@yume-chan/struct";
import { AndroidCodecLevel, AndroidCodecProfile } from "../codec"; import { AndroidCodecLevel, AndroidCodecProfile } from "../codec";
import { ScrcpyClientConnection, ScrcpyClientForwardConnection, ScrcpyClientReverseConnection } from "../connection"; import { ScrcpyClientConnection, ScrcpyClientConnectionOptions, ScrcpyClientForwardConnection, ScrcpyClientReverseConnection } from "../connection";
import { AndroidKeyEventAction, ScrcpyControlMessageType } from "../message"; import { AndroidKeyEventAction, ScrcpyControlMessageType } from "../message";
import type { ScrcpyInjectScrollControlMessage1_22 } from "./1_22"; import type { ScrcpyInjectScrollControlMessage1_22 } from "./1_22";
import { ScrcpyLogLevel, ScrcpyOptions, ScrcpyOptionValue, ScrcpyScreenOrientation, toScrcpyOptionValue } from "./common"; import { ScrcpyLogLevel, ScrcpyOptions, ScrcpyOptionValue, ScrcpyScreenOrientation, toScrcpyOptionValue } from "./common";
@ -43,6 +43,11 @@ export interface ScrcpyOptions1_16Type {
bitRate: number; bitRate: number;
/**
* 0 for unlimited.
*
* @default 0
*/
maxFps: number; maxFps: number;
/** /**
@ -60,12 +65,14 @@ export interface ScrcpyOptions1_16Type {
/** /**
* Send PTS so that the client may record properly * Send PTS so that the client may record properly
* *
* TODO: This is not implemented yet * @default true
*
* TODO: Add support for `sendFrameMeta: false`
*/ */
sendFrameMeta: boolean; sendFrameMeta: boolean;
/** /**
* TODO: Scrcpy 1.22 changed how `control: false` works, and it's not supported yet * @default true
*/ */
control: boolean; control: boolean;
@ -156,10 +163,16 @@ export class ScrcpyOptions1_16<T extends ScrcpyOptions1_16Type = ScrcpyOptions1_
} }
public createConnection(device: Adb): ScrcpyClientConnection { public createConnection(device: Adb): ScrcpyClientConnection {
const options: ScrcpyClientConnectionOptions = {
// Old scrcpy connection always have control stream no matter what the option is
control: true,
sendDummyByte: true,
sendDeviceMeta: true,
};
if (this.value.tunnelForward) { if (this.value.tunnelForward) {
return new ScrcpyClientForwardConnection(device); return new ScrcpyClientForwardConnection(device, options);
} else { } else {
return new ScrcpyClientReverseConnection(device); return new ScrcpyClientReverseConnection(device, options);
} }
} }

View file

@ -1,4 +1,6 @@
import { Adb } from "@yume-chan/adb";
import Struct from "@yume-chan/struct"; import Struct from "@yume-chan/struct";
import { ScrcpyClientConnection, ScrcpyClientConnectionOptions, ScrcpyClientForwardConnection, ScrcpyClientReverseConnection } from "../connection";
import { ScrcpyInjectScrollControlMessage1_16 } from "./1_16"; import { ScrcpyInjectScrollControlMessage1_16 } from "./1_16";
import { ScrcpyOptions1_21, ScrcpyOptions1_21Type } from "./1_21"; import { ScrcpyOptions1_21, ScrcpyOptions1_21Type } from "./1_21";
@ -8,21 +10,23 @@ export interface ScrcpyOptions1_22Type extends ScrcpyOptions1_21Type {
/** /**
* Send device name and size * Send device name and size
* *
* TODO: This is not implemented yet * @default true
*/ */
sendDeviceMeta: boolean; sendDeviceMeta: boolean;
/** /**
* Write a byte on start to detect connection issues * Write a byte on start to detect connection issues
* *
* TODO: This is not implemented yet * @default true
*/ */
sendDummyByte: boolean; sendDummyByte: boolean;
/** /**
* Implies `sendDeviceMeta: false`, `sendFrameMeta: false` and `sendDummyByte: false` * Implies `sendDeviceMeta: false`, `sendFrameMeta: false` and `sendDummyByte: false`
* *
* TODO: This is not implemented yet * @default false
*
* TODO: Add support for `sendFrameMeta: false`
*/ */
rawVideoStream: boolean; rawVideoStream: boolean;
} }
@ -36,6 +40,20 @@ export type ScrcpyInjectScrollControlMessage1_22 = typeof ScrcpyInjectScrollCont
export class ScrcpyOptions1_22<T extends ScrcpyOptions1_22Type = ScrcpyOptions1_22Type> extends ScrcpyOptions1_21<T> { export class ScrcpyOptions1_22<T extends ScrcpyOptions1_22Type = ScrcpyOptions1_22Type> extends ScrcpyOptions1_21<T> {
public constructor(init: Partial<ScrcpyOptions1_22Type>) { public constructor(init: Partial<ScrcpyOptions1_22Type>) {
if (init.rawVideoStream) {
// Set implied options for client-side processing
init.sendDeviceMeta = false;
init.sendFrameMeta = false;
init.sendDummyByte = false;
// TODO: Add support for `sendFrameMeta: false`
throw new Error('`rawVideoStream:true` is not supported');
}
if (!init.sendFrameMeta) {
// TODO: Add support for `sendFrameMeta: false`
throw new Error('`sendFrameMeta:false` is not supported');
}
super(init); super(init);
} }
@ -49,6 +67,20 @@ export class ScrcpyOptions1_22<T extends ScrcpyOptions1_22Type = ScrcpyOptions1_
}; };
} }
public override createConnection(device: Adb): ScrcpyClientConnection {
const defaultValue = this.getDefaultValue();
const options: ScrcpyClientConnectionOptions = {
control: this.value.control ?? defaultValue.control,
sendDummyByte: this.value.sendDummyByte ?? defaultValue.sendDummyByte,
sendDeviceMeta: this.value.sendDeviceMeta ?? defaultValue.sendDeviceMeta,
};
if (this.value.tunnelForward) {
return new ScrcpyClientForwardConnection(device, options);
} else {
return new ScrcpyClientReverseConnection(device, options);
}
}
public override serializeInjectScrollControlMessage( public override serializeInjectScrollControlMessage(
message: ScrcpyInjectScrollControlMessage1_22, message: ScrcpyInjectScrollControlMessage1_22,
): ArrayBuffer { ): ArrayBuffer {