mirror of
https://github.com/yume-chan/ya-webadb.git
synced 2025-10-05 02:39:26 +02:00
refactor(scrcpy): small optimizations
This commit is contained in:
parent
6b23154694
commit
b5f58227fd
11 changed files with 359 additions and 71 deletions
|
@ -5,16 +5,21 @@ import {
|
||||||
import { ScrcpyControlMessageType } from "./type.js";
|
import { ScrcpyControlMessageType } from "./type.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* On Android, touching the screen with a finger will disable mouse cursor.
|
* On both Android and Windows, while both mouse and touch are supported input devices,
|
||||||
* However, Scrcpy doesn't do that, and can inject two pointers at the same time.
|
* only one of them can be active at a time. Touch the screen with a finger will deactivate mouse,
|
||||||
* This can cause finger events to be "ignored" because mouse is still the primary pointer.
|
* 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.
|
* so Scrcpy server can remove the previously hovering pointer.
|
||||||
*/
|
*/
|
||||||
export class ScrcpyHoverHelper {
|
export class ScrcpyHoverHelper {
|
||||||
// AFAIK, only mouse and pen can have hover state
|
// AFAIK, only mouse and pen can have hover state
|
||||||
// and you can't have two mouses or pens.
|
// and you can't have two mouses or pens.
|
||||||
|
// So remember the last hovering pointer is enough.
|
||||||
private lastHoverMessage: ScrcpyInjectTouchControlMessage | undefined;
|
private lastHoverMessage: ScrcpyInjectTouchControlMessage | undefined;
|
||||||
|
|
||||||
public process(
|
public process(
|
||||||
|
|
|
@ -40,7 +40,7 @@ export class ScrcpyControlMessageSerializer {
|
||||||
this.scrollController = options.getScrollController();
|
this.scrollController = options.getScrollController();
|
||||||
}
|
}
|
||||||
|
|
||||||
public getTypeValue(type: ScrcpyControlMessageType): number {
|
public getActualMessageType(type: ScrcpyControlMessageType): number {
|
||||||
const value = this.types.indexOf(type);
|
const value = this.types.indexOf(type);
|
||||||
if (value === -1) {
|
if (value === -1) {
|
||||||
throw new Error("Not supported");
|
throw new Error("Not supported");
|
||||||
|
@ -48,14 +48,24 @@ export class ScrcpyControlMessageSerializer {
|
||||||
return value;
|
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(
|
public injectKeyCode(
|
||||||
message: Omit<ScrcpyInjectKeyCodeControlMessage, "type">
|
message: Omit<ScrcpyInjectKeyCodeControlMessage, "type">
|
||||||
) {
|
) {
|
||||||
return this.writer.write(
|
return this.writer.write(
|
||||||
ScrcpyInjectKeyCodeControlMessage.serialize({
|
ScrcpyInjectKeyCodeControlMessage.serialize(
|
||||||
...message,
|
this.addMessageType(
|
||||||
type: this.getTypeValue(ScrcpyControlMessageType.InjectKeyCode),
|
message,
|
||||||
})
|
ScrcpyControlMessageType.InjectKeyCode
|
||||||
|
)
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -63,7 +73,9 @@ export class ScrcpyControlMessageSerializer {
|
||||||
return this.writer.write(
|
return this.writer.write(
|
||||||
ScrcpyInjectTextControlMessage.serialize({
|
ScrcpyInjectTextControlMessage.serialize({
|
||||||
text,
|
text,
|
||||||
type: this.getTypeValue(ScrcpyControlMessageType.InjectText),
|
type: this.getActualMessageType(
|
||||||
|
ScrcpyControlMessageType.InjectText
|
||||||
|
),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -73,10 +85,12 @@ export class ScrcpyControlMessageSerializer {
|
||||||
*/
|
*/
|
||||||
public injectTouch(message: Omit<ScrcpyInjectTouchControlMessage, "type">) {
|
public injectTouch(message: Omit<ScrcpyInjectTouchControlMessage, "type">) {
|
||||||
return this.writer.write(
|
return this.writer.write(
|
||||||
ScrcpyInjectTouchControlMessage.serialize({
|
ScrcpyInjectTouchControlMessage.serialize(
|
||||||
...message,
|
this.addMessageType(
|
||||||
type: this.getTypeValue(ScrcpyControlMessageType.InjectTouch),
|
message,
|
||||||
})
|
ScrcpyControlMessageType.InjectTouch
|
||||||
|
)
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -86,13 +100,10 @@ export class ScrcpyControlMessageSerializer {
|
||||||
public injectScroll(
|
public injectScroll(
|
||||||
message: Omit<ScrcpyInjectScrollControlMessage, "type">
|
message: Omit<ScrcpyInjectScrollControlMessage, "type">
|
||||||
) {
|
) {
|
||||||
(message as ScrcpyInjectScrollControlMessage).type = this.getTypeValue(
|
const data = this.scrollController.serializeScrollMessage(
|
||||||
ScrcpyControlMessageType.InjectScroll
|
this.addMessageType(message, ScrcpyControlMessageType.InjectScroll)
|
||||||
);
|
);
|
||||||
|
|
||||||
const data = this.scrollController.serializeScrollMessage(
|
|
||||||
message as ScrcpyInjectScrollControlMessage
|
|
||||||
);
|
|
||||||
if (!data) {
|
if (!data) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -103,7 +114,9 @@ export class ScrcpyControlMessageSerializer {
|
||||||
public async backOrScreenOn(action: AndroidKeyEventAction) {
|
public async backOrScreenOn(action: AndroidKeyEventAction) {
|
||||||
const buffer = this.options.serializeBackOrScreenOnControlMessage({
|
const buffer = this.options.serializeBackOrScreenOnControlMessage({
|
||||||
action,
|
action,
|
||||||
type: this.getTypeValue(ScrcpyControlMessageType.BackOrScreenOn),
|
type: this.getActualMessageType(
|
||||||
|
ScrcpyControlMessageType.BackOrScreenOn
|
||||||
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (buffer) {
|
if (buffer) {
|
||||||
|
@ -115,7 +128,7 @@ export class ScrcpyControlMessageSerializer {
|
||||||
return this.writer.write(
|
return this.writer.write(
|
||||||
ScrcpySetScreenPowerModeControlMessage.serialize({
|
ScrcpySetScreenPowerModeControlMessage.serialize({
|
||||||
mode,
|
mode,
|
||||||
type: this.getTypeValue(
|
type: this.getActualMessageType(
|
||||||
ScrcpyControlMessageType.SetScreenPowerMode
|
ScrcpyControlMessageType.SetScreenPowerMode
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
|
@ -125,7 +138,9 @@ export class ScrcpyControlMessageSerializer {
|
||||||
public rotateDevice() {
|
public rotateDevice() {
|
||||||
return this.writer.write(
|
return this.writer.write(
|
||||||
ScrcpyRotateDeviceControlMessage.serialize({
|
ScrcpyRotateDeviceControlMessage.serialize({
|
||||||
type: this.getTypeValue(ScrcpyControlMessageType.RotateDevice),
|
type: this.getActualMessageType(
|
||||||
|
ScrcpyControlMessageType.RotateDevice
|
||||||
|
),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -55,8 +55,8 @@ class BitReader {
|
||||||
* Split NAL units from a H.264 Annex B stream.
|
* Split NAL units from a H.264 Annex B stream.
|
||||||
*
|
*
|
||||||
* The input is not modified.
|
* The input is not modified.
|
||||||
* The returned NAL units are views of the input (no memory allocation and copy),
|
* The returned NAL units are views of the input (no memory allocation nor copy),
|
||||||
* but still contains emulation prevention bytes.
|
* and still contains emulation prevention bytes.
|
||||||
*
|
*
|
||||||
* This methods returns a generator, so it can be stopped immediately
|
* This methods returns a generator, so it can be stopped immediately
|
||||||
* after the interested NAL unit is found.
|
* after the interested NAL unit is found.
|
||||||
|
@ -160,13 +160,66 @@ export function removeH264Emulation(buffer: Uint8Array) {
|
||||||
let zeroCount = 0;
|
let zeroCount = 0;
|
||||||
let inEmulation = false;
|
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]!;
|
const byte = buffer[i]!;
|
||||||
|
|
||||||
if (output) {
|
|
||||||
output[outputOffset] = byte;
|
output[outputOffset] = byte;
|
||||||
outputOffset += 1;
|
outputOffset += 1;
|
||||||
}
|
|
||||||
|
|
||||||
if (inEmulation) {
|
if (inEmulation) {
|
||||||
if (byte > 0x03) {
|
if (byte > 0x03) {
|
||||||
|
@ -211,15 +264,8 @@ export function removeH264Emulation(buffer: Uint8Array) {
|
||||||
// `0x000000`, `0x000001`, `0x000002` and `0x000003` respectively
|
// `0x000000`, `0x000001`, `0x000002` and `0x000003` respectively
|
||||||
inEmulation = true;
|
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
|
// Remove the emulation prevention byte
|
||||||
outputOffset -= 1;
|
outputOffset -= 1;
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
// `0x000004` or larger are as-is
|
// `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
|
// 7.3.2.1.1 Sequence parameter set data syntax
|
||||||
|
|
90
libraries/scrcpy/src/options/1_16/scroll.spec.ts
Normal file
90
libraries/scrcpy/src/options/1_16/scroll.spec.ts
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
13
libraries/scrcpy/src/options/1_22/options.spec.ts
Normal file
13
libraries/scrcpy/src/options/1_22/options.spec.ts
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
23
libraries/scrcpy/src/options/1_22/scroll.spec.ts
Normal file
23
libraries/scrcpy/src/options/1_22/scroll.spec.ts
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
13
libraries/scrcpy/src/options/1_25/options.spec.ts
Normal file
13
libraries/scrcpy/src/options/1_25/options.spec.ts
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
76
libraries/scrcpy/src/options/1_25/scroll.spec.ts
Normal file
76
libraries/scrcpy/src/options/1_25/scroll.spec.ts
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
|
@ -10,17 +10,18 @@ import {
|
||||||
} from "../../control/index.js";
|
} from "../../control/index.js";
|
||||||
import { type ScrcpyScrollController } from "../1_16/index.js";
|
import { type ScrcpyScrollController } from "../1_16/index.js";
|
||||||
|
|
||||||
const Int16Max = (1 << 15) - 1;
|
export const ScrcpyFloatToInt16NumberType: NumberFieldType = {
|
||||||
|
|
||||||
const ScrcpyFloatToInt16NumberType: NumberFieldType = {
|
|
||||||
size: 2,
|
size: 2,
|
||||||
signed: true,
|
signed: true,
|
||||||
deserialize(array, littleEndian) {
|
deserialize(array, littleEndian) {
|
||||||
const value = NumberFieldType.Int16.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) {
|
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);
|
NumberFieldType.Int16.serialize(dataView, offset, value, littleEndian);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -14,7 +14,7 @@ function testEndian(
|
||||||
max: number,
|
max: number,
|
||||||
littleEndian: boolean
|
littleEndian: boolean
|
||||||
) {
|
) {
|
||||||
test("min", () => {
|
test(`min = ${min}`, () => {
|
||||||
const buffer = new ArrayBuffer(type.size);
|
const buffer = new ArrayBuffer(type.size);
|
||||||
const view = new DataView(buffer);
|
const view = new DataView(buffer);
|
||||||
(
|
(
|
||||||
|
@ -43,7 +43,7 @@ function testEndian(
|
||||||
expect(output).toBe(input);
|
expect(output).toBe(input);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("max", () => {
|
test(`max = ${max}`, () => {
|
||||||
const buffer = new ArrayBuffer(type.size);
|
const buffer = new ArrayBuffer(type.size);
|
||||||
const view = new DataView(buffer);
|
const view = new DataView(buffer);
|
||||||
(
|
(
|
||||||
|
@ -61,41 +61,30 @@ function testEndian(
|
||||||
function testDeserialize(type: NumberFieldType) {
|
function testDeserialize(type: NumberFieldType) {
|
||||||
if (type.size === 1) {
|
if (type.size === 1) {
|
||||||
if (type.signed) {
|
if (type.signed) {
|
||||||
testEndian(
|
const MIN = -(2 ** (type.size * 8 - 1));
|
||||||
type,
|
const MAX = -MIN - 1;
|
||||||
2 ** (type.size * 8) / -2,
|
testEndian(type, MIN, MAX, false);
|
||||||
2 ** (type.size * 8) / 2 - 1,
|
|
||||||
false
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
testEndian(type, 0, 2 ** (type.size * 8) - 1, false);
|
const MAX = 2 ** (type.size * 8) - 1;
|
||||||
|
testEndian(type, 0, MAX, false);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (type.signed) {
|
if (type.signed) {
|
||||||
|
const MIN = -(2 ** (type.size * 8 - 1));
|
||||||
|
const MAX = -MIN - 1;
|
||||||
describe("big endian", () => {
|
describe("big endian", () => {
|
||||||
testEndian(
|
testEndian(type, MIN, MAX, false);
|
||||||
type,
|
|
||||||
2 ** (type.size * 8) / -2,
|
|
||||||
2 ** (type.size * 8) / 2 - 1,
|
|
||||||
false
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("little endian", () => {
|
describe("little endian", () => {
|
||||||
testEndian(
|
testEndian(type, MIN, MAX, true);
|
||||||
type,
|
|
||||||
2 ** (type.size * 8) / -2,
|
|
||||||
2 ** (type.size * 8) / 2 - 1,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
const MAX = 2 ** (type.size * 8) - 1;
|
||||||
describe("big endian", () => {
|
describe("big endian", () => {
|
||||||
testEndian(type, 0, 2 ** (type.size * 8) - 1, false);
|
testEndian(type, 0, MAX, false);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("little endian", () => {
|
describe("little endian", () => {
|
||||||
testEndian(type, 0, 2 ** (type.size * 8) - 1, true);
|
testEndian(type, 0, MAX, true);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -55,15 +55,32 @@ export function placeholder<T>(): T {
|
||||||
|
|
||||||
// This library can't use `@types/node` or `lib: dom`
|
// This library can't use `@types/node` or `lib: dom`
|
||||||
// because they will pollute the global scope
|
// 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.
|
// Node.js 8.3 ships `TextEncoder` and `TextDecoder` in `util` module.
|
||||||
// But using top level await to load them requires Node.js 14.1.
|
// 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.
|
// 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();
|
const Utf8Encoder = new TextEncoder();
|
||||||
// @ts-expect-error See reason above
|
|
||||||
const Utf8Decoder = new TextDecoder();
|
const Utf8Decoder = new TextDecoder();
|
||||||
|
|
||||||
export function encodeUtf8(input: string): Uint8Array {
|
export function encodeUtf8(input: string): Uint8Array {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue