feat(scrcpy): support server version 3.2

This commit is contained in:
Simon Chan 2025-04-09 18:18:54 +08:00
parent ab23e4baa3
commit 418971cdbd
No known key found for this signature in database
GPG key ID: A8B69F750B9BCEDD
12 changed files with 296 additions and 25 deletions

View file

@ -0,0 +1,50 @@
import type { Adb, AdbNoneProtocolSpawner } from "@yume-chan/adb";
import type { ScrcpyDisplay, ScrcpyEncoder } from "@yume-chan/scrcpy";
import { ScrcpyOptions3_2 } from "@yume-chan/scrcpy";
import {
createConnection,
getDisplays,
getEncoders,
} from "./2_1/impl/index.js";
import type { AdbScrcpyClientOptions } from "./client-options.js";
import type { AdbScrcpyConnection } from "./connection.js";
import type { AdbScrcpyOptions, AdbScrcpyOptionsGetEncoders } from "./types.js";
export class AdbScrcpyOptions3_2<TVideo extends boolean>
extends ScrcpyOptions3_2<TVideo>
implements
AdbScrcpyOptions<ScrcpyOptions3_2.Init<TVideo>>,
AdbScrcpyOptionsGetEncoders
{
readonly version: string;
readonly spawner: AdbNoneProtocolSpawner | undefined;
constructor(
init: ScrcpyOptions3_2.Init<TVideo>,
clientOptions?: AdbScrcpyClientOptions,
) {
super(init);
this.version = clientOptions?.version ?? "3.2";
this.spawner = clientOptions?.spawner;
}
getEncoders(adb: Adb, path: string): Promise<ScrcpyEncoder[]> {
return getEncoders(adb, path, this);
}
getDisplays(adb: Adb, path: string): Promise<ScrcpyDisplay[]> {
return getDisplays(adb, path, this);
}
createConnection(adb: Adb): AdbScrcpyConnection {
return createConnection(adb, this.value);
}
}
export namespace AdbScrcpyOptions3_2 {
export type Init<TVideo extends boolean = boolean> =
ScrcpyOptions3_2.Init<TVideo>;
}

View file

@ -22,6 +22,7 @@ export * from "./3_0.js";
export * from "./3_0_1.js";
export * from "./3_0_2.js";
export * from "./3_1.js";
export * from "./3_2.js";
export * from "./client-options.js";
export * from "./client.js";
export * from "./connection.js";

View file

