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

Unverified Commit b5f58227 authored by Simon Chan's avatar Simon Chan
Browse files

refactor(scrcpy): small optimizations

parent 6b231546
Loading
Loading
Loading
Loading
+9 −4
Original line number Diff line number Diff line
@@ -5,16 +5,21 @@ import {
import { ScrcpyControlMessageType } from "./type.js";

/**
 * On Android, touching the screen with a finger will disable mouse cursor.
 * However, Scrcpy doesn't do that, and can inject two pointers at the same time.
 * This can cause finger events to be "ignored" because mouse is still the primary pointer.
 * On both Android and Windows, while both mouse and touch are supported input devices,
 * only one of them can be active at a time. Touch the screen with a finger will deactivate mouse,
 * and move the mouse will deactivate touch.
 *
 * This helper class injects an extra `ACTION_UP` event,
 * On Android, this is achieved by dispatching a `MotionEvent.ACTION_UP` event for the previous input type.
 * But on Chrome, there is no such event, causing both mouse and touch to be active at the same time.
 * This can cause the new input to appear as "ignored".
 *
 * This helper class synthesis `ACTION_UP` events when a different pointer type appears,
 * so Scrcpy server can remove the previously hovering pointer.
 */
export class ScrcpyHoverHelper {
    // AFAIK, only mouse and pen can have hover state
    // and you can't have two mouses or pens.
    // So remember the last hovering pointer is enough.
    private lastHoverMessage: ScrcpyInjectTouchControlMessage | undefined;

    public process(
+33 −18
Original line number Diff line number Diff line
@@ -40,7 +40,7 @@ export class ScrcpyControlMessageSerializer {
        this.scrollController = options.getScrollController();
    }

    public getTypeValue(type: ScrcpyControlMessageType): number {
    public getActualMessageType(type: ScrcpyControlMessageType): number {
        const value = this.types.indexOf(type);
        if (value === -1) {
            throw new Error("Not supported");
@@ -48,14 +48,24 @@ export class ScrcpyControlMessageSerializer {
        return value;
    }

    public addMessageType<T extends { type: ScrcpyControlMessageType }>(
        message: Omit<T, "type">,
        type: T["type"]
    ): T {
        (message as T).type = this.getActualMessageType(type);
        return message as T;
    }

    public injectKeyCode(
        message: Omit<ScrcpyInjectKeyCodeControlMessage, "type">
    ) {
        return this.writer.write(
            ScrcpyInjectKeyCodeControlMessage.serialize({
                ...message,
                type: this.getTypeValue(ScrcpyControlMessageType.InjectKeyCode),
            })
            ScrcpyInjectKeyCodeControlMessage.serialize(
                this.addMessageType(
                    message,
                    ScrcpyControlMessageType.InjectKeyCode
                )
            )
        );
    }

@@ -63,7 +73,9 @@ export class ScrcpyControlMessageSerializer {
        return this.writer.write(
            ScrcpyInjectTextControlMessage.serialize({
                text,
                type: this.getTypeValue(ScrcpyControlMessageType.InjectText),
                type: this.getActualMessageType(
                    ScrcpyControlMessageType.InjectText
                ),
            })
        );
    }
@@ -73,10 +85,12 @@ export class ScrcpyControlMessageSerializer {
     */
    public injectTouch(message: Omit<ScrcpyInjectTouchControlMessage, "type">) {
        return this.writer.write(
            ScrcpyInjectTouchControlMessage.serialize({
                ...message,
                type: this.getTypeValue(ScrcpyControlMessageType.InjectTouch),
            })
            ScrcpyInjectTouchControlMessage.serialize(
                this.addMessageType(
                    message,
                    ScrcpyControlMessageType.InjectTouch
                )
            )
        );
    }

@@ -86,13 +100,10 @@ export class ScrcpyControlMessageSerializer {
    public injectScroll(
        message: Omit<ScrcpyInjectScrollControlMessage, "type">
    ) {
        (message as ScrcpyInjectScrollControlMessage).type = this.getTypeValue(
            ScrcpyControlMessageType.InjectScroll
        );

        const data = this.scrollController.serializeScrollMessage(
            message as ScrcpyInjectScrollControlMessage
            this.addMessageType(message, ScrcpyControlMessageType.InjectScroll)
        );

        if (!data) {
            return;
        }
@@ -103,7 +114,9 @@ export class ScrcpyControlMessageSerializer {
    public async backOrScreenOn(action: AndroidKeyEventAction) {
        const buffer = this.options.serializeBackOrScreenOnControlMessage({
            action,
            type: this.getTypeValue(ScrcpyControlMessageType.BackOrScreenOn),
            type: this.getActualMessageType(
                ScrcpyControlMessageType.BackOrScreenOn
            ),
        });

        if (buffer) {
@@ -115,7 +128,7 @@ export class ScrcpyControlMessageSerializer {
        return this.writer.write(
            ScrcpySetScreenPowerModeControlMessage.serialize({
                mode,
                type: this.getTypeValue(
                type: this.getActualMessageType(
                    ScrcpyControlMessageType.SetScreenPowerMode
                ),
            })
@@ -125,7 +138,9 @@ export class ScrcpyControlMessageSerializer {
    public rotateDevice() {
        return this.writer.write(
            ScrcpyRotateDeviceControlMessage.serialize({
                type: this.getTypeValue(ScrcpyControlMessageType.RotateDevice),
                type: this.getActualMessageType(
                    ScrcpyControlMessageType.RotateDevice
                ),
            })
        );
    }
+62 −16
Original line number Diff line number Diff line
@@ -55,8 +55,8 @@ class BitReader {
 * Split NAL units from a H.264 Annex B stream.
 *
 * The input is not modified.
 * The returned NAL units are views of the input (no memory allocation and copy),
 * but still contains emulation prevention bytes.
 * The returned NAL units are views of the input (no memory allocation nor copy),
 * and still contains emulation prevention bytes.
 *
 * This methods returns a generator, so it can be stopped immediately
 * after the interested NAL unit is found.
@@ -160,13 +160,66 @@ export function removeH264Emulation(buffer: Uint8Array) {
    let zeroCount = 0;
    let inEmulation = false;

    for (let i = 0; i < buffer.length; i += 1) {
    let i = 0;
    scan: for (; i < buffer.length; i += 1) {
        const byte = buffer[i]!;

        if (byte === 0x00) {
            zeroCount += 1;
            continue;
        }

        // Current byte is not zero
        const prevZeroCount = zeroCount;
        zeroCount = 0;

        if (prevZeroCount < 2) {
            // zero or one `0x00`s are acceptable
            continue;
        }

        if (byte === 0x01) {
            // Unexpected start code
            throw new Error("Invalid data");
        }

        if (prevZeroCount > 2) {
            // Too much `0x00`s
            throw new Error("Invalid data");
        }

        switch (byte) {
            case 0x02:
                // Didn't find why, but 7.4.1 NAL unit semantics forbids `0x000002` appearing in NAL units
                throw new Error("Invalid data");
            case 0x03:
                // `0x000003` is the "emulation_prevention_three_byte"
                // `0x00000300`, `0x00000301`, `0x00000302` and `0x00000303` represent
                // `0x000000`, `0x000001`, `0x000002` and `0x000003` respectively
                inEmulation = true;

                // Create output and copy the data before the emulation prevention byte
                // Output size is unknown, so we use the input size as an upper bound
                output = new Uint8Array(buffer.length - 1);
                output.set(buffer.subarray(0, i - prevZeroCount));
                outputOffset = i - prevZeroCount + 1;
                break scan;
            default:
                // `0x000004` or larger are as-is
                break;
        }
    }

    if (!output) {
        return buffer;
    }

    // Continue at the byte after the emulation prevention byte
    for (; i < buffer.length; i += 1) {
        const byte = buffer[i]!;

        if (output) {
        output[outputOffset] = byte;
        outputOffset += 1;
        }

        if (inEmulation) {
            if (byte > 0x03) {
@@ -211,15 +264,8 @@ export function removeH264Emulation(buffer: Uint8Array) {
                // `0x000000`, `0x000001`, `0x000002` and `0x000003` respectively
                inEmulation = true;

                if (!output) {
                    // Create output and copy the data before the emulation prevention byte
                    output = new Uint8Array(buffer.length - 1);
                    output.set(buffer.subarray(0, i - prevZeroCount));
                    outputOffset = i - prevZeroCount + 1;
                } else {
                // Remove the emulation prevention byte
                outputOffset -= 1;
                }
                break;
            default:
                // `0x000004` or larger are as-is
@@ -227,7 +273,7 @@ export function removeH264Emulation(buffer: Uint8Array) {
        }
    }

    return output?.subarray(0, outputOffset) ?? buffer;
    return output.subarray(0, outputOffset);
}

// 7.3.2.1.1 Sequence parameter set data syntax
+90 −0
Original line number Diff line number Diff line
import { describe, expect, it } from "@jest/globals";

import { ScrcpyControlMessageType } from "../../control/index.js";

import { ScrcpyScrollController1_16 } from "./scroll.js";

describe("ScrcpyScrollController1_16", () => {
    it("should return undefined when scroll distance is less than 1", () => {
        const controller = new ScrcpyScrollController1_16();
        const message = controller.serializeScrollMessage({
            type: ScrcpyControlMessageType.InjectScroll,
            pointerX: 0,
            pointerY: 0,
            screenWidth: 0,
            screenHeight: 0,
            scrollX: 0.5,
            scrollY: 0.5,
            buttons: 0,
        });
        expect(message).toBeUndefined();
    });

    it("should return a message when scroll distance is greater than 1", () => {
        const controller = new ScrcpyScrollController1_16();
        const message = controller.serializeScrollMessage({
            type: ScrcpyControlMessageType.InjectScroll,
            pointerX: 0,
            pointerY: 0,
            screenWidth: 0,
            screenHeight: 0,
            scrollX: 1.5,
            scrollY: 1.5,
            buttons: 0,
        });
        expect(message).toBeInstanceOf(Uint8Array);
        expect(message).toHaveProperty("byteLength", 21);
    });

    it("should return a message when accumulated scroll distance is greater than 1", () => {
        const controller = new ScrcpyScrollController1_16();
        controller.serializeScrollMessage({
            type: ScrcpyControlMessageType.InjectScroll,
            pointerX: 0,
            pointerY: 0,
            screenWidth: 0,
            screenHeight: 0,
            scrollX: 0.5,
            scrollY: 0.5,
            buttons: 0,
        });
        const message = controller.serializeScrollMessage({
            type: ScrcpyControlMessageType.InjectScroll,
            pointerX: 0,
            pointerY: 0,
            screenWidth: 0,
            screenHeight: 0,
            scrollX: 0.5,
            scrollY: 0.5,
            buttons: 0,
        });
        expect(message).toBeInstanceOf(Uint8Array);
        expect(message).toHaveProperty("byteLength", 21);
    });

    it("should return a message when accumulated scroll distance is less than -1", () => {
        const controller = new ScrcpyScrollController1_16();
        controller.serializeScrollMessage({
            type: ScrcpyControlMessageType.InjectScroll,
            pointerX: 0,
            pointerY: 0,
            screenWidth: 0,
            screenHeight: 0,
            scrollX: -0.5,
            scrollY: -0.5,
            buttons: 0,
        });
        const message = controller.serializeScrollMessage({
            type: ScrcpyControlMessageType.InjectScroll,
            pointerX: 0,
            pointerY: 0,
            screenWidth: 0,
            screenHeight: 0,
            scrollX: -0.5,
            scrollY: -0.5,
            buttons: 0,
        });
        expect(message).toBeInstanceOf(Uint8Array);
        expect(message).toHaveProperty("byteLength", 21);
    });
});
+13 −0
Original line number Diff line number Diff line
import { describe, expect, it } from "@jest/globals";

import { ScrcpyOptions1_21 } from "../1_21.js";

import { ScrcpyOptions1_22 } from "./options.js";

describe("ScrcpyOptions1_22", () => {
    it("should return a different scroll controller", () => {
        const controller1_21 = new ScrcpyOptions1_21({}).getScrollController();
        const controller1_22 = new ScrcpyOptions1_22({}).getScrollController();
        expect(controller1_22).not.toBe(controller1_21);
    });
});
Loading