refactor(scrcpy): rewrite option classes to improve tree-shaking

This commit is contained in:
Simon Chan 2024-11-25 18:10:15 +08:00
parent db8466f6ee
commit 92472007db
No known key found for this signature in database
GPG key ID: A8B69F750B9BCEDD
218 changed files with 5412 additions and 2380 deletions

View file

@ -10,12 +10,12 @@ import type {
ScrcpyDisplay,
ScrcpyEncoder,
ScrcpyMediaStreamPacket,
ScrcpyOptionsInit1_16,
ScrcpyOptions1_15,
ScrcpyVideoStreamMetadata,
} from "@yume-chan/scrcpy";
import {
Av1,
DEFAULT_SERVER_PATH,
DefaultServerPath,
ScrcpyControlMessageWriter,
ScrcpyVideoCodecId,
h264ParseConfiguration,
@ -104,7 +104,7 @@ export class AdbScrcpyClient {
static async pushServer(
adb: Adb,
file: ReadableStream<MaybeConsumable<Uint8Array>>,
filename = DEFAULT_SERVER_PATH,
filename = DefaultServerPath,
) {
const sync = await adb.sync();
try {
@ -121,7 +121,9 @@ export class AdbScrcpyClient {
adb: Adb,
path: string,
version: string,
options: AdbScrcpyOptions<Pick<ScrcpyOptionsInit1_16, "tunnelForward">>,
options: AdbScrcpyOptions<
Pick<ScrcpyOptions1_15.Init, "tunnelForward">
>,
) {
let connection: AdbScrcpyConnection | undefined;
let process: AdbSubprocessProtocol | undefined;
@ -342,7 +344,7 @@ export class AdbScrcpyClient {
type = result[0]!;
} catch (e) {
if (e instanceof ExactReadableEndedError) {
await this.#options.endDeviceMessageStream();
this.#options.endDeviceMessageStream();
break;
}
throw e;
@ -350,7 +352,7 @@ export class AdbScrcpyClient {
await this.#options.parseDeviceMessage(type, buffered);
}
} catch (e) {
await this.#options.endDeviceMessageStream(e);
this.#options.endDeviceMessageStream(e);
buffered.cancel(e).catch(() => {});
}
}

View file

@ -2,7 +2,7 @@ import type { Adb } from "@yume-chan/adb";
import type {
ScrcpyDisplay,
ScrcpyEncoder,
ScrcpyOptionsInit1_16,
ScrcpyOptions1_16Impl,
} from "@yume-chan/scrcpy";
import { WritableStream } from "@yume-chan/stream-extra";
@ -21,7 +21,7 @@ import { AdbScrcpyOptions } from "./types.js";
export class AdbScrcpyOptions1_16 extends AdbScrcpyOptions<
// Only pick options that are used in this class,
// so changes in `ScrcpyOptionsInitX_XX` won't affect type assignability with this class
Pick<ScrcpyOptionsInit1_16, "tunnelForward">
Pick<ScrcpyOptions1_16Impl.Init, "tunnelForward">
> {
static createConnection(
adb: Adb,
@ -39,7 +39,9 @@ export class AdbScrcpyOptions1_16 extends AdbScrcpyOptions<
adb: Adb,
path: string,
version: string,
options: AdbScrcpyOptions<Pick<ScrcpyOptionsInit1_16, "tunnelForward">>,
options: AdbScrcpyOptions<
Pick<ScrcpyOptions1_16Impl.Init, "tunnelForward">
>,
): Promise<ScrcpyEncoder[]> {
const client = await AdbScrcpyClient.start(adb, path, version, options);
@ -62,7 +64,9 @@ export class AdbScrcpyOptions1_16 extends AdbScrcpyOptions<
adb: Adb,
path: string,
version: string,
options: AdbScrcpyOptions<Pick<ScrcpyOptionsInit1_16, "tunnelForward">>,
options: AdbScrcpyOptions<
Pick<ScrcpyOptions1_16Impl.Init, "tunnelForward">
>,
): Promise<ScrcpyDisplay[]> {
try {
// Server will exit before opening connections when an invalid display id was given

View file

@ -2,7 +2,7 @@ import type { Adb } from "@yume-chan/adb";
import type {
ScrcpyDisplay,
ScrcpyEncoder,
ScrcpyOptionsInit1_22,
ScrcpyOptions1_22Impl,
} from "@yume-chan/scrcpy";
import type { AdbScrcpyConnection } from "../connection.js";
@ -13,7 +13,10 @@ import { AdbScrcpyOptions } from "./types.js";
export class AdbScrcpyOptions1_22 extends AdbScrcpyOptions<
// Only pick options that are used in this class,
// so changes in `ScrcpyOptionsInitX_XX` won't affect type assignability with this class
Pick<ScrcpyOptionsInit1_22, "tunnelForward" | "control" | "sendDummyByte">
Pick<
ScrcpyOptions1_22Impl.Init,
"tunnelForward" | "control" | "sendDummyByte"
>
> {
override getEncoders(
adb: Adb,

View file

@ -2,8 +2,8 @@ import type { Adb } from "@yume-chan/adb";
import type {
ScrcpyDisplay,
ScrcpyEncoder,
ScrcpyOptionsInit1_16,
ScrcpyOptionsInit2_0,
ScrcpyOptions1_16Impl,
ScrcpyOptions2_0Impl,
} from "@yume-chan/scrcpy";
import { AdbScrcpyClient, AdbScrcpyExitedError } from "../client.js";
@ -16,7 +16,7 @@ export class AdbScrcpyOptions2_0 extends AdbScrcpyOptions<
// Only pick options that are used in this class,
// so changes in `ScrcpyOptionsInitX_XX` won't affect type assignability with this class
Pick<
ScrcpyOptionsInit2_0,
ScrcpyOptions2_0Impl.Init,
"tunnelForward" | "control" | "sendDummyByte" | "scid" | "audio"
>
> {
@ -24,7 +24,9 @@ export class AdbScrcpyOptions2_0 extends AdbScrcpyOptions<
adb: Adb,
path: string,
version: string,
options: AdbScrcpyOptions<Pick<ScrcpyOptionsInit1_16, "tunnelForward">>,
options: AdbScrcpyOptions<
Pick<ScrcpyOptions1_16Impl.Init, "tunnelForward">
>,
): Promise<ScrcpyEncoder[]> {
try {
// Similar to `AdbScrcpyOptions1_16.getDisplays`,

View file

@ -2,7 +2,7 @@ import type { Adb } from "@yume-chan/adb";
import type {
ScrcpyDisplay,
ScrcpyEncoder,
ScrcpyOptionsInit2_1,
ScrcpyOptions2_1Impl,
} from "@yume-chan/scrcpy";
import type { AdbScrcpyConnection } from "../connection.js";
@ -15,7 +15,7 @@ export class AdbScrcpyOptions2_1 extends AdbScrcpyOptions<
// Only pick options that are used in this class,
// so changes in `ScrcpyOptionsInitX_XX` won't affect type assignability with this class
Pick<
ScrcpyOptionsInit2_1,
ScrcpyOptions2_1Impl.Init,
| "tunnelForward"
| "control"
| "sendDummyByte"

View file

@ -1,39 +1,12 @@
import type { Adb } from "@yume-chan/adb";
import type { ScrcpyDisplay, ScrcpyEncoder } from "@yume-chan/scrcpy";
import { ScrcpyOptions } from "@yume-chan/scrcpy";
import { ScrcpyOptionsWrapper } from "@yume-chan/scrcpy";
import type { AdbScrcpyConnection } from "../connection.js";
export abstract class AdbScrcpyOptions<
T extends object,
> extends ScrcpyOptions<T> {
#base: ScrcpyOptions<T>;
override get defaults(): Required<T> {
return this.#base.defaults;
}
constructor(base: ScrcpyOptions<T>) {
super(
// HACK: `ScrcpyOptions`'s constructor requires a constructor for the base class,
// but we need to pass an instance here.
// A normal `function` can be used as a constructor, and constructors can return
// any object to override the default return value.
function () {
return base;
} as never,
// HACK: `base.value` contains `SkipDefaultMark`, so it will be used as is,
// and `defaults` parameter is not used.
base.value,
{} as Required<T>,
);
this.#base = base;
}
serialize(): string[] {
return this.#base.serialize();
}
> extends ScrcpyOptionsWrapper<T> {
abstract getEncoders(
adb: Adb,
path: string,

View file

@ -210,6 +210,8 @@ export class WebCodecsVideoDecoder implements ScrcpyVideoDecoder {
this.#updateSize,
);
break;
default:
throw new Error(`Unsupported codec: ${this.#codec}`);
}
this.#writable = new WritableStream<ScrcpyMediaStreamPacket>({

View file

@ -0,0 +1,19 @@
{
"name": "side-effect-test",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "rollup -c rollup.config.ts --configPlugin @rollup/plugin-typescript"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@rollup/plugin-node-resolve": "^15.3.0",
"@rollup/plugin-terser": "^0.4.4",
"@rollup/plugin-typescript": "^12.1.1",
"rollup": "^4.27.4"
}
}

View file

@ -0,0 +1,24 @@
import node from "@rollup/plugin-node-resolve";
import terser from "@rollup/plugin-terser";
import typescript from "@rollup/plugin-typescript";
import { defineConfig } from "rollup";
export default defineConfig({
input: "src/index.js",
experimentalLogSideEffects: true,
output: {
name: "index",
file: "dist/index.js",
format: "esm",
},
plugins: [
typescript(),
node(),
terser({
module: true,
format: {
beautify: true,
},
}),
],
});

View file

@ -0,0 +1 @@
export * from "../../esm/index";

View file

@ -0,0 +1,21 @@
import type { StructInit } from "@yume-chan/struct";
import { AndroidKeyEventAction } from "../../android/index.js";
import { EmptyControlMessage } from "../../control/index.js";
import type { ScrcpyBackOrScreenOnControlMessage } from "../../latest.js";
export const BackOrScreenOnControlMessage = EmptyControlMessage;
export type BackOrScreenOnControlMessage = StructInit<
typeof BackOrScreenOnControlMessage
>;
export function serializeBackOrScreenOnControlMessage(
message: ScrcpyBackOrScreenOnControlMessage,
) {
if (message.action === AndroidKeyEventAction.Down) {
return BackOrScreenOnControlMessage.serialize(message);
}
return undefined;
}

View file

@ -0,0 +1,43 @@
import type { PushReadableStreamController } from "@yume-chan/stream-extra";
import { PushReadableStream } from "@yume-chan/stream-extra";
import type { AsyncExactReadable } from "@yume-chan/struct";
import { string, struct, u32 } from "@yume-chan/struct";
import type { ScrcpyDeviceMessageParser } from "../../base/index.js";
export const ClipboardDeviceMessage = struct(
{ content: string(u32) },
{ littleEndian: false },
);
export class ClipboardStream
extends PushReadableStream<string>
implements ScrcpyDeviceMessageParser
{
#controller: PushReadableStreamController<string>;
constructor() {
let controller!: PushReadableStreamController<string>;
super((controller_) => {
controller = controller_;
});
this.#controller = controller;
}
async parse(id: number, stream: AsyncExactReadable): Promise<boolean> {
if (id === 0) {
const message = await ClipboardDeviceMessage.deserialize(stream);
await this.#controller.enqueue(message.content);
return true;
}
return false;
}
close() {
this.#controller.close();
}
error(e?: unknown) {
this.#controller.error(e);
}
}

View file

@ -0,0 +1,16 @@
import { ScrcpyControlMessageType } from "../../base/index.js";
export const ControlMessageTypes: readonly ScrcpyControlMessageType[] =
/* #__PURE__ */ (() => [
/* 0 */ ScrcpyControlMessageType.InjectKeyCode,
/* 1 */ ScrcpyControlMessageType.InjectText,
/* 2 */ ScrcpyControlMessageType.InjectTouch,
/* 3 */ ScrcpyControlMessageType.InjectScroll,
/* 4 */ ScrcpyControlMessageType.BackOrScreenOn,
/* 5 */ ScrcpyControlMessageType.ExpandNotificationPanel,
/* 6 */ ScrcpyControlMessageType.CollapseNotificationPanel,
/* 7 */ ScrcpyControlMessageType.GetClipboard,
/* 8 */ ScrcpyControlMessageType.SetClipboard,
/* 9 */ ScrcpyControlMessageType.SetDisplayPower,
/* 10 */ ScrcpyControlMessageType.RotateDevice,
])();

View file

@ -0,0 +1,19 @@
import type { Init } from "./init.js";
import { CodecOptions, 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: CodecOptions.Empty,
}) as const satisfies Required<Init>)();

View file

@ -0,0 +1,14 @@
export * from "./back-or-screen-on.js";
export * from "./clipboard-stream.js";
export * from "./control-message-types.js";
export * from "./defaults.js";
export * from "./init.js";
export * from "./inject-touch.js";
export * from "./media-stream-transformer.js";
export * from "./parse-display.js";
export * from "./parse-video-stream-metadata.js";
export * from "./scroll-controller.js";
export * from "./serialize-order.js";
export * from "./serialize.js";
export * from "./set-clipboard.js";
export * from "./set-list-display.js";

View file

@ -0,0 +1,17 @@
import * as assert from "node:assert";
import { describe, it } from "node:test";
import { CodecOptions } from "./init.js";
describe("CodecOptions", () => {
it("should convert empty value to `undefined`", () => {
assert.strictEqual(new CodecOptions({}).toOptionValue(), undefined);
});
it("should serialize unknown options as integers", () => {
assert.strictEqual(
new CodecOptions({ profile: 42 }).toOptionValue(),
"profile=42",
);
});
});

View file

@ -0,0 +1,150 @@
import type { ScrcpyOptionValue } from "../../base/index.js";
export const VideoOrientation = {
Unlocked: -1,
Portrait: 0,
Landscape: 1,
PortraitFlipped: 2,
LandscapeFlipped: 3,
};
export type VideoOrientation =
(typeof VideoOrientation)[keyof typeof VideoOrientation];
export type LogLevel = "debug" | "info" | "warn" | "error";
/**
* If the option you need is not in this type,
* please file an issue on GitHub.
*/
export interface CodecOptionsInit {
profile?: number | undefined;
level?: number | undefined;
iFrameInterval?: number | undefined;
maxBframes?: number | undefined;
repeatPreviousFrameAfter?: number | undefined;
maxPtsGapToEncoder?: number | undefined;
intraRefreshPeriod?: number | undefined;
}
function toDashCase(input: string) {
return input.replace(/([A-Z])/g, "-$1").toLowerCase();
}
const CodecOptionTypes: Partial<
Record<keyof CodecOptionsInit, "long" | "float" | "string">
> = {
repeatPreviousFrameAfter: "long",
maxPtsGapToEncoder: "long",
};
export class CodecOptions implements ScrcpyOptionValue {
static Empty = /* #__PURE__ */ new CodecOptions();
options: CodecOptionsInit;
constructor(options: CodecOptionsInit = {}) {
for (const [key, value] of Object.entries(options)) {
if (value === undefined) {
continue;
}
if (typeof value !== "number") {
throw new Error(
`Invalid option value for ${key}: ${String(value)}`,
);
}
}
this.options = options;
}
toOptionValue(): string | undefined {
const entries = Object.entries(this.options).filter(
([, value]) => value !== undefined,
);
if (entries.length === 0) {
return undefined;
}
return entries
.map(([key, value]) => {
let result = toDashCase(key);
const type = CodecOptionTypes[key as keyof CodecOptionsInit];
if (type) {
result += `:${type}`;
}
result += `=${value}`;
return result;
})
.join(",");
}
}
export interface Init {
logLevel?: LogLevel;
/**
* The maximum value of both width and height.
*/
maxSize?: number;
bitRate?: number;
/**
* 0 for unlimited.
*
* @default 0
*/
maxFps?: number;
/**
* The orientation of the video stream.
*
* It will not keep the device screen in specific orientation,
* only the captured video will in this orientation.
*/
lockVideoOrientation?: VideoOrientation;
/**
* Use ADB forward tunnel instead of reverse tunnel.
*
* This option is mainly used for working around the bug that on Android <9,
* ADB daemon can't create reverse tunnels if connected wirelessly (ADB over WiFi).
*
* When using `AdbScrcpyClient`, it can detect this situation and enable this option automatically.
*/
tunnelForward?: boolean;
crop?: string | undefined;
/**
* Send PTS so that the client may record properly
*
* Note: When `sendFrameMeta: false` is specified,
* the video stream will not contain `configuration` typed packets,
* which means it can't be decoded by the companion decoders.
* It's still possible to record the stream into a file,
* or to decode it with a more tolerant decoder like FFMpeg.
*
* @default true
*/
sendFrameMeta?: boolean;
/**
* @default true
*/
control?: boolean;
displayId?: number;
showTouches?: boolean;
stayAwake?: boolean;
codecOptions?: CodecOptions;
}

View file

@ -1,6 +1,9 @@
import { getUint16, setUint16 } from "@yume-chan/no-data-view";
import type { Field } from "@yume-chan/struct";
import { bipedal } from "@yume-chan/struct";
import type { Field, StructInit } from "@yume-chan/struct";
import { bipedal, struct, u16, u32, u64, u8 } from "@yume-chan/struct";
import type { AndroidMotionEventAction } from "../../android/index.js";
import type { ScrcpyInjectTouchControlMessage } from "../../latest.js";
export function clamp(value: number, min: number, max: number): number {
if (value < min) {
@ -14,7 +17,7 @@ export function clamp(value: number, min: number, max: number): number {
return value;
}
export const ScrcpyUnsignedFloat: Field<number, never, never> = {
export const UnsignedFloat: Field<number, never, never> = {
size: 2,
serialize(value, { buffer, index, littleEndian }) {
// https://github.com/Genymobile/scrcpy/blob/1f138aef41de651668043b32c4effc2d4adbfc44/app/src/util/binary.h#L51
@ -29,3 +32,28 @@ export const ScrcpyUnsignedFloat: Field<number, never, never> = {
return value === 0xffff ? 1 : value / 0x10000;
}),
};
export const InjectTouchControlMessage = struct(
{
type: u8,
action: u8<AndroidMotionEventAction>(),
pointerId: u64,
pointerX: u32,
pointerY: u32,
screenWidth: u16,
screenHeight: u16,
pressure: UnsignedFloat,
buttons: u32,
},
{ littleEndian: false },
);
export type InjectTouchControlMessage = StructInit<
typeof InjectTouchControlMessage
>;
export function serializeInjectTouchControlMessage(
message: ScrcpyInjectTouchControlMessage,
): Uint8Array {
return InjectTouchControlMessage.serialize(message);
}

View file

@ -0,0 +1,56 @@
import {
StructDeserializeStream,
TransformStream,
} from "@yume-chan/stream-extra";
import { buffer, struct, u32, u64 } from "@yume-chan/struct";
import type { ScrcpyMediaStreamPacket } from "../../base/index.js";
import type { Init } from "./init.js";
export const MediaStreamRawPacket = struct(
{ pts: u64, data: buffer(u32) },
{ littleEndian: false },
);
export const PtsConfig = 1n << 63n;
export function createMediaStreamTransformer(
options: Pick<Init, "sendFrameMeta">,
): TransformStream<Uint8Array, ScrcpyMediaStreamPacket> {
// Optimized path for video frames only
if (!options.sendFrameMeta) {
return new TransformStream({
transform(chunk, controller) {
controller.enqueue({
type: "data",
data: chunk,
});
},
});
}
const deserializeStream = new StructDeserializeStream(MediaStreamRawPacket);
return {
writable: deserializeStream.writable,
readable: deserializeStream.readable.pipeThrough(
new TransformStream({
transform(packet, controller) {
if (packet.pts === PtsConfig) {
controller.enqueue({
type: "configuration",
data: packet.data,
});
return;
}
controller.enqueue({
type: "data",
pts: packet.pts,
data: packet.data,
});
},
}),
),
};
}

View file

@ -0,0 +1,11 @@
import type { ScrcpyDisplay } from "../../base/index.js";
export function parseDisplay(line: string): ScrcpyDisplay | undefined {
const match = line.match(/^\s+scrcpy --display (\d+)$/);
if (match) {
return {
id: Number.parseInt(match[1]!, 10),
};
}
return undefined;
}

View file

@ -0,0 +1,53 @@
import {
getUint16BigEndian,
getUint32BigEndian,
} from "@yume-chan/no-data-view";
import type { ReadableStream } from "@yume-chan/stream-extra";
import { BufferedReadableStream } from "@yume-chan/stream-extra";
import type { AsyncExactReadable } from "@yume-chan/struct";
import { decodeUtf8 } from "@yume-chan/struct";
import type {
ScrcpyVideoStream,
ScrcpyVideoStreamMetadata,
} from "../../base/index.js";
import { ScrcpyVideoCodecId } from "../../base/index.js";
/**
* Parse a fixed-length, null-terminated string.
* @param stream The stream to read from
* @param maxLength The maximum length of the string, including the null terminator, in bytes
* @returns The parsed string, without the null terminator
*/
export async function readString(
stream: AsyncExactReadable,
maxLength: number,
): Promise<string> {
const buffer = await stream.readExactly(maxLength);
// If null terminator is not found, `subarray(0, -1)` will remove the last byte
// But since it's a invalid case, it's fine
return decodeUtf8(buffer.subarray(0, buffer.indexOf(0)));
}
export async function readU16(stream: AsyncExactReadable): Promise<number> {
const buffer = await stream.readExactly(2);
return getUint16BigEndian(buffer, 0);
}
export async function readU32(stream: AsyncExactReadable): Promise<number> {
const buffer = await stream.readExactly(4);
return getUint32BigEndian(buffer, 0);
}
export async function parseVideoStreamMetadata(
stream: ReadableStream<Uint8Array>,
): Promise<ScrcpyVideoStream> {
const buffered = new BufferedReadableStream(stream);
const metadata: ScrcpyVideoStreamMetadata = {
codec: ScrcpyVideoCodecId.H264,
};
metadata.deviceName = await readString(buffered, 64);
metadata.width = await readU16(buffered);
metadata.height = await readU16(buffered);
return { stream: buffered.release(), metadata };
}

View file

@ -1,13 +1,13 @@
import * as assert from "node:assert";
import { describe, it } from "node:test";
import { ScrcpyControlMessageType } from "../../control/index.js";
import { ScrcpyControlMessageType } from "../../base/index.js";
import { ScrcpyScrollController1_16 } from "./scroll.js";
import { ScrollController } from "./scroll-controller.js";
describe("ScrcpyScrollController1_16", () => {
describe("ScrollController", () => {
it("should return undefined when scroll distance is less than 1", () => {
const controller = new ScrcpyScrollController1_16();
const controller = new ScrollController();
const message = controller.serializeScrollMessage({
type: ScrcpyControlMessageType.InjectScroll,
pointerX: 0,
@ -22,7 +22,7 @@ describe("ScrcpyScrollController1_16", () => {
});
it("should return a message when scroll distance is greater than 1", () => {
const controller = new ScrcpyScrollController1_16();
const controller = new ScrollController();
const message = controller.serializeScrollMessage({
type: ScrcpyControlMessageType.InjectScroll,
pointerX: 0,
@ -38,7 +38,7 @@ describe("ScrcpyScrollController1_16", () => {
});
it("should return a message when accumulated scroll distance is greater than 1", () => {
const controller = new ScrcpyScrollController1_16();
const controller = new ScrollController();
controller.serializeScrollMessage({
type: ScrcpyControlMessageType.InjectScroll,
pointerX: 0,
@ -64,7 +64,7 @@ describe("ScrcpyScrollController1_16", () => {
});
it("should return a message when accumulated scroll distance is less than -1", () => {
const controller = new ScrcpyScrollController1_16();
const controller = new ScrollController();
controller.serializeScrollMessage({
type: ScrcpyControlMessageType.InjectScroll,
pointerX: 0,

View file

@ -1,14 +1,10 @@
import type { StructInit } from "@yume-chan/struct";
import { s32, struct, u16, u32, u8 } from "@yume-chan/struct";
import type { ScrcpyInjectScrollControlMessage } from "../../control/index.js";
import type { ScrcpyScrollController } from "../../base/index.js";
import type { ScrcpyInjectScrollControlMessage } from "../../latest.js";
export interface ScrcpyScrollController {
serializeScrollMessage(
message: ScrcpyInjectScrollControlMessage,
): Uint8Array | undefined;
}
export const ScrcpyInjectScrollControlMessage1_16 = struct(
export const InjectScrollControlMessage = struct(
{
type: u8,
pointerX: u32,
@ -21,13 +17,17 @@ export const ScrcpyInjectScrollControlMessage1_16 = struct(
{ littleEndian: false },
);
export type InjectScrollControlMessage = StructInit<
typeof InjectScrollControlMessage
>;
/**
* Old version of Scrcpy server only supports integer values for scroll.
*
* Accumulate scroll values and send scroll message when accumulated value
* reaches 1 or -1.
*/
export class ScrcpyScrollController1_16 implements ScrcpyScrollController {
export class ScrollController implements ScrcpyScrollController {
#accumulatedX = 0;
#accumulatedY = 0;
@ -72,6 +72,10 @@ export class ScrcpyScrollController1_16 implements ScrcpyScrollController {
return undefined;
}
return ScrcpyInjectScrollControlMessage1_16.serialize(processed);
return InjectScrollControlMessage.serialize(processed);
}
}
export function createScrollController(): ScrcpyScrollController {
return new ScrollController();
}

View file

@ -0,0 +1,17 @@
import type { Init } from "./init.js";
export const SerializeOrder = [
"logLevel",
"maxSize",
"bitRate",
"maxFps",
"lockVideoOrientation",
"tunnelForward",
"crop",
"sendFrameMeta",
"control",
"displayId",
"showTouches",
"stayAwake",
"codecOptions",
] as const satisfies readonly (keyof Init)[];

View file

@ -0,0 +1,5 @@
import { toScrcpyOptionValue } from "../../base/index.js";
export function serialize<T>(options: T, order: readonly (keyof T)[]) {
return order.map((key) => toScrcpyOptionValue(options[key], "-"));
}

View file

@ -0,0 +1,19 @@
import type { StructInit } from "@yume-chan/struct";
import { string, struct, u32, u8 } from "@yume-chan/struct";
import type { ScrcpySetClipboardControlMessage } from "../../latest.js";
export const SetClipboardControlMessage = struct(
{ type: u8, content: string(u32) },
{ littleEndian: false },
);
export type SetClipboardControlMessage = StructInit<
typeof SetClipboardControlMessage
>;
export function serializeSetClipboardControlMessage(
message: ScrcpySetClipboardControlMessage,
): Uint8Array {
return SetClipboardControlMessage.serialize(message);
}

View file

@ -0,0 +1,8 @@
import type { Init } from "./init.js";
export function setListDisplays(options: Pick<Init, "displayId">): void {
// Set to an invalid value
// Server will print valid values before crashing
// (server will crash before opening sockets)
options.displayId = -1;
}

View file

@ -0,0 +1,2 @@
export * as ScrcpyOptions1_15Impl from "./impl/index.js";
export * from "./options.js";

View file

@ -1,12 +1,12 @@
import * as assert from "node:assert";
import { describe, it } from "node:test";
import { ScrcpyOptions1_16 } from "./options.js";
import { ScrcpyOptions1_15 } from "./options.js";
describe("ScrcpyOptions1_16", () => {
describe("ScrcpyOptions1_15", () => {
describe("serialize", () => {
it("should return `-` for default values", () => {
assert.deepStrictEqual(new ScrcpyOptions1_16({}).serialize(), [
assert.deepStrictEqual(new ScrcpyOptions1_15({}).serialize(), [
"debug",
"0",
"8000000",
@ -26,7 +26,7 @@ describe("ScrcpyOptions1_16", () => {
describe("setListDisplays", () => {
it("should set `display` to `-1`", () => {
const options = new ScrcpyOptions1_16({});
const options = new ScrcpyOptions1_15({});
options.setListDisplays();
assert.strictEqual(options.value.displayId, -1);
});

View file

@ -0,0 +1,127 @@
import type { MaybePromiseLike } from "@yume-chan/async";
import type { ReadableStream, TransformStream } from "@yume-chan/stream-extra";
import type { AsyncExactReadable } from "@yume-chan/struct";
import type {
ScrcpyControlMessageType,
ScrcpyDisplay,
ScrcpyMediaStreamPacket,
ScrcpyOptions,
ScrcpyScrollController,
ScrcpyVideoStream,
} from "../base/index.js";
import type {
ScrcpyBackOrScreenOnControlMessage,
ScrcpyInjectTouchControlMessage,
ScrcpySetClipboardControlMessage,
} from "../latest.js";
import type { Init } from "./impl/index.js";
import {
ClipboardStream,
ControlMessageTypes,
createMediaStreamTransformer,
createScrollController,
Defaults,
parseDisplay,
parseVideoStreamMetadata,
serialize,
serializeBackOrScreenOnControlMessage,
serializeInjectTouchControlMessage,
SerializeOrder,
serializeSetClipboardControlMessage,
setListDisplays,
} from "./impl/index.js";
export class ScrcpyOptions1_15 implements ScrcpyOptions<Init> {
readonly value: Required<Init>;
get controlMessageTypes(): readonly ScrcpyControlMessageType[] {
return ControlMessageTypes;
}
#clipboard: ClipboardStream | undefined;
get clipboard(): ReadableStream<string> | undefined {
return this.#clipboard;
}
constructor(init: Init) {
this.value = { ...Defaults, ...init };
if (this.value.control) {
this.#clipboard = new ClipboardStream();
}
}
serialize(): string[] {
return serialize(this.value, SerializeOrder);
}
setListDisplays(): void {
setListDisplays(this.value);
}
parseDisplay(line: string): ScrcpyDisplay | undefined {
return parseDisplay(line);
}
parseVideoStreamMetadata(
stream: ReadableStream<Uint8Array>,
): MaybePromiseLike<ScrcpyVideoStream> {
return parseVideoStreamMetadata(stream);
}
async parseDeviceMessage(
id: number,
stream: AsyncExactReadable,
): Promise<void> {
if (await this.#clipboard!.parse(id, stream)) {
return;
}
throw new Error("Unknown device message");
}
endDeviceMessageStream(e?: unknown): void {
if (e) {
this.#clipboard!.error(e);
} else {
this.#clipboard!.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 {
return serializeSetClipboardControlMessage(message);
}
createScrollController(): ScrcpyScrollController {
return createScrollController();
}
}
type Init_ = Init;
export namespace ScrcpyOptions1_15 {
export type Init = Init_;
}

View file

@ -0,0 +1,4 @@
export {
ScrcpyOptions1_15 as ScrcpyOptions1_16,
ScrcpyOptions1_15Impl as ScrcpyOptions1_16Impl,
} from "./1_15/index.js";

View file

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

View file

@ -0,0 +1,6 @@
export * from "../../1_15/impl/index.js";
export { Defaults } from "./defaults.js";
export type { Init } from "./init.js";
export * from "./parse-encoder.js";
export { SerializeOrder } from "./serialize-order.js";
export * from "./set-list-encoder.js";

View file

@ -0,0 +1,5 @@
import type { PrevImpl } from "./prev.js";
export interface Init extends PrevImpl.Init {
encoderName?: string | undefined;
}

View file

@ -0,0 +1,14 @@
import type { ScrcpyEncoder } from "../../base/index.js";
export function parseEncoder(
line: string,
encoderNameRegex: RegExp,
): ScrcpyEncoder | undefined {
const match = line.match(encoderNameRegex);
if (match) {
return { type: "video", name: match[1]! };
}
return undefined;
}
export const EncoderRegex = /^\s+scrcpy --encoder-name '([^']+)'$/;

View file

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

View file

@ -0,0 +1,8 @@
import type { Init } from "./init.js";
import { PrevImpl } from "./prev.js";
export const SerializeOrder = /* #__PURE__ */ (() =>
[
...PrevImpl.SerializeOrder,
"encoderName",
] as const satisfies readonly (keyof Init)[])();

View file

@ -0,0 +1,8 @@
import type { Init } from "./init.js";
export function setListEncoders(options: Pick<Init, "encoderName">): void {
// Set to an invalid value
// Server will print valid values before crashing
// (server will crash after opening video and control sockets)
options.encoderName = "_";
}

View file

@ -0,0 +1,2 @@
export * as ScrcpyOptions1_17Impl from "./impl/index.js";
export * from "./options.js";

View file

@ -1,7 +1,7 @@
import * as assert from "node:assert";
import { describe, it } from "node:test";
import { ScrcpyOptions1_17 } from "./1_17.js";
import { ScrcpyOptions1_17 } from "./options.js";
describe("ScrcpyOptions1_17", () => {
it("should share `value` with `base`", () => {

View file

@ -0,0 +1,139 @@
import type { MaybePromiseLike } from "@yume-chan/async";
import type { ReadableStream, TransformStream } from "@yume-chan/stream-extra";
import type { AsyncExactReadable } from "@yume-chan/struct";
import type {
ScrcpyControlMessageType,
ScrcpyDisplay,
ScrcpyEncoder,
ScrcpyMediaStreamPacket,
ScrcpyOptions,
ScrcpyScrollController,
ScrcpyVideoStream,
} from "../base/index.js";
import type {
ScrcpyBackOrScreenOnControlMessage,
ScrcpyInjectTouchControlMessage,
ScrcpySetClipboardControlMessage,
} from "../latest.js";
import type { Init } from "./impl/index.js";
import {
ClipboardStream,
ControlMessageTypes,
createMediaStreamTransformer,
createScrollController,
Defaults,
EncoderRegex,
parseDisplay,
parseEncoder,
parseVideoStreamMetadata,
serialize,
serializeBackOrScreenOnControlMessage,
serializeInjectTouchControlMessage,
SerializeOrder,
serializeSetClipboardControlMessage,
setListDisplays,
setListEncoders,
} from "./impl/index.js";
export class ScrcpyOptions1_17 implements ScrcpyOptions<Init> {
readonly value: Required<Init>;
get controlMessageTypes(): readonly ScrcpyControlMessageType[] {
return ControlMessageTypes;
}
#clipboard: ClipboardStream | undefined;
get clipboard(): ReadableStream<string> | undefined {
return this.#clipboard;
}
constructor(init: Init) {
this.value = { ...Defaults, ...init };
if (this.value.control) {
this.#clipboard = new ClipboardStream();
}
}
serialize(): string[] {
return serialize(this.value, SerializeOrder);
}
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, EncoderRegex);
}
parseVideoStreamMetadata(
stream: ReadableStream<Uint8Array>,
): MaybePromiseLike<ScrcpyVideoStream> {
return parseVideoStreamMetadata(stream);
}
async parseDeviceMessage(
id: number,
stream: AsyncExactReadable,
): Promise<void> {
if (await this.#clipboard!.parse(id, stream)) {
return;
}
throw new Error("Unknown device message");
}
endDeviceMessageStream(e?: unknown): void {
if (e) {
this.#clipboard!.error(e);
} else {
this.#clipboard!.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 {
return serializeSetClipboardControlMessage(message);
}
createScrollController(): ScrcpyScrollController {
return createScrollController();
}
}
type Init_ = Init;
export namespace ScrcpyOptions1_17 {
export type Init = Init_;
}

View file

@ -0,0 +1,26 @@
import type { StructInit } from "@yume-chan/struct";
import { struct, u8 } from "@yume-chan/struct";
import type { AndroidKeyEventAction } from "../../android/index.js";
import type { ScrcpyBackOrScreenOnControlMessage } from "../../latest.js";
import { PrevImpl } from "./prev.js";
export const BackOrScreenOnControlMessage = /* #__PURE__ */ (() =>
struct(
{
...PrevImpl.BackOrScreenOnControlMessage.fields,
action: u8<AndroidKeyEventAction>(),
},
{ littleEndian: false },
))();
export type BackOrScreenOnControlMessage = StructInit<
typeof BackOrScreenOnControlMessage
>;
export function serializeBackOrScreenOnControlMessage(
message: ScrcpyBackOrScreenOnControlMessage,
) {
return BackOrScreenOnControlMessage.serialize(message);
}

View file

@ -0,0 +1,10 @@
import { ScrcpyControlMessageType } from "../../base/index.js";
import { PrevImpl } from "./prev.js";
export const ControlMessageTypes: readonly ScrcpyControlMessageType[] =
/* #__PURE__ */ (() => {
const result = PrevImpl.ControlMessageTypes.slice();
result.splice(6, 0, ScrcpyControlMessageType.ExpandSettingPanel);
return result;
})();

View file

@ -0,0 +1,11 @@
import type { Init } from "./init.js";
import { VideoOrientation } from "./init.js";
import { PrevImpl } from "./prev.js";
export const Defaults = /* #__PURE__ */ (() =>
({
...PrevImpl.Defaults,
logLevel: "debug",
lockVideoOrientation: VideoOrientation.Unlocked,
powerOffOnClose: false,
}) as const satisfies Required<Init>)();

View file

@ -0,0 +1,10 @@
export * from "../../1_17/impl/index.js";
export {
BackOrScreenOnControlMessage,
serializeBackOrScreenOnControlMessage,
} from "./back-or-screen-on.js";
export { ControlMessageTypes } from "./control-message-types.js";
export { Defaults } from "./defaults.js";
export type { Init } from "./init.js";
export { EncoderRegex } from "./parse-encoder.js";
export { SerializeOrder } from "./serialize-order.js";

View file

@ -0,0 +1,24 @@
import type { PrevImpl } from "./prev.js";
export type LogLevel = "verbose" | "debug" | "info" | "warn" | "error";
export const VideoOrientation = {
Initial: -2,
Unlocked: -1,
Portrait: 0,
Landscape: 1,
PortraitFlipped: 2,
LandscapeFlipped: 3,
};
export type VideoOrientation =
(typeof VideoOrientation)[keyof typeof VideoOrientation];
export interface Init
extends Omit<PrevImpl.Init, "logLevel" | "lockVideoOrientation"> {
logLevel?: LogLevel;
lockVideoOrientation?: VideoOrientation;
powerOffOnClose?: boolean;
}

View file

@ -0,0 +1 @@
export const EncoderRegex = /^\s+scrcpy --encoder '([^']+)'$/;

View file

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

View file

@ -0,0 +1,8 @@
import type { Init } from "./init.js";
import { PrevImpl } from "./prev.js";
export const SerializeOrder = /* #__PURE__ */ (() =>
[
...PrevImpl.SerializeOrder,
"powerOffOnClose",
] as const satisfies readonly (keyof Init)[])();

View file

@ -0,0 +1,2 @@
export * as ScrcpyOptions1_18Impl from "./impl/index.js";
export * from "./options.js";

View file

@ -1,7 +1,7 @@
import * as assert from "node:assert";
import { describe, it } from "node:test";
import { ScrcpyOptions1_18 } from "./1_18.js";
import { ScrcpyOptions1_18 } from "./options.js";
describe("ScrcpyOptions1_18", () => {
it("should share `value` with `base`", () => {

View file

@ -0,0 +1,139 @@
import type { MaybePromiseLike } from "@yume-chan/async";
import type { ReadableStream, TransformStream } from "@yume-chan/stream-extra";
import type { AsyncExactReadable } from "@yume-chan/struct";
import type {
ScrcpyControlMessageType,
ScrcpyDisplay,
ScrcpyEncoder,
ScrcpyMediaStreamPacket,
ScrcpyOptions,
ScrcpyScrollController,
ScrcpyVideoStream,
} from "../base/index.js";
import type {
ScrcpyBackOrScreenOnControlMessage,
ScrcpyInjectTouchControlMessage,
ScrcpySetClipboardControlMessage,
} from "../latest.js";
import type { Init } from "./impl/index.js";
import {
ClipboardStream,
ControlMessageTypes,
createMediaStreamTransformer,
createScrollController,
Defaults,
EncoderRegex,
parseDisplay,
parseEncoder,
parseVideoStreamMetadata,
serialize,
serializeBackOrScreenOnControlMessage,
serializeInjectTouchControlMessage,
SerializeOrder,
serializeSetClipboardControlMessage,
setListDisplays,
setListEncoders,
} from "./impl/index.js";
export class ScrcpyOptions1_18 implements ScrcpyOptions<Init> {
readonly value: Required<Init>;
get controlMessageTypes(): readonly ScrcpyControlMessageType[] {
return ControlMessageTypes;
}
#clipboard: ClipboardStream | undefined;
get clipboard(): ReadableStream<string> | undefined {
return this.#clipboard;
}
constructor(init: Init) {
this.value = { ...Defaults, ...init };
if (this.value.control) {
this.#clipboard = new ClipboardStream();
}
}
serialize(): string[] {
return serialize(this.value, SerializeOrder);
}
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, EncoderRegex);
}
parseVideoStreamMetadata(
stream: ReadableStream<Uint8Array>,
): MaybePromiseLike<ScrcpyVideoStream> {
return parseVideoStreamMetadata(stream);
}
async parseDeviceMessage(
id: number,
stream: AsyncExactReadable,
): Promise<void> {
if (await this.#clipboard!.parse(id, stream)) {
return;
}
throw new Error("Unknown device message");
}
endDeviceMessageStream(e?: unknown): void {
if (e) {
this.#clipboard!.error(e);
} else {
this.#clipboard!.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 {
return serializeSetClipboardControlMessage(message);
}
createScrollController(): ScrcpyScrollController {
return createScrollController();
}
}
type Init_ = Init;
export namespace ScrcpyOptions1_18 {
export type Init = Init_;
}

View file

@ -0,0 +1,4 @@
export {
ScrcpyOptions1_18 as ScrcpyOptions1_19,
ScrcpyOptions1_18Impl as ScrcpyOptions1_19Impl,
} from "./1_18/index.js";

View file

@ -0,0 +1,4 @@
export {
ScrcpyOptions1_18 as ScrcpyOptions1_20,
ScrcpyOptions1_18Impl as ScrcpyOptions1_20Impl,
} from "./1_18/index.js";

View file

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

View file

@ -0,0 +1,10 @@
export * from "../../1_18/impl/index.js";
export { Defaults } from "./defaults.js";
export type { Init } from "./init.js";
export { EncoderRegex } from "./parse-encoder.js";
export { serialize } from "./serialize.js";
export {
AckClipboardDeviceMessage,
AckClipboardHandler,
SetClipboardControlMessage,
} from "./set-clipboard.js";

View file

@ -0,0 +1,5 @@
import type { PrevImpl } from "./prev.js";
export interface Init extends PrevImpl.Init {
clipboardAutosync?: boolean;
}

View file

@ -0,0 +1 @@
export const EncoderRegex = /^\s+scrcpy --encoder-name '([^']+)'$/;

View file

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

View file

@ -0,0 +1,30 @@
import { toScrcpyOptionValue } from "../../base/option-value.js";
function toSnakeCase(input: string): string {
return input.replace(/([A-Z])/g, "_$1").toLowerCase();
}
export function serialize<T extends object>(
options: T,
defaults: Required<T>,
): string[] {
// 1.21 changed the format of arguments
const result: string[] = [];
for (const [key, value] of Object.entries(options)) {
const serializedValue = toScrcpyOptionValue(value, undefined);
if (!serializedValue) {
continue;
}
const defaultValue = toScrcpyOptionValue(
defaults[key as keyof T],
undefined,
);
if (serializedValue == defaultValue) {
continue;
}
result.push(`${toSnakeCase(key)}=${serializedValue}`);
}
return result;
}

View file

@ -0,0 +1,80 @@
import { PromiseResolver } from "@yume-chan/async";
import type { AsyncExactReadable, StructInit } from "@yume-chan/struct";
import { string, struct, u32, u64, u8 } from "@yume-chan/struct";
import type { ScrcpyDeviceMessageParser } from "../../base/index.js";
import type { ScrcpySetClipboardControlMessage } from "../../latest.js";
export const AckClipboardDeviceMessage = struct(
{ sequence: u64 },
{ littleEndian: false },
);
export const SetClipboardControlMessage = struct(
{
type: u8,
sequence: u64,
paste: u8<boolean>(),
content: string(u32),
},
{ littleEndian: false },
);
export type SetClipboardControlMessage = StructInit<
typeof SetClipboardControlMessage
>;
export class AckClipboardHandler implements ScrcpyDeviceMessageParser {
#resolvers = new Map<bigint, PromiseResolver<void>>();
#closed = false;
async parse(id: number, stream: AsyncExactReadable) {
if (id !== 1) {
return false;
}
const message = await AckClipboardDeviceMessage.deserialize(stream);
const resolver = this.#resolvers.get(message.sequence);
if (resolver) {
resolver.resolve();
this.#resolvers.delete(message.sequence);
}
return true;
}
close(): void {
for (const resolver of this.#resolvers.values()) {
resolver.reject();
}
this.#resolvers.clear();
this.#closed = true;
}
error(e?: unknown): void {
for (const resolver of this.#resolvers.values()) {
resolver.reject(e);
}
this.#resolvers.clear();
this.#closed = true;
}
serializeSetClipboardControlMessage(
message: ScrcpySetClipboardControlMessage,
): Uint8Array | [Uint8Array, Promise<void>] {
if (message.sequence === 0n) {
return SetClipboardControlMessage.serialize(message);
}
if (this.#closed) {
throw new Error();
}
const resolver = new PromiseResolver<void>();
this.#resolvers.set(message.sequence, resolver);
return [
SetClipboardControlMessage.serialize(message),
resolver.promise,
];
}
}

View file

@ -0,0 +1,2 @@
export * as ScrcpyOptions1_21Impl from "./impl/index.js";
export * from "./options.js";

View file

@ -3,9 +3,8 @@ import { describe, it } from "node:test";
import { AndroidAvcProfile } from "../codec/index.js";
import { ScrcpyOptions1_21 } from "./1_21.js";
import { CodecOptions } from "./index.js";
import { CodecOptions } from "./impl/index.js";
import { ScrcpyOptions1_21 } from "./options.js";
describe("ScrcpyOptions1_21", () => {
describe("serialize", () => {
@ -35,7 +34,9 @@ describe("ScrcpyOptions1_21", () => {
profile: AndroidAvcProfile.High,
}),
});
assert.deepEqual(options.serialize(), ["codec_options=profile=8"]);
assert.deepStrictEqual(options.serialize(), [
"codec_options=profile=8",
]);
});
});
});

View file

@ -0,0 +1,149 @@
import type { MaybePromiseLike } from "@yume-chan/async";
import type { ReadableStream, TransformStream } from "@yume-chan/stream-extra";
import type { AsyncExactReadable } from "@yume-chan/struct";
import type {
ScrcpyControlMessageType,
ScrcpyDisplay,
ScrcpyEncoder,
ScrcpyMediaStreamPacket,
ScrcpyOptions,
ScrcpyScrollController,
ScrcpyVideoStream,
} from "../base/index.js";
import type {
ScrcpyBackOrScreenOnControlMessage,
ScrcpyInjectTouchControlMessage,
ScrcpySetClipboardControlMessage,
} from "../latest.js";
import type { Init } from "./impl/index.js";
import {
AckClipboardHandler,
ClipboardStream,
ControlMessageTypes,
createMediaStreamTransformer,
createScrollController,
Defaults,
EncoderRegex,
parseDisplay,
parseEncoder,
parseVideoStreamMetadata,
serialize,
serializeBackOrScreenOnControlMessage,
serializeInjectTouchControlMessage,
setListDisplays,
setListEncoders,
} from "./impl/index.js";
export class ScrcpyOptions1_21 implements ScrcpyOptions<Init> {
readonly value: Required<Init>;
get controlMessageTypes(): readonly ScrcpyControlMessageType[] {
return ControlMessageTypes;
}
#clipboard: ClipboardStream | undefined;
get clipboard(): ReadableStream<string> | undefined {
return this.#clipboard;
}
#ackClipboardHandler: AckClipboardHandler | undefined;
constructor(init: Init) {
this.value = { ...Defaults, ...init };
if (this.value.control && this.value.clipboardAutosync) {
this.#clipboard = new ClipboardStream();
this.#ackClipboardHandler = new AckClipboardHandler();
}
}
serialize(): string[] {
return serialize(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, EncoderRegex);
}
parseVideoStreamMetadata(
stream: ReadableStream<Uint8Array>,
): MaybePromiseLike<ScrcpyVideoStream> {
return parseVideoStreamMetadata(stream);
}
async parseDeviceMessage(
id: number,
stream: 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();
}
}
type Init_ = Init;
export namespace ScrcpyOptions1_21 {
export type Init = Init_;
}

View file

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

View file

@ -0,0 +1,8 @@
export * from "../../1_21/impl/index.js";
export { Defaults } from "./defaults.js";
export type { Init } from "./init.js";
export { parseVideoStreamMetadata } from "./parse-video-stream-metadata.js";
export {
InjectScrollControlMessage,
ScrollController,
} from "./scroll-controller.js";

View file

@ -1,6 +1,6 @@
import type { ScrcpyOptionsInit1_21 } from "../1_21.js";
import type { PrevImpl } from "./prev.js";
export interface ScrcpyOptionsInit1_22 extends ScrcpyOptionsInit1_21 {
export interface Init extends PrevImpl.Init {
downsizeOnError?: boolean;
/**

View file

@ -0,0 +1,18 @@
import type { ReadableStream } from "@yume-chan/stream-extra";
import type { ScrcpyVideoStream } from "../../base/video.js";
import { ScrcpyVideoCodecId } from "../../base/video.js";
import type { Init } from "./init.js";
import { PrevImpl } from "./prev.js";
export async function parseVideoStreamMetadata(
options: Pick<Init, "sendDeviceMeta">,
stream: ReadableStream<Uint8Array>,
): Promise<ScrcpyVideoStream> {
if (!options.sendDeviceMeta) {
return { stream, metadata: { codec: ScrcpyVideoCodecId.H264 } };
} else {
return PrevImpl.parseVideoStreamMetadata(stream);
}
}

View file

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

View file

@ -1,13 +1,13 @@
import * as assert from "node:assert";
import { describe, it } from "node:test";
import { ScrcpyControlMessageType } from "../../control/index.js";
import { ScrcpyControlMessageType } from "../../base/index.js";
import { ScrcpyScrollController1_22 } from "./scroll.js";
import { ScrollController } from "./scroll-controller.js";
describe("ScrcpyScrollController1_22", () => {
describe("ScrollController", () => {
it("should return correct message length", () => {
const controller = new ScrcpyScrollController1_22();
const controller = new ScrollController();
const message = controller.serializeScrollMessage({
type: ScrcpyControlMessageType.InjectScroll,
pointerX: 0,

View file

@ -0,0 +1,30 @@
import type { StructInit } from "@yume-chan/struct";
import { s32, struct } from "@yume-chan/struct";
import { PrevImpl } from "./prev.js";
export const InjectScrollControlMessage = /* #__PURE__ */ (() =>
struct(
{
...PrevImpl.InjectScrollControlMessage.fields,
buttons: s32,
},
{ littleEndian: false },
))();
export type InjectScrollControlMessage = StructInit<
typeof InjectScrollControlMessage
>;
export class ScrollController extends PrevImpl.ScrollController {
override serializeScrollMessage(
message: InjectScrollControlMessage,
): Uint8Array | undefined {
const processed = this.processMessage(message);
if (!processed) {
return undefined;
}
return InjectScrollControlMessage.serialize(processed);
}
}

View file

@ -0,0 +1,2 @@
export * as ScrcpyOptions1_22Impl from "./impl/index.js";
export * from "./options.js";

View file

@ -1,7 +1,7 @@
import * as assert from "node:assert";
import { describe, it } from "node:test";
import { ScrcpyOptions1_21 } from "../1_21.js";
import { ScrcpyOptions1_21 } from "../1_21/index.js";
import { ScrcpyOptions1_22 } from "./options.js";

View file

@ -0,0 +1,149 @@
import type { MaybePromiseLike } from "@yume-chan/async";
import type { ReadableStream, TransformStream } from "@yume-chan/stream-extra";
import type { AsyncExactReadable } from "@yume-chan/struct";
import type {
ScrcpyControlMessageType,
ScrcpyDisplay,
ScrcpyEncoder,
ScrcpyMediaStreamPacket,
ScrcpyOptions,
ScrcpyScrollController,
ScrcpyVideoStream,
} from "../base/index.js";
import type {
ScrcpyBackOrScreenOnControlMessage,
ScrcpyInjectTouchControlMessage,
ScrcpySetClipboardControlMessage,
} from "../latest.js";
import type { Init } from "./impl/index.js";
import {
AckClipboardHandler,
ClipboardStream,
ControlMessageTypes,
createMediaStreamTransformer,
createScrollController,
Defaults,
EncoderRegex,
parseDisplay,
parseEncoder,
parseVideoStreamMetadata,
serialize,
serializeBackOrScreenOnControlMessage,
serializeInjectTouchControlMessage,
setListDisplays,
setListEncoders,
} from "./impl/index.js";
export class ScrcpyOptions1_22 implements ScrcpyOptions<Init> {
readonly value: Required<Init>;
get controlMessageTypes(): readonly ScrcpyControlMessageType[] {
return ControlMessageTypes;
}
#clipboard: ClipboardStream | undefined;
get clipboard(): ReadableStream<string> | undefined {
return this.#clipboard;
}
#ackClipboardHandler: AckClipboardHandler | undefined;
constructor(init: Init) {
this.value = { ...Defaults, ...init };
if (this.value.control && this.value.clipboardAutosync) {
this.#clipboard = new ClipboardStream();
this.#ackClipboardHandler = new AckClipboardHandler();
}
}
serialize(): string[] {
return serialize(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, EncoderRegex);
}
parseVideoStreamMetadata(
stream: ReadableStream<Uint8Array>,
): MaybePromiseLike<ScrcpyVideoStream> {
return parseVideoStreamMetadata(this.value, stream);
}
async parseDeviceMessage(
id: number,
stream: 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();
}
}
type Init_ = Init;
export namespace ScrcpyOptions1_22 {
export type Init = Init_;
}

View file

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

View file

@ -0,0 +1,7 @@
export * from "../../1_22/impl/index.js";
export { Defaults } from "./defaults.js";
export type { Init } from "./init.js";
export {
PtsKeyframe,
createMediaStreamTransformer,
} from "./media-stream-transformer.js";

View file

@ -0,0 +1,5 @@
import type { PrevImpl } from "./prev.js";
export interface Init extends PrevImpl.Init {
cleanup?: boolean;
}

View file

@ -0,0 +1,64 @@
import {
StructDeserializeStream,
TransformStream,
} from "@yume-chan/stream-extra";
import type { ScrcpyMediaStreamPacket } from "../../base/index.js";
import type { Init } from "./init.js";
import { PrevImpl } from "./prev.js";
export const PtsKeyframe = 1n << 62n;
export function createMediaStreamTransformer(
options: Pick<Init, "sendFrameMeta">,
): TransformStream<Uint8Array, ScrcpyMediaStreamPacket> {
// Optimized path for video frames only
if (!options.sendFrameMeta) {
return new TransformStream({
transform(chunk, controller) {
controller.enqueue({
type: "data",
data: chunk,
});
},
});
}
const deserializeStream = new StructDeserializeStream(
PrevImpl.MediaStreamRawPacket,
);
return {
writable: deserializeStream.writable,
readable: deserializeStream.readable.pipeThrough(
new TransformStream({
transform(packet, controller) {
if (packet.pts === PrevImpl.PtsConfig) {
controller.enqueue({
type: "configuration",
data: packet.data,
});
return;
}
if (packet.pts & PtsKeyframe) {
controller.enqueue({
type: "data",
keyframe: true,
pts: packet.pts & ~PtsKeyframe,
data: packet.data,
});
return;
}
controller.enqueue({
type: "data",
keyframe: false,
pts: packet.pts,
data: packet.data,
});
},
}),
),
};
}

View file

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

View file

@ -0,0 +1,2 @@
export * as ScrcpyOptions1_23Impl from "./impl/index.js";
export * from "./options.js";

View file

@ -0,0 +1,149 @@
import type { MaybePromiseLike } from "@yume-chan/async";
import type { ReadableStream, TransformStream } from "@yume-chan/stream-extra";
import type { AsyncExactReadable } from "@yume-chan/struct";
import type {
ScrcpyControlMessageType,
ScrcpyDisplay,
ScrcpyEncoder,
ScrcpyMediaStreamPacket,
ScrcpyOptions,
ScrcpyScrollController,
ScrcpyVideoStream,
} from "../base/index.js";
import type {
ScrcpyBackOrScreenOnControlMessage,
ScrcpyInjectTouchControlMessage,
ScrcpySetClipboardControlMessage,
} from "../latest.js";
import type { Init } from "./impl/index.js";
import {
AckClipboardHandler,
ClipboardStream,
ControlMessageTypes,
createMediaStreamTransformer,
createScrollController,
Defaults,
EncoderRegex,
parseDisplay,
parseEncoder,
parseVideoStreamMetadata,
serialize,
serializeBackOrScreenOnControlMessage,
serializeInjectTouchControlMessage,
setListDisplays,
setListEncoders,
} from "./impl/index.js";
export class ScrcpyOptions1_23 implements ScrcpyOptions<Init> {
readonly value: Required<Init>;
get controlMessageTypes(): readonly ScrcpyControlMessageType[] {
return ControlMessageTypes;
}
#clipboard: ClipboardStream | undefined;
get clipboard(): ReadableStream<string> | undefined {
return this.#clipboard;
}
#ackClipboardHandler: AckClipboardHandler | undefined;
constructor(init: Init) {
this.value = { ...Defaults, ...init };
if (this.value.control && this.value.clipboardAutosync) {
this.#clipboard = new ClipboardStream();
this.#ackClipboardHandler = new AckClipboardHandler();
}
}
serialize(): string[] {
return serialize(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, EncoderRegex);
}
parseVideoStreamMetadata(
stream: ReadableStream<Uint8Array>,
): MaybePromiseLike<ScrcpyVideoStream> {
return parseVideoStreamMetadata(this.value, stream);
}
async parseDeviceMessage(
id: number,
stream: 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();
}
}
type Init_ = Init;
export namespace ScrcpyOptions1_23 {
export type Init = Init_;
}

View file

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

View file

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

View file

@ -0,0 +1,5 @@
import type { PrevImpl } from "./prev.js";
export interface Init extends PrevImpl.Init {
powerOn?: boolean;
}

View file

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

View file

@ -0,0 +1,2 @@
export * as ScrcpyOptions1_24Impl from "./impl/index.js";
export * from "./options.js";

View file

@ -0,0 +1,149 @@
import type { MaybePromiseLike } from "@yume-chan/async";
import type { ReadableStream, TransformStream } from "@yume-chan/stream-extra";
import type { AsyncExactReadable } from "@yume-chan/struct";
import type {
ScrcpyControlMessageType,
ScrcpyDisplay,
ScrcpyEncoder,
ScrcpyMediaStreamPacket,
ScrcpyOptions,
ScrcpyScrollController,
ScrcpyVideoStream,
} from "../base/index.js";
import type {
ScrcpyBackOrScreenOnControlMessage,
ScrcpyInjectTouchControlMessage,
ScrcpySetClipboardControlMessage,
} from "../latest.js";
import type { Init } from "./impl/index.js";
import {
AckClipboardHandler,
ClipboardStream,
ControlMessageTypes,
createMediaStreamTransformer,
createScrollController,
Defaults,
EncoderRegex,
parseDisplay,
parseEncoder,
parseVideoStreamMetadata,
serialize,
serializeBackOrScreenOnControlMessage,
serializeInjectTouchControlMessage,
setListDisplays,
setListEncoders,
} from "./impl/index.js";
export class ScrcpyOptions1_24 implements ScrcpyOptions<Init> {
readonly value: Required<Init>;
get controlMessageTypes(): readonly ScrcpyControlMessageType[] {
return ControlMessageTypes;
}
#clipboard: ClipboardStream | undefined;
get clipboard(): ReadableStream<string> | undefined {
return this.#clipboard;
}
#ackClipboardHandler: AckClipboardHandler | undefined;
constructor(init: Init) {
this.value = { ...Defaults, ...init };
if (this.value.control && this.value.clipboardAutosync) {
this.#clipboard = new ClipboardStream();
this.#ackClipboardHandler = new AckClipboardHandler();
}
}
serialize(): string[] {
return serialize(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, EncoderRegex);
}
parseVideoStreamMetadata(
stream: ReadableStream<Uint8Array>,
): MaybePromiseLike<ScrcpyVideoStream> {
return parseVideoStreamMetadata(this.value, stream);
}
async parseDeviceMessage(
id: number,
stream: 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();
}
}
type Init_ = Init;
export namespace ScrcpyOptions1_24 {
export type Init = Init_;
}

View file

@ -0,0 +1,6 @@
export * from "../../1_24/impl/index.js";
export {
InjectScrollControlMessage,
ScrollController,
SignedFloat,
} from "./scroll-controller.js";

View file

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

View file

@ -1,14 +1,14 @@
import * as assert from "node:assert";
import { describe, it } from "node:test";
import { ScrcpyControlMessageType } from "../../control/index.js";
import { ScrcpyControlMessageType } from "../../base/index.js";
import { ScrcpyScrollController1_25, ScrcpySignedFloat } from "./scroll.js";
import { ScrollController, SignedFloat } from "./scroll-controller.js";
describe("ScrcpySignedFloat", () => {
describe("SignedFloat", () => {
it("should serialize", () => {
const array = new Uint8Array(2);
ScrcpySignedFloat.serialize(-1, {
SignedFloat.serialize(-1, {
buffer: array,
index: 0,
littleEndian: true,
@ -18,14 +18,14 @@ describe("ScrcpySignedFloat", () => {
-0x8000,
);
ScrcpySignedFloat.serialize(0, {
SignedFloat.serialize(0, {
buffer: array,
index: 0,
littleEndian: true,
});
assert.strictEqual(new DataView(array.buffer).getInt16(0, true), 0);
ScrcpySignedFloat.serialize(1, {
SignedFloat.serialize(1, {
buffer: array,
index: 0,
littleEndian: true,
@ -38,7 +38,7 @@ describe("ScrcpySignedFloat", () => {
it("should clamp input values", () => {
const array = new Uint8Array(2);
ScrcpySignedFloat.serialize(-2, {
SignedFloat.serialize(-2, {
buffer: array,
index: 0,
littleEndian: true,
@ -48,7 +48,7 @@ describe("ScrcpySignedFloat", () => {
-0x8000,
);
ScrcpySignedFloat.serialize(2, {
SignedFloat.serialize(2, {
buffer: array,
index: 0,
littleEndian: true,
@ -65,7 +65,7 @@ describe("ScrcpySignedFloat", () => {
dataView.setInt16(0, -0x8000, true);
assert.strictEqual(
ScrcpySignedFloat.deserialize({
SignedFloat.deserialize({
runtimeStruct: {} as never,
reader: { position: 0, readExactly: () => view },
littleEndian: true,
@ -75,7 +75,7 @@ describe("ScrcpySignedFloat", () => {
dataView.setInt16(0, 0, true);
assert.strictEqual(
ScrcpySignedFloat.deserialize({
SignedFloat.deserialize({
runtimeStruct: {} as never,
reader: { position: 0, readExactly: () => view },
littleEndian: true,
@ -85,7 +85,7 @@ describe("ScrcpySignedFloat", () => {
dataView.setInt16(0, 0x7fff, true);
assert.strictEqual(
ScrcpySignedFloat.deserialize({
SignedFloat.deserialize({
runtimeStruct: {} as never,
reader: { position: 0, readExactly: () => view },
littleEndian: true,
@ -95,9 +95,9 @@ describe("ScrcpySignedFloat", () => {
});
});
describe("ScrcpyScrollController1_25", () => {
describe("ScrollController", () => {
it("should return a message for each scroll event", () => {
const controller = new ScrcpyScrollController1_25();
const controller = new ScrollController();
const message1 = controller.serializeScrollMessage({
type: ScrcpyControlMessageType.InjectScroll,
pointerX: 0,

View file

@ -2,15 +2,16 @@ import { getInt16, setInt16 } from "@yume-chan/no-data-view";
import type { Field, StructInit } from "@yume-chan/struct";
import { bipedal, struct, u16, u32, u8 } from "@yume-chan/struct";
import type { ScrcpyInjectScrollControlMessage } from "../../control/index.js";
import type { ScrcpyScrollController } from "../1_16/index.js";
import { clamp } from "../1_16/index.js";
import type { ScrcpyScrollController } from "../../base/index.js";
import type { ScrcpyInjectScrollControlMessage } from "../../latest.js";
export const ScrcpySignedFloat: Field<number, never, never> = {
import { PrevImpl } from "./prev.js";
export const SignedFloat: Field<number, never, never> = {
size: 2,
serialize(value, { buffer, index, littleEndian }) {
// https://github.com/Genymobile/scrcpy/blob/1f138aef41de651668043b32c4effc2d4adbfc44/app/src/util/binary.h#L51
value = clamp(value, -1, 1);
value = PrevImpl.clamp(value, -1, 1);
value = value === 1 ? 0x7fff : value * 0x8000;
setInt16(buffer, index, value, littleEndian);
},
@ -22,28 +23,28 @@ export const ScrcpySignedFloat: Field<number, never, never> = {
}),
};
export const ScrcpyInjectScrollControlMessage1_25 = struct(
export const InjectScrollControlMessage = struct(
{
type: u8,
pointerX: u32,
pointerY: u32,
screenWidth: u16,
screenHeight: u16,
scrollX: ScrcpySignedFloat,
scrollY: ScrcpySignedFloat,
scrollX: SignedFloat,
scrollY: SignedFloat,
buttons: u32,
},
{ littleEndian: false },
);
export type ScrcpyInjectScrollControlMessage1_25 = StructInit<
typeof ScrcpyInjectScrollControlMessage1_25
export type InjectScrollControlMessage = StructInit<
typeof InjectScrollControlMessage
>;
export class ScrcpyScrollController1_25 implements ScrcpyScrollController {
export class ScrollController implements ScrcpyScrollController {
serializeScrollMessage(
message: ScrcpyInjectScrollControlMessage,
): Uint8Array | undefined {
return ScrcpyInjectScrollControlMessage1_25.serialize(message);
return InjectScrollControlMessage.serialize(message);
}
}

View file

@ -0,0 +1,2 @@
export * as ScrcpyOptions1_25Impl from "./impl/index.js";
export * from "./options.js";

View file

@ -0,0 +1,149 @@
import type { MaybePromiseLike } from "@yume-chan/async";
import type { ReadableStream, TransformStream } from "@yume-chan/stream-extra";
import type { AsyncExactReadable } from "@yume-chan/struct";
import type {
ScrcpyControlMessageType,
ScrcpyDisplay,
ScrcpyEncoder,
ScrcpyMediaStreamPacket,
ScrcpyOptions,
ScrcpyScrollController,
ScrcpyVideoStream,
} from "../base/index.js";
import type {
ScrcpyBackOrScreenOnControlMessage,
ScrcpyInjectTouchControlMessage,
ScrcpySetClipboardControlMessage,
} from "../latest.js";
import type { Init } from "./impl/index.js";
import {
AckClipboardHandler,
ClipboardStream,
ControlMessageTypes,
createMediaStreamTransformer,
createScrollController,
Defaults,
EncoderRegex,
parseDisplay,
parseEncoder,
parseVideoStreamMetadata,
serialize,
serializeBackOrScreenOnControlMessage,
serializeInjectTouchControlMessage,
setListDisplays,
setListEncoders,
} from "./impl/index.js";
export class ScrcpyOptions1_25 implements ScrcpyOptions<Init> {
readonly value: Required<Init>;
get controlMessageTypes(): readonly ScrcpyControlMessageType[] {
return ControlMessageTypes;
}
#clipboard: ClipboardStream | undefined;
get clipboard(): ReadableStream<string> | undefined {
return this.#clipboard;
}
#ackClipboardHandler: AckClipboardHandler | undefined;
constructor(init: Init) {
this.value = { ...Defaults, ...init };
if (this.value.control && this.value.clipboardAutosync) {
this.#clipboard = new ClipboardStream();
this.#ackClipboardHandler = new AckClipboardHandler();
}
}
serialize(): string[] {
return serialize(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, EncoderRegex);
}
parseVideoStreamMetadata(
stream: ReadableStream<Uint8Array>,
): MaybePromiseLike<ScrcpyVideoStream> {
return parseVideoStreamMetadata(this.value, stream);
}
async parseDeviceMessage(
id: number,
stream: 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();
}
}
type Init_ = Init;
export namespace ScrcpyOptions1_25 {
export type Init = Init_;
}

View file

@ -0,0 +1,24 @@
import type { Init } from "./init.js";
import { InstanceId } from "./init.js";
import { PrevImpl } from "./prev.js";
export const Defaults = /* #__PURE__ */ (() =>
({
...PrevImpl.Defaults,
scid: InstanceId.NONE,
videoCodec: "h264",
videoBitRate: 8000000,
videoCodecOptions: PrevImpl.CodecOptions.Empty,
videoEncoder: undefined,
audio: true,
audioCodec: "opus",
audioBitRate: 128000,
audioCodecOptions: PrevImpl.CodecOptions.Empty,
audioEncoder: undefined,
listEncoders: false,
listDisplays: false,
sendCodecMeta: true,
}) as const satisfies Required<Init>)();

View file

@ -0,0 +1,12 @@
export * from "../../1_25/impl/index.js";
export { Defaults } from "./defaults.js";
export { InstanceId, type Init } from "./init.js";
export {
InjectTouchControlMessage,
serializeInjectTouchControlMessage,
} from "./inject-touch.js";
export * from "./parse-audio-stream-metadata.js";
export { parseEncoder } from "./parse-encoder.js";
export { parseVideoStreamMetadata } from "./parse-video-stream-metadata.js";
export { setListDisplays } from "./set-list-display.js";
export { setListEncoders } from "./set-list-encoder.js";

View file

@ -0,0 +1,45 @@
import type { ScrcpyOptionValue } from "../../base/index.js";
import type { PrevImpl } from "./prev.js";
export class InstanceId implements ScrcpyOptionValue {
static readonly NONE = /* #__PURE__ */ new InstanceId(-1);
static random(): InstanceId {
// A random 31-bit unsigned integer
return new InstanceId((Math.random() * 0x80000000) | 0);
}
value: number;
constructor(value: number) {
this.value = value;
}
toOptionValue(): string | undefined {
if (this.value < 0) {
return undefined;
}
return this.value.toString(16);
}
}
export interface Init
extends Omit<PrevImpl.Init, "bitRate" | "codecOptions" | "encoderName"> {
scid?: InstanceId;
videoCodec?: "h264" | "h265" | "av1";
videoBitRate?: number;
videoCodecOptions?: PrevImpl.CodecOptions;
videoEncoder?: string | undefined;
audio?: boolean;
audioCodec?: "raw" | "opus" | "aac";
audioBitRate?: number;
audioCodecOptions?: PrevImpl.CodecOptions;
audioEncoder?: string | undefined;
listEncoders?: boolean;
listDisplays?: boolean;
sendCodecMeta?: boolean;
}

View file

@ -0,0 +1,33 @@
import type { StructInit } from "@yume-chan/struct";
import { struct, u16, u32, u64, u8 } from "@yume-chan/struct";
import type { AndroidMotionEventAction } from "../../android/motion-event.js";
import type { ScrcpyInjectTouchControlMessage } from "../../latest.js";
import { PrevImpl } from "./prev.js";
export const InjectTouchControlMessage = struct(
{
type: u8,
action: u8<AndroidMotionEventAction>(),
pointerId: u64,
pointerX: u32,
pointerY: u32,
screenWidth: u16,
screenHeight: u16,
pressure: PrevImpl.UnsignedFloat,
actionButton: u32,
buttons: u32,
},
{ littleEndian: false },
);
export type InjectTouchControlMessage = StructInit<
typeof InjectTouchControlMessage
>;
export function serializeInjectTouchControlMessage(
message: ScrcpyInjectTouchControlMessage,
): Uint8Array {
return InjectTouchControlMessage.serialize(message);
}

View file

@ -0,0 +1,99 @@
import { getUint32BigEndian } from "@yume-chan/no-data-view";
import type { ReadableStream } from "@yume-chan/stream-extra";
import {
BufferedReadableStream,
PushReadableStream,
} from "@yume-chan/stream-extra";
import type { Init } from "../../2_3/impl/init.js";
import type { ScrcpyAudioStreamMetadata } from "../../base/index.js";
import { ScrcpyAudioCodec } from "../../base/index.js";
export async function parseAudioStreamMetadata(
stream: ReadableStream<Uint8Array>,
options: Pick<Required<Init>, "sendCodecMeta" | "audioCodec">,
): Promise<ScrcpyAudioStreamMetadata> {
const buffered = new BufferedReadableStream(stream);
const buffer = await buffered.readExactly(4);
// Treat it as a 32-bit number for simpler comparisons
const codecMetadataValue = getUint32BigEndian(buffer, 0);
// Server will send `0x00_00_00_00` and `0x00_00_00_01` even if `sendCodecMeta` is false
switch (codecMetadataValue) {
case 0x00_00_00_00:
return {
type: "disabled",
};
case 0x00_00_00_01:
return {
type: "errored",
};
}
if (options.sendCodecMeta) {
let codec: ScrcpyAudioCodec;
switch (codecMetadataValue) {
case ScrcpyAudioCodec.Raw.metadataValue:
codec = ScrcpyAudioCodec.Raw;
break;
case ScrcpyAudioCodec.Opus.metadataValue:
codec = ScrcpyAudioCodec.Opus;
break;
case ScrcpyAudioCodec.Aac.metadataValue:
codec = ScrcpyAudioCodec.Aac;
break;
case ScrcpyAudioCodec.Flac.metadataValue:
codec = ScrcpyAudioCodec.Flac;
break;
default:
throw new Error(
`Unknown audio codec metadata value: ${codecMetadataValue}`,
);
}
return {
type: "success",
codec,
stream: buffered.release(),
};
}
// Infer codec from `audioCodec` option
let codec: ScrcpyAudioCodec;
switch (options.audioCodec as string) {
case "raw":
codec = ScrcpyAudioCodec.Raw;
break;
case "opus":
codec = ScrcpyAudioCodec.Opus;
break;
case "aac":
codec = ScrcpyAudioCodec.Aac;
break;
case "flac":
codec = ScrcpyAudioCodec.Flac;
break;
default:
throw new Error(
`Unknown audio codec metadata value: ${codecMetadataValue}`,
);
}
return {
type: "success",
codec,
stream: new PushReadableStream<Uint8Array>(async (controller) => {
// Put the first 4 bytes back
await controller.enqueue(buffer);
const stream = buffered.release();
const reader = stream.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) {
break;
}
await controller.enqueue(value);
}
}),
};
}

View file

@ -0,0 +1,15 @@
import type { ScrcpyDisplay } from "../../base/index.js";
export function parseDisplay(line: string): ScrcpyDisplay | undefined {
const match = line.match(/^\s+--display=(\d+)\s+\(([^)]+)\)$/);
if (match) {
const display: ScrcpyDisplay = {
id: Number.parseInt(match[1]!, 10),
};
if (match[2] !== "size unknown") {
display.resolution = match[2]!;
}
return display;
}
return undefined;
}

Some files were not shown because too many files have changed in this diff Show more