@ -1,11 +1,11 @@
import { AdbScrcpyOptions3_1 } from "./3_1.js";
import { AdbScrcpyOptions3_2 } from "./3_2.js";
import type { AdbScrcpyClientOptions } from "./client-options.js";
export class AdbScrcpyOptionsLatest<
TVideo extends boolean,
> extends AdbScrcpyOptions3_1<TVideo> {
> extends AdbScrcpyOptions3_2<TVideo> {
constructor(
init: AdbScrcpyOptions3_1.Init<TVideo>,
init: AdbScrcpyOptions3_2.Init<TVideo>,
clientOptions?: AdbScrcpyClientOptions,
) {
super(init, clientOptions);
@ -14,5 +14,5 @@ export class AdbScrcpyOptionsLatest<
export namespace AdbScrcpyOptionsLatest {
export type Init<TVideo extends boolean = boolean> =
AdbScrcpyOptions3_1.Init<TVideo>;
AdbScrcpyOptions3_2.Init<TVideo>;
}

View file

@ -1,19 +1,18 @@
import type { Init } from "./init.js";
import { VideoOrientation } from "./init.js";
export const Defaults = /* #__PURE__ */ (() =>
({
logLevel: "debug",
maxSize: 0,
bitRate: 8_000_000,
maxFps: 0,
lockVideoOrientation: VideoOrientation.Unlocked,
tunnelForward: false,
crop: undefined,
sendFrameMeta: true,
control: true,
displayId: 0,
showTouches: false,
stayAwake: false,
codecOptions: undefined,
}) as const satisfies Required<Init>)();
export const Defaults = {
logLevel: "debug",
maxSize: 0,
bitRate: 8_000_000,
maxFps: 0,
lockVideoOrientation: VideoOrientation.Unlocked,
tunnelForward: false,
crop: undefined,
sendFrameMeta: true,
control: true,
displayId: 0,
showTouches: false,
stayAwake: false,
codecOptions: undefined,
} as const satisfies Required<Init>;

View file

@ -0,0 +1,8 @@
import type { Init } from "./init.js";
import { PrevImpl } from "./prev.js";
export const Defaults = /* #__PURE__ */ (() =>
({
...PrevImpl.Defaults,
displayImePolicy: undefined,
}) as const satisfies Required<Init<true>>)();

View file

@ -0,0 +1,3 @@
export * from "../../3_1/impl/index.js";
export { Defaults } from "./defaults.js";
export type { Init } from "./init.js";

View file

@ -0,0 +1,17 @@
import type { PrevImpl } from "./prev.js";
export interface Init<TVideo extends boolean>
extends Omit<PrevImpl.Init<TVideo>, "audioSource"> {
audioSource?:
| PrevImpl.Init<TVideo>["audioSource"]
| "mic-unprocessed"
| "mic-camcorder"
| "mic-voice-recognition"
| "mic-voice-communication"
| "voice-call"
| "voice-call-uplink"
| "voice-call-downlink"
| "voice-performance";
displayImePolicy?: "local" | "fallback" | "hide" | undefined;
}

View file

@ -0,0 +1 @@
export * as PrevImpl from "../../3_1/impl/index.js";

View file

@ -0,0 +1 @@
export * from "./options.js";

View file

@ -0,0 +1,190 @@
import type { MaybePromiseLike } from "@yume-chan/async";
import type { ReadableStream, TransformStream } from "@yume-chan/stream-extra";
import type { AsyncExactReadable, ExactReadable } from "@yume-chan/struct";
import type {
ScrcpyAudioStreamMetadata,
ScrcpyControlMessageType,
ScrcpyDisplay,
ScrcpyEncoder,
ScrcpyMediaStreamPacket,
ScrcpyOptions,
ScrcpyOptionsListEncoders,
ScrcpyScrollController,
ScrcpyVideoStream,
} from "../base/index.js";
import type {
ScrcpyBackOrScreenOnControlMessage,
ScrcpyInjectTouchControlMessage,
ScrcpySetClipboardControlMessage,
ScrcpyUHidCreateControlMessage,
ScrcpyUHidOutputDeviceMessage,
} from "../latest.js";
import type { Init } from "./impl/index.js";
import {
AckClipboardHandler,
ClipboardStream,
ControlMessageTypes,
createMediaStreamTransformer,
createScrollController,
Defaults,
parseAudioStreamMetadata,
parseDisplay,
parseEncoder,
parseVideoStreamMetadata,
serialize,
serializeBackOrScreenOnControlMessage,
serializeInjectTouchControlMessage,
serializeUHidCreateControlMessage,
setListDisplays,
setListEncoders,
UHidOutputStream,
} from "./impl/index.js";
export class ScrcpyOptions3_2<TVideo extends boolean>
implements ScrcpyOptions<Init<TVideo>>, ScrcpyOptionsListEncoders
{
static readonly Defaults = Defaults;
readonly value: Required<Init<TVideo>>;
get controlMessageTypes(): readonly ScrcpyControlMessageType[] {
return ControlMessageTypes;
}
#clipboard: ClipboardStream | undefined;
get clipboard(): ReadableStream<string> | undefined {
return this.#clipboard;
}
#ackClipboardHandler: AckClipboardHandler | undefined;
#uHidOutput: UHidOutputStream | undefined;
get uHidOutput():
| ReadableStream<ScrcpyUHidOutputDeviceMessage>
| undefined {
return this.#uHidOutput;
}
constructor(init: Init<TVideo>) {
this.value = { ...Defaults, ...init } as never;
if (this.value.videoSource === "camera") {
this.value.control = false;
}
if (this.value.audioDup) {
this.value.audioSource = "playback";
}
if (this.value.control) {
if (this.value.clipboardAutosync) {
this.#clipboard = new ClipboardStream();
this.#ackClipboardHandler = new AckClipboardHandler();
}
this.#uHidOutput = new UHidOutputStream();
}
}
serialize(): string[] {
return serialize<Init<boolean>>(this.value, Defaults);
}
setListDisplays(): void {
setListDisplays(this.value);
}
parseDisplay(line: string): ScrcpyDisplay | undefined {
return parseDisplay(line);
}
setListEncoders() {
setListEncoders(this.value);
}
parseEncoder(line: string): ScrcpyEncoder | undefined {
return parseEncoder(line);
}
parseVideoStreamMetadata(
stream: ReadableStream<Uint8Array>,
): MaybePromiseLike<ScrcpyVideoStream> {
return parseVideoStreamMetadata(this.value, stream);
}
parseAudioStreamMetadata(
stream: ReadableStream<Uint8Array>,
): MaybePromiseLike<ScrcpyAudioStreamMetadata> {
return parseAudioStreamMetadata(stream, this.value);
}
async parseDeviceMessage(
id: number,
stream: ExactReadable | AsyncExactReadable,
): Promise<void> {
if (await this.#clipboard?.parse(id, stream)) {
return;
}
if (await this.#ackClipboardHandler?.parse(id, stream)) {
return;
}
throw new Error("Unknown device message");
}
endDeviceMessageStream(e?: unknown): void {
if (e) {
this.#clipboard?.error(e);
this.#ackClipboardHandler?.error(e);
} else {
this.#clipboard?.close();
this.#ackClipboardHandler?.close();
}
}
createMediaStreamTransformer(): TransformStream<
Uint8Array,
ScrcpyMediaStreamPacket
> {
return createMediaStreamTransformer(this.value);
}
serializeInjectTouchControlMessage(
message: ScrcpyInjectTouchControlMessage,
): Uint8Array {
return serializeInjectTouchControlMessage(message);
}
serializeBackOrScreenOnControlMessage(
message: ScrcpyBackOrScreenOnControlMessage,
): Uint8Array | undefined {
return serializeBackOrScreenOnControlMessage(message);
}
serializeSetClipboardControlMessage(
message: ScrcpySetClipboardControlMessage,
): Uint8Array | [Uint8Array, Promise<void>] {
return this.#ackClipboardHandler!.serializeSetClipboardControlMessage(
message,
);
}
createScrollController(): ScrcpyScrollController {
return createScrollController();
}
serializeUHidCreateControlMessage(
message: ScrcpyUHidCreateControlMessage,
): Uint8Array {
return serializeUHidCreateControlMessage(message);
}
}
type Init_<TVideo extends boolean> = Init<TVideo>;
export namespace ScrcpyOptions3_2 {
export type Init<TVideo extends boolean = boolean> = Init_<TVideo>;
}

View file

@ -25,6 +25,7 @@ export * from "./3_0/index.js";
export * from "./3_0_1.js";
export * from "./3_0_2.js";
export * from "./3_1/index.js";
export * from "./3_2/index.js";
export * from "./android/index.js";
export * from "./base/index.js";
export * from "./codec/index.js";

View file

@ -1,16 +1,16 @@
import { ScrcpyOptions3_1 } from "./3_1/options.js";
import { ScrcpyOptions3_2 } from "./3_2/options.js";
export class ScrcpyOptionsLatest<
TVideo extends boolean,
> extends ScrcpyOptions3_1<TVideo> {
constructor(init: ScrcpyOptions3_1.Init<TVideo>) {
> extends ScrcpyOptions3_2<TVideo> {
constructor(init: ScrcpyOptions3_2.Init<TVideo>) {
super(init);
}
}
export namespace ScrcpyOptionsLatest {
export type Init<TVideo extends boolean = boolean> =
ScrcpyOptions3_1.Init<TVideo>;
ScrcpyOptions3_2.Init<TVideo>;
}
export {
@ -28,4 +28,4 @@ export {
SetClipboardControlMessage as ScrcpySetClipboardControlMessage,
UHidCreateControlMessage as ScrcpyUHidCreateControlMessage,
UHidOutputDeviceMessage as ScrcpyUHidOutputDeviceMessage,
} from "./3_1/impl/index.js";
} from "./3_2/impl/index.js";