Loading libraries/scrcpy/src/control/hover-helper.ts +9 −4 Original line number Diff line number Diff line Loading @@ -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( Loading libraries/scrcpy/src/control/serializer.ts +33 −18 Original line number Diff line number Diff line Loading @@ -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"); Loading @@ -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 ) ) ); } Loading @@ -63,7 +73,9 @@ export class ScrcpyControlMessageSerializer { return this.writer.write( ScrcpyInjectTextControlMessage.serialize({ text, type: this.getTypeValue(ScrcpyControlMessageType.InjectText), type: this.getActualMessageType( ScrcpyControlMessageType.InjectText ), }) ); } Loading @@ -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 ) ) ); } Loading @@ -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; } Loading @@ -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) { Loading @@ -115,7 +128,7 @@ export class ScrcpyControlMessageSerializer { return this.writer.write( ScrcpySetScreenPowerModeControlMessage.serialize({ mode, type: this.getTypeValue( type: this.getActualMessageType( ScrcpyControlMessageType.SetScreenPowerMode ), }) Loading @@ -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 ), }) ); } Loading libraries/scrcpy/src/options/1_16/h264-configuration.ts +62 −16 Original line number Diff line number Diff line Loading @@ -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. Loading Loading @@ -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) { Loading Loading @@ -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 Loading @@ -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 Loading libraries/scrcpy/src/options/1_16/scroll.spec.ts 0 → 100644 +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); }); }); libraries/scrcpy/src/options/1_22/options.spec.ts 0 → 100644 +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
libraries/scrcpy/src/control/hover-helper.ts +9 −4 Original line number Diff line number Diff line Loading @@ -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( Loading
libraries/scrcpy/src/control/serializer.ts +33 −18 Original line number Diff line number Diff line Loading @@ -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"); Loading @@ -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 ) ) ); } Loading @@ -63,7 +73,9 @@ export class ScrcpyControlMessageSerializer { return this.writer.write( ScrcpyInjectTextControlMessage.serialize({ text, type: this.getTypeValue(ScrcpyControlMessageType.InjectText), type: this.getActualMessageType( ScrcpyControlMessageType.InjectText ), }) ); } Loading @@ -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 ) ) ); } Loading @@ -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; } Loading @@ -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) { Loading @@ -115,7 +128,7 @@ export class ScrcpyControlMessageSerializer { return this.writer.write( ScrcpySetScreenPowerModeControlMessage.serialize({ mode, type: this.getTypeValue( type: this.getActualMessageType( ScrcpyControlMessageType.SetScreenPowerMode ), }) Loading @@ -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 ), }) ); } Loading
libraries/scrcpy/src/options/1_16/h264-configuration.ts +62 −16 Original line number Diff line number Diff line Loading @@ -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. Loading Loading @@ -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) { Loading Loading @@ -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 Loading @@ -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 Loading
libraries/scrcpy/src/options/1_16/scroll.spec.ts 0 → 100644 +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); }); });
libraries/scrcpy/src/options/1_22/options.spec.ts 0 → 100644 +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); }); });