Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 4d0f1a11 authored by Simon Chan's avatar Simon Chan
Browse files

feat(scrcpy): support 1.22 new server options

fixes #374
parent 6750bbc3
Loading
Loading
Loading
Loading
+4 −0
Original line number Diff line number Diff line
@@ -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 −0

File mode changed from 100644 to 100755.

+31 −35
Original line number Diff line number Diff line
@@ -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'>) {
    private checkControlStream(caller: string) {
        if (!this._running) {
            throw new Error(`${caller} called before start`);
        }

        if (!this.controlStream) {
            throw new Error('injectKeyCode called before initialization');
            throw new Error(`${caller} called with control disabled`);
        }

        return this.controlStream;
    }

        await this.controlStream.write(ScrcpyInjectKeyCodeControlMessage.serialize({
    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();
    }
}
+46 −15
Original line number Diff line number Diff line
@@ -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() {
+18 −5
Original line number Diff line number Diff line
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);
        }
    }

Loading