refactor(scrcpy): small optimizations

This commit is contained in:
Simon Chan 2023-01-20 20:43:43 +08:00
parent 6b23154694
commit b5f58227fd
No known key found for this signature in database
GPG key ID: A8B69F750B9BCEDD
11 changed files with 359 additions and 71 deletions

View file

@ -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(

View file

@ -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(
this.addMessageType(message, ScrcpyControlMessageType.InjectScroll)
);
const data = this.scrollController.serializeScrollMessage(
message as ScrcpyInjectScrollControlMessage
);
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
),
})
);
}

View file

@ -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

View file

@ -0,0 +1,90 @@
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);
});
});

View file

@ -0,0 +1,13 @@
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);
});
});

View file

@ -0,0 +1,23 @@
import { describe, expect, it } from "@jest/globals";
import { ScrcpyControlMessageType } from "../../control/index.js";
import { ScrcpyScrollController1_22 } from "./scroll.js";
describe("ScrcpyScrollController1_22", () => {
it("should return correct message length", () => {
const controller = new ScrcpyScrollController1_22();
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", 25);
});
});

View file

@ -0,0 +1,13 @@
import { describe, expect, it } from "@jest/globals";
import { ScrcpyOptions1_24 } from "../1_24.js";
import { ScrcpyOptions1_25 } from "./options.js";
describe("ScrcpyOptions1_25", () => {
it("should return a different scroll controller", () => {
const controller1_24 = new ScrcpyOptions1_24({}).getScrollController();
const controller1_25 = new ScrcpyOptions1_25({}).getScrollController();
expect(controller1_25).not.toBe(controller1_24);
});
});

View file

@ -0,0 +1,76 @@
import { describe, expect, it } from "@jest/globals";
import { ScrcpyControlMessageType } from "../../control/index.js";
import {
ScrcpyFloatToInt16NumberType,
ScrcpyScrollController1_25,
} from "./scroll.js";
describe("ScrcpyFloatToInt16NumberType", () => {
it("should serialize", () => {
const dataView = new DataView(new ArrayBuffer(2));
ScrcpyFloatToInt16NumberType.serialize(dataView, 0, -1, true);
expect(dataView.getInt16(0, true)).toBe(-0x8000);
ScrcpyFloatToInt16NumberType.serialize(dataView, 0, 0, true);
expect(dataView.getInt16(0, true)).toBe(0);
ScrcpyFloatToInt16NumberType.serialize(dataView, 0, 1, true);
expect(dataView.getInt16(0, true)).toBe(0x7fff);
});
it("should clamp input values", () => {
const dataView = new DataView(new ArrayBuffer(2));
ScrcpyFloatToInt16NumberType.serialize(dataView, 0, -2, true);
expect(dataView.getInt16(0, true)).toBe(-0x8000);
ScrcpyFloatToInt16NumberType.serialize(dataView, 0, 2, true);
expect(dataView.getInt16(0, true)).toBe(0x7fff);
});
it("should deserialize", () => {
const dataView = new DataView(new ArrayBuffer(2));
const view = new Uint8Array(dataView.buffer);
dataView.setInt16(0, -0x8000, true);
expect(ScrcpyFloatToInt16NumberType.deserialize(view, true)).toBe(-1);
dataView.setInt16(0, 0, true);
expect(ScrcpyFloatToInt16NumberType.deserialize(view, true)).toBe(0);
dataView.setInt16(0, 0x7fff, true);
expect(ScrcpyFloatToInt16NumberType.deserialize(view, true)).toBe(1);
});
});
describe("ScrcpyScrollController1_25", () => {
it("should return a message for each scroll event", () => {
const controller = new ScrcpyScrollController1_25();
const message1 = controller.serializeScrollMessage({
type: ScrcpyControlMessageType.InjectScroll,
pointerX: 0,
pointerY: 0,
screenWidth: 0,
screenHeight: 0,
scrollX: 0.5,
scrollY: 0.5,
buttons: 0,
});
expect(message1).toBeInstanceOf(Uint8Array);
expect(message1).toHaveProperty("byteLength", 21);
const message2 = controller.serializeScrollMessage({
type: ScrcpyControlMessageType.InjectScroll,
pointerX: 0,
pointerY: 0,
screenWidth: 0,
screenHeight: 0,
scrollX: 1.5,
scrollY: 1.5,
buttons: 0,
});
expect(message2).toBeInstanceOf(Uint8Array);
expect(message2).toHaveProperty("byteLength", 21);
});
});

