diff --git a/apps/demo/pages/scrcpy.tsx b/apps/demo/pages/scrcpy.tsx index 244f5578..26592e61 100644 --- a/apps/demo/pages/scrcpy.tsx +++ b/apps/demo/pages/scrcpy.tsx @@ -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, diff --git a/libraries/scrcpy/scripts/fetch-server.cjs b/libraries/scrcpy/scripts/fetch-server.cjs old mode 100644 new mode 100755 diff --git a/libraries/scrcpy/src/client.ts b/libraries/scrcpy/src/client.ts index 0af007e8..03012556 100644 --- a/libraries/scrcpy/src/client.ts +++ b/libraries/scrcpy/src/client.ts @@ -25,11 +25,6 @@ function* splitLines(text: string): Generator { } } -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) { - if (!this.controlStream) { - throw new Error('injectKeyCode called before initialization'); + private checkControlStream(caller: string) { + if (!this._running) { + throw new Error(`${caller} called before start`); } - await this.controlStream.write(ScrcpyInjectKeyCodeControlMessage.serialize({ + if (!this.controlStream) { + throw new Error(`${caller} called with control disabled`); + } + + return this.controlStream; + } + + public async injectKeyCode(message: Omit) { + 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) { - 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) { - 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(); } } diff --git a/libraries/scrcpy/src/connection.ts b/libraries/scrcpy/src/connection.ts index c1058d01..def8a1c9 100644 --- a/libraries/scrcpy/src/connection.ts +++ b/libraries/scrcpy/src/connection.ts @@ -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 { } - 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 { + private async connectVideoStream(): Promise { const stream = await this.connectAndRetry(); - // server will write a `0` to signal connection success - await stream.read(1); + 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() { diff --git a/libraries/scrcpy/src/options/1_16.ts b/libraries/scrcpy/src/options/1_16.ts index e36eab2a..6849939a 100644 --- a/libraries/scrcpy/src/options/1_16.ts +++ b/libraries/scrcpy/src/options/1_16.ts @@ -1,7 +1,7 @@ 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 extends ScrcpyOptions1_21 { public constructor(init: Partial) { + if (init.rawVideoStream) { + // Set implied options for client-side processing + init.sendDeviceMeta = false; + init.sendFrameMeta = false; + init.sendDummyByte = false; + // TODO: Add support for `sendFrameMeta: false` + throw new Error('`rawVideoStream:true` is not supported'); + } + + if (!init.sendFrameMeta) { + // TODO: Add support for `sendFrameMeta: false` + throw new Error('`sendFrameMeta:false` is not supported'); + } + super(init); } @@ -49,6 +67,20 @@ export class ScrcpyOptions1_22