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,
bitRate: 4_000_000,
tunnelForward: this.tunnelForward,
sendDeviceMeta: false,
sendDummyByte: false,
})
);
if (encoders.length === 0) {
@ -468,6 +470,8 @@ class ScrcpyPageState {
lockVideoOrientation: ScrcpyScreenOrientation.Unlocked,
tunnelForward: this.tunnelForward,
encoderName: this.selectedEncoder ?? encoders[0],
sendDeviceMeta: false,
sendDummyByte: false,
codecOptions: new CodecOptions({
profile: decoder.maxProfile,
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 =
new Struct()
.int64('pts')
@ -95,6 +90,8 @@ export class ScrcpyClient {
// Provide an invalid encoder name
// So the server will return all available encoders
options.value.encoderName = '_';
// Disable control for faster connection in 1.22+
options.value.control = false;
// Scrcpy server will open connections, before initializing encoder
// Thus although an invalid encoder name is given, the start process will success
@ -227,14 +224,6 @@ export class ScrcpyClient {
}
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;
while (this._running) {
const { pts, data } = await VideoPacket.deserialize(this.videoStream);
@ -307,7 +296,8 @@ export class ScrcpyClient {
private async receiveControl() {
if (!this.controlStream) {
throw new Error('receiveControl started before initialization');
// control disabled
return;
}
try {
@ -329,32 +319,38 @@ export class ScrcpyClient {
}
}
public async injectKeyCode(message: Omit<ScrcpyInjectKeyCodeControlMessage, 'type'>) {
if (!this.controlStream) {
throw new Error('injectKeyCode called before initialization');
private checkControlStream(caller: string) {
if (!this._running) {
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,
type: ScrcpyControlMessageType.InjectKeycode,
}));
}
public async injectText(text: string) {
if (!this.controlStream) {
throw new Error('injectText called before initialization');
}
const controlStream = this.checkControlStream('injectText');
await this.controlStream.write(ScrcpyInjectTextControlMessage.serialize({
await controlStream.write(ScrcpyInjectTextControlMessage.serialize({
type: ScrcpyControlMessageType.InjectText,
text,
}));
}
public async injectTouch(message: Omit<ScrcpyInjectTouchControlMessage, 'type' | 'screenWidth' | 'screenHeight'>) {
if (!this.controlStream) {
throw new Error('injectTouch called before initialization');
}
const controlStream = this.checkControlStream('injectTouch');
if (!this.screenWidth || !this.screenHeight) {
return;
@ -369,20 +365,17 @@ export class ScrcpyClient {
}
this.sendingTouchMessage = true;
const buffer = ScrcpyInjectTouchControlMessage.serialize({
await controlStream.write(ScrcpyInjectTouchControlMessage.serialize({
...message,
type: ScrcpyControlMessageType.InjectTouch,
screenWidth: this.screenWidth,
screenHeight: this.screenHeight,
});
await this.controlStream.write(buffer);
}));
this.sendingTouchMessage = false;
}
public async injectScroll(message: Omit<ScrcpyInjectScrollControlMessage1_22, 'type' | 'screenWidth' | 'screenHeight'>) {
if (!this.controlStream) {
throw new Error('injectScroll called before initialization');
}
const controlStream = this.checkControlStream('injectScroll');
if (!this.screenWidth || !this.screenHeight) {
return;
@ -394,17 +387,15 @@ export class ScrcpyClient {
screenWidth: this.screenWidth,
screenHeight: this.screenHeight,
});
await this.controlStream.write(buffer);
await controlStream.write(buffer);
}
public async pressBackOrTurnOnScreen(action: AndroidKeyEventAction) {
if (!this.controlStream) {
throw new Error('pressBackOrTurnOnScreen called before initialization');
}
const controlStream = this.checkControlStream('pressBackOrTurnOnScreen');
const buffer = this.options!.serializeBackOrScreenOnControlMessage(action, this.device);
if (buffer) {
await this.controlStream.write(buffer);
await controlStream.write(buffer);
}
}
@ -414,8 +405,13 @@ export class ScrcpyClient {
}
this._running = false;
this.videoStream?.close();
this.videoStream = undefined;
this.controlStream?.close();
this.controlStream = undefined;
await this.process?.kill();
}
}

View file

@ -3,16 +3,33 @@ import { Disposable } from "@yume-chan/event";
import { ValueOrPromise } from "@yume-chan/struct";
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 {
protected device: Adb;
public constructor(device: Adb) {
protected options: ScrcpyClientConnectionOptions;
public constructor(device: Adb, options: ScrcpyClientConnectionOptions) {
this.device = device;
this.options = options;
}
public initialize(): ValueOrPromise<void> { }
public abstract getStreams(): ValueOrPromise<[videoSteam: AdbBufferedStream, controlStream: AdbBufferedStream]>;
public abstract getStreams(): ValueOrPromise<[videoSteam: AdbBufferedStream, controlStream: AdbBufferedStream | undefined]>;
public dispose(): void { }
}
@ -33,18 +50,26 @@ export class ScrcpyClientForwardConnection extends ScrcpyClientConnection {
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();
if (this.options.sendDummyByte) {
// server will write a `0` to signal connection success
await stream.read(1);
}
return stream;
}
public async getStreams(): Promise<[videoSteam: AdbBufferedStream, controlStream: AdbBufferedStream]> {
return [
await this.connectAndReadByte(),
await this.connectAndRetry()
];
public async getStreams(): Promise<[videoSteam: AdbBufferedStream, controlStream: AdbBufferedStream | undefined]> {
const videoStream = await this.connectVideoStream();
let controlStream: AdbBufferedStream | undefined;
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());
}
public async getStreams(): Promise<[videoSteam: AdbBufferedStream, controlStream: AdbBufferedStream]> {
return [
await this.accept(),
await this.accept(),
];
public async getStreams(): Promise<[videoSteam: AdbBufferedStream, controlStream: AdbBufferedStream | undefined]> {
const videoStream = await this.accept();
let controlStream: AdbBufferedStream | undefined;
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() {

View file

@ -1,7 +1,7 @@
import type { Adb } from "@yume-chan/adb";
import Struct, { placeholder } from "@yume-chan/struct";
import { AndroidCodecLevel, AndroidCodecProfile } from "../codec";
import { ScrcpyClientConnection, ScrcpyClientForwardConnection, ScrcpyClientReverseConnection } from "../connection";
import { ScrcpyClientConnection, ScrcpyClientConnectionOptions, ScrcpyClientForwardConnection, ScrcpyClientReverseConnection } from "../connection";
import { AndroidKeyEventAction, ScrcpyControlMessageType } from "../message";
import type { ScrcpyInjectScrollControlMessage1_22 } from "./1_22";
import { ScrcpyLogLevel, ScrcpyOptions, ScrcpyOptionValue, ScrcpyScreenOrientation, toScrcpyOptionValue } from "./common";
@ -43,6 +43,11 @@ export interface ScrcpyOptions1_16Type {
bitRate: number;
/**
* 0 for unlimited.
*
* @default 0
*/
maxFps: number;
/**
@ -60,12 +65,14 @@ export interface ScrcpyOptions1_16Type {
/**
* 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;
/**
* TODO: Scrcpy 1.22 changed how `control: false` works, and it's not supported yet
* @default true
*/
control: boolean;
@ -156,10 +163,16 @@ export class ScrcpyOptions1_16<T extends ScrcpyOptions1_16Type = ScrcpyOptions1_
}
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) {
return new ScrcpyClientForwardConnection(device);
return new ScrcpyClientForwardConnection(device, options);
} 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 { ScrcpyClientConnection, ScrcpyClientConnectionOptions, ScrcpyClientForwardConnection, ScrcpyClientReverseConnection } from "../connection";
import { ScrcpyInjectScrollControlMessage1_16 } from "./1_16";
import { ScrcpyOptions1_21, ScrcpyOptions1_21Type } from "./1_21";
@ -8,21 +10,23 @@ export interface ScrcpyOptions1_22Type extends ScrcpyOptions1_21Type {
/**
* Send device name and size
*
* TODO: This is not implemented yet
* @default true
*/
sendDeviceMeta: boolean;
/**
* Write a byte on start to detect connection issues
*
* TODO: This is not implemented yet
* @default true
*/
sendDummyByte: boolean;
/**
* Implies `sendDeviceMeta: false`, `sendFrameMeta: false` and `sendDummyByte: false`
*
* TODO: This is not implemented yet
* @default false
*
* TODO: Add support for `sendFrameMeta: false`
*/
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> {
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);
}
@ -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(
message: ScrcpyInjectScrollControlMessage1_22,
): ArrayBuffer {