View file

@ -10,17 +10,18 @@ import {
} from "../../control/index.js";
import { type ScrcpyScrollController } from "../1_16/index.js";
const Int16Max = (1 << 15) - 1;
const ScrcpyFloatToInt16NumberType: NumberFieldType = {
export const ScrcpyFloatToInt16NumberType: NumberFieldType = {
size: 2,
signed: true,
deserialize(array, littleEndian) {
const value = NumberFieldType.Int16.deserialize(array, littleEndian);
return value / Int16Max;
// https://github.com/Genymobile/scrcpy/blob/1f138aef41de651668043b32c4effc2d4adbfc44/server/src/main/java/com/genymobile/scrcpy/Binary.java#L34
return value === 0x7fff ? 1 : value / 0x8000;
},
serialize(dataView, offset, value, littleEndian) {
value = clamp(value, -1, 1) * Int16Max;
// https://github.com/Genymobile/scrcpy/blob/1f138aef41de651668043b32c4effc2d4adbfc44/app/src/util/binary.h#L65
value = clamp(value, -1, 1);
value = value === 1 ? 0x7fff : value * 0x8000;
NumberFieldType.Int16.serialize(dataView, offset, value, littleEndian);
},
};

View file

@ -14,7 +14,7 @@ function testEndian(
max: number,
littleEndian: boolean
) {
test("min", () => {
test(`min = ${min}`, () => {
const buffer = new ArrayBuffer(type.size);
const view = new DataView(buffer);
(
@ -43,7 +43,7 @@ function testEndian(
expect(output).toBe(input);
});
test("max", () => {
test(`max = ${max}`, () => {
const buffer = new ArrayBuffer(type.size);
const view = new DataView(buffer);
(
@ -61,41 +61,30 @@ function testEndian(
function testDeserialize(type: NumberFieldType) {
if (type.size === 1) {
if (type.signed) {
testEndian(
type,
2 ** (type.size * 8) / -2,
2 ** (type.size * 8) / 2 - 1,
false
);
const MIN = -(2 ** (type.size * 8 - 1));
const MAX = -MIN - 1;
testEndian(type, MIN, MAX, false);
} else {
testEndian(type, 0, 2 ** (type.size * 8) - 1, false);
const MAX = 2 ** (type.size * 8) - 1;
testEndian(type, 0, MAX, false);
}
} else {
if (type.signed) {
const MIN = -(2 ** (type.size * 8 - 1));
const MAX = -MIN - 1;
describe("big endian", () => {
testEndian(
type,
2 ** (type.size * 8) / -2,
2 ** (type.size * 8) / 2 - 1,
false
);
testEndian(type, MIN, MAX, false);
});
describe("little endian", () => {
testEndian(
type,
2 ** (type.size * 8) / -2,
2 ** (type.size * 8) / 2 - 1,
true
);
testEndian(type, MIN, MAX, true);
});
} else {
const MAX = 2 ** (type.size * 8) - 1;
describe("big endian", () => {
testEndian(type, 0, 2 ** (type.size * 8) - 1, false);
testEndian(type, 0, MAX, false);
});
describe("little endian", () => {
testEndian(type, 0, 2 ** (type.size * 8) - 1, true);
testEndian(type, 0, MAX, true);
});
}
}

View file

@ -55,15 +55,32 @@ export function placeholder<T>(): T {
// This library can't use `@types/node` or `lib: dom`
// because they will pollute the global scope
// So `TextEncoder` and `TextDecoder` are not available
// So `TextEncoder` and `TextDecoder` types are not available
// Node.js 8.3 ships `TextEncoder` and `TextDecoder` in `util` module.
// But using top level await to load them requires Node.js 14.1.
// So there is no point to do that. Let's just assume they exist in global.
// @ts-expect-error See reason above
declare class TextEncoderType {
constructor();
encode(input: string): Uint8Array;
}
declare class TextDecoderType {
constructor();
decode(buffer: ArrayBufferView | ArrayBuffer): string;
}
interface GlobalExtension {
TextEncoder: typeof TextEncoderType;
TextDecoder: typeof TextDecoderType;
}
const { TextEncoder, TextDecoder } = globalThis as unknown as GlobalExtension;
const Utf8Encoder = new TextEncoder();
// @ts-expect-error See reason above
const Utf8Decoder = new TextDecoder();
export function encodeUtf8(input: string): Uint8Array {