mirror of
https://github.com/yume-chan/ya-webadb.git
synced 2025-10-03 09:49:24 +02:00
refactor(scrcpy): rewrite option classes to improve tree-shaking
This commit is contained in:
parent
db8466f6ee
commit
92472007db
218 changed files with 5412 additions and 2380 deletions
|
@ -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(() => {});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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`,
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>({
|
||||
|
|
19
libraries/scrcpy/side-effect-test/package.json
Normal file
19
libraries/scrcpy/side-effect-test/package.json
Normal 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"
|
||||
}
|
||||
}
|
24
libraries/scrcpy/side-effect-test/rollup.config.ts
Normal file
24
libraries/scrcpy/side-effect-test/rollup.config.ts
Normal 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,
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
1
libraries/scrcpy/side-effect-test/src/index.js
Normal file
1
libraries/scrcpy/side-effect-test/src/index.js
Normal file
|
@ -0,0 +1 @@
|
|||
export * from "../../esm/index";
|
21
libraries/scrcpy/src/1_15/impl/back-or-screen-on.ts
Normal file
21
libraries/scrcpy/src/1_15/impl/back-or-screen-on.ts
Normal 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;
|
||||
}
|
43
libraries/scrcpy/src/1_15/impl/clipboard-stream.ts
Normal file
43
libraries/scrcpy/src/1_15/impl/clipboard-stream.ts
Normal 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);
|
||||
}
|
||||
}
|
16
libraries/scrcpy/src/1_15/impl/control-message-types.ts
Normal file
16
libraries/scrcpy/src/1_15/impl/control-message-types.ts
Normal 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,
|
||||
])();
|
19
libraries/scrcpy/src/1_15/impl/defaults.ts
Normal file
19
libraries/scrcpy/src/1_15/impl/defaults.ts
Normal 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>)();
|
14
libraries/scrcpy/src/1_15/impl/index.ts
Normal file
14
libraries/scrcpy/src/1_15/impl/index.ts
Normal 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";
|
17
libraries/scrcpy/src/1_15/impl/init.spec.ts
Normal file
17
libraries/scrcpy/src/1_15/impl/init.spec.ts
Normal 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",
|
||||
);
|
||||
});
|
||||
});
|
150
libraries/scrcpy/src/1_15/impl/init.ts
Normal file
150
libraries/scrcpy/src/1_15/impl/init.ts
Normal 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;
|
||||
}
|
|
@ -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);
|
||||
}
|
56
libraries/scrcpy/src/1_15/impl/media-stream-transformer.ts
Normal file
56
libraries/scrcpy/src/1_15/impl/media-stream-transformer.ts
Normal 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,
|
||||
});
|
||||
},
|
||||
}),
|
||||
),
|
||||
};
|
||||
}
|
11
libraries/scrcpy/src/1_15/impl/parse-display.ts
Normal file
11
libraries/scrcpy/src/1_15/impl/parse-display.ts
Normal 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;
|
||||
}
|
|
@ -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 };
|
||||
}
|
|
@ -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,
|
|
@ -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();
|
||||
}
|
17
libraries/scrcpy/src/1_15/impl/serialize-order.ts
Normal file
17
libraries/scrcpy/src/1_15/impl/serialize-order.ts
Normal 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)[];
|
5
libraries/scrcpy/src/1_15/impl/serialize.ts
Normal file
5
libraries/scrcpy/src/1_15/impl/serialize.ts
Normal 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], "-"));
|
||||
}
|
19
libraries/scrcpy/src/1_15/impl/set-clipboard.ts
Normal file
19
libraries/scrcpy/src/1_15/impl/set-clipboard.ts
Normal 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);
|
||||
}
|
8
libraries/scrcpy/src/1_15/impl/set-list-display.ts
Normal file
8
libraries/scrcpy/src/1_15/impl/set-list-display.ts
Normal 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;
|
||||
}
|
2
libraries/scrcpy/src/1_15/index.ts
Normal file
2
libraries/scrcpy/src/1_15/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export * as ScrcpyOptions1_15Impl from "./impl/index.js";
|
||||
export * from "./options.js";
|
|
@ -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);
|
||||
});
|
127
libraries/scrcpy/src/1_15/options.ts
Normal file
127
libraries/scrcpy/src/1_15/options.ts
Normal 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_;
|
||||
}
|
4
libraries/scrcpy/src/1_16.ts
Normal file
4
libraries/scrcpy/src/1_16.ts
Normal file
|
@ -0,0 +1,4 @@
|
|||
export {
|
||||
ScrcpyOptions1_15 as ScrcpyOptions1_16,
|
||||
ScrcpyOptions1_15Impl as ScrcpyOptions1_16Impl,
|
||||
} from "./1_15/index.js";
|
8
libraries/scrcpy/src/1_17/impl/defaults.ts
Normal file
8
libraries/scrcpy/src/1_17/impl/defaults.ts
Normal 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>)();
|
6
libraries/scrcpy/src/1_17/impl/index.ts
Normal file
6
libraries/scrcpy/src/1_17/impl/index.ts
Normal 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";
|
5
libraries/scrcpy/src/1_17/impl/init.ts
Normal file
5
libraries/scrcpy/src/1_17/impl/init.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
import type { PrevImpl } from "./prev.js";
|
||||
|
||||
export interface Init extends PrevImpl.Init {
|
||||
encoderName?: string | undefined;
|
||||
}
|
14
libraries/scrcpy/src/1_17/impl/parse-encoder.ts
Normal file
14
libraries/scrcpy/src/1_17/impl/parse-encoder.ts
Normal 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 '([^']+)'$/;
|
1
libraries/scrcpy/src/1_17/impl/prev.ts
Normal file
1
libraries/scrcpy/src/1_17/impl/prev.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * as PrevImpl from "../../1_15/impl/index.js";
|
8
libraries/scrcpy/src/1_17/impl/serialize-order.ts
Normal file
8
libraries/scrcpy/src/1_17/impl/serialize-order.ts
Normal 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)[])();
|
8
libraries/scrcpy/src/1_17/impl/set-list-encoder.ts
Normal file
8
libraries/scrcpy/src/1_17/impl/set-list-encoder.ts
Normal 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 = "_";
|
||||
}
|
2
libraries/scrcpy/src/1_17/index.ts
Normal file
2
libraries/scrcpy/src/1_17/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export * as ScrcpyOptions1_17Impl from "./impl/index.js";
|
||||
export * from "./options.js";
|
|
@ -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`", () => {
|
139
libraries/scrcpy/src/1_17/options.ts
Normal file
139
libraries/scrcpy/src/1_17/options.ts
Normal 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_;
|
||||
}
|
26
libraries/scrcpy/src/1_18/impl/back-or-screen-on.ts
Normal file
26
libraries/scrcpy/src/1_18/impl/back-or-screen-on.ts
Normal 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);
|
||||
}
|
10
libraries/scrcpy/src/1_18/impl/control-message-types.ts
Normal file
10
libraries/scrcpy/src/1_18/impl/control-message-types.ts
Normal 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;
|
||||
})();
|
11
libraries/scrcpy/src/1_18/impl/defaults.ts
Normal file
11
libraries/scrcpy/src/1_18/impl/defaults.ts
Normal 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>)();
|
10
libraries/scrcpy/src/1_18/impl/index.ts
Normal file
10
libraries/scrcpy/src/1_18/impl/index.ts
Normal 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";
|
24
libraries/scrcpy/src/1_18/impl/init.ts
Normal file
24
libraries/scrcpy/src/1_18/impl/init.ts
Normal 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;
|
||||
}
|
1
libraries/scrcpy/src/1_18/impl/parse-encoder.ts
Normal file
1
libraries/scrcpy/src/1_18/impl/parse-encoder.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export const EncoderRegex = /^\s+scrcpy --encoder '([^']+)'$/;
|
1
libraries/scrcpy/src/1_18/impl/prev.ts
Normal file
1
libraries/scrcpy/src/1_18/impl/prev.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * as PrevImpl from "../../1_17/impl/index.js";
|
8
libraries/scrcpy/src/1_18/impl/serialize-order.ts
Normal file
8
libraries/scrcpy/src/1_18/impl/serialize-order.ts
Normal 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)[])();
|
2
libraries/scrcpy/src/1_18/index.ts
Normal file
2
libraries/scrcpy/src/1_18/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export * as ScrcpyOptions1_18Impl from "./impl/index.js";
|
||||
export * from "./options.js";
|
|
@ -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`", () => {
|
139
libraries/scrcpy/src/1_18/options.ts
Normal file
139
libraries/scrcpy/src/1_18/options.ts
Normal 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_;
|
||||
}
|
4
libraries/scrcpy/src/1_19.ts
Normal file
4
libraries/scrcpy/src/1_19.ts
Normal file
|
@ -0,0 +1,4 @@
|
|||
export {
|
||||
ScrcpyOptions1_18 as ScrcpyOptions1_19,
|
||||
ScrcpyOptions1_18Impl as ScrcpyOptions1_19Impl,
|
||||
} from "./1_18/index.js";
|
4
libraries/scrcpy/src/1_20.ts
Normal file
4
libraries/scrcpy/src/1_20.ts
Normal file
|
@ -0,0 +1,4 @@
|
|||
export {
|
||||
ScrcpyOptions1_18 as ScrcpyOptions1_20,
|
||||
ScrcpyOptions1_18Impl as ScrcpyOptions1_20Impl,
|
||||
} from "./1_18/index.js";
|
8
libraries/scrcpy/src/1_21/impl/defaults.ts
Normal file
8
libraries/scrcpy/src/1_21/impl/defaults.ts
Normal 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>)();
|
10
libraries/scrcpy/src/1_21/impl/index.ts
Normal file
10
libraries/scrcpy/src/1_21/impl/index.ts
Normal 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";
|
5
libraries/scrcpy/src/1_21/impl/init.ts
Normal file
5
libraries/scrcpy/src/1_21/impl/init.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
import type { PrevImpl } from "./prev.js";
|
||||
|
||||
export interface Init extends PrevImpl.Init {
|
||||
clipboardAutosync?: boolean;
|
||||
}
|
1
libraries/scrcpy/src/1_21/impl/parse-encoder.ts
Normal file
1
libraries/scrcpy/src/1_21/impl/parse-encoder.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export const EncoderRegex = /^\s+scrcpy --encoder-name '([^']+)'$/;
|
1
libraries/scrcpy/src/1_21/impl/prev.ts
Normal file
1
libraries/scrcpy/src/1_21/impl/prev.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * as PrevImpl from "../../1_18/impl/index.js";
|
30
libraries/scrcpy/src/1_21/impl/serialize.ts
Normal file
30
libraries/scrcpy/src/1_21/impl/serialize.ts
Normal 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;
|
||||
}
|
80
libraries/scrcpy/src/1_21/impl/set-clipboard.ts
Normal file
80
libraries/scrcpy/src/1_21/impl/set-clipboard.ts
Normal 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,
|
||||
];
|
||||
}
|
||||
}
|
2
libraries/scrcpy/src/1_21/index.ts
Normal file
2
libraries/scrcpy/src/1_21/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export * as ScrcpyOptions1_21Impl from "./impl/index.js";
|
||||
export * from "./options.js";
|
|
@ -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",
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
149
libraries/scrcpy/src/1_21/options.ts
Normal file
149
libraries/scrcpy/src/1_21/options.ts
Normal 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_;
|
||||
}
|
10
libraries/scrcpy/src/1_22/impl/defaults.ts
Normal file
10
libraries/scrcpy/src/1_22/impl/defaults.ts
Normal 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>)();
|
8
libraries/scrcpy/src/1_22/impl/index.ts
Normal file
8
libraries/scrcpy/src/1_22/impl/index.ts
Normal 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";
|
|
@ -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;
|
||||
|
||||
/**
|
|
@ -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);
|
||||
}
|
||||
}
|
1
libraries/scrcpy/src/1_22/impl/prev.ts
Normal file
1
libraries/scrcpy/src/1_22/impl/prev.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * as PrevImpl from "../../1_21/impl/index.js";
|
|
@ -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,
|
30
libraries/scrcpy/src/1_22/impl/scroll-controller.ts
Normal file
30
libraries/scrcpy/src/1_22/impl/scroll-controller.ts
Normal 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);
|
||||
}
|
||||
}
|
2
libraries/scrcpy/src/1_22/index.ts
Normal file
2
libraries/scrcpy/src/1_22/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export * as ScrcpyOptions1_22Impl from "./impl/index.js";
|
||||
export * from "./options.js";
|
|
@ -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";
|
||||
|
149
libraries/scrcpy/src/1_22/options.ts
Normal file
149
libraries/scrcpy/src/1_22/options.ts
Normal 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_;
|
||||
}
|
8
libraries/scrcpy/src/1_23/impl/defaults.ts
Normal file
8
libraries/scrcpy/src/1_23/impl/defaults.ts
Normal 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>)();
|
7
libraries/scrcpy/src/1_23/impl/index.ts
Normal file
7
libraries/scrcpy/src/1_23/impl/index.ts
Normal 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";
|
5
libraries/scrcpy/src/1_23/impl/init.ts
Normal file
5
libraries/scrcpy/src/1_23/impl/init.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
import type { PrevImpl } from "./prev.js";
|
||||
|
||||
export interface Init extends PrevImpl.Init {
|
||||
cleanup?: boolean;
|
||||
}
|
64
libraries/scrcpy/src/1_23/impl/media-stream-transformer.ts
Normal file
64
libraries/scrcpy/src/1_23/impl/media-stream-transformer.ts
Normal 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,
|
||||
});
|
||||
},
|
||||
}),
|
||||
),
|
||||
};
|
||||
}
|
1
libraries/scrcpy/src/1_23/impl/prev.ts
Normal file
1
libraries/scrcpy/src/1_23/impl/prev.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * as PrevImpl from "../../1_22/impl/index.js";
|
2
libraries/scrcpy/src/1_23/index.ts
Normal file
2
libraries/scrcpy/src/1_23/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export * as ScrcpyOptions1_23Impl from "./impl/index.js";
|
||||
export * from "./options.js";
|
149
libraries/scrcpy/src/1_23/options.ts
Normal file
149
libraries/scrcpy/src/1_23/options.ts
Normal 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_;
|
||||
}
|
0
libraries/scrcpy/src/1_24/impl/control-message-types.ts
Normal file
0
libraries/scrcpy/src/1_24/impl/control-message-types.ts
Normal file
8
libraries/scrcpy/src/1_24/impl/defaults.ts
Normal file
8
libraries/scrcpy/src/1_24/impl/defaults.ts
Normal 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>)();
|
3
libraries/scrcpy/src/1_24/impl/index.ts
Normal file
3
libraries/scrcpy/src/1_24/impl/index.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
export * from "../../1_23/impl/index.js";
|
||||
export { Defaults } from "./defaults.js";
|
||||
export type { Init } from "./init.js";
|
5
libraries/scrcpy/src/1_24/impl/init.ts
Normal file
5
libraries/scrcpy/src/1_24/impl/init.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
import type { PrevImpl } from "./prev.js";
|
||||
|
||||
export interface Init extends PrevImpl.Init {
|
||||
powerOn?: boolean;
|
||||
}
|
1
libraries/scrcpy/src/1_24/impl/prev.ts
Normal file
1
libraries/scrcpy/src/1_24/impl/prev.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * as PrevImpl from "../../1_23/impl/index.js";
|
2
libraries/scrcpy/src/1_24/index.ts
Normal file
2
libraries/scrcpy/src/1_24/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export * as ScrcpyOptions1_24Impl from "./impl/index.js";
|
||||
export * from "./options.js";
|
149
libraries/scrcpy/src/1_24/options.ts
Normal file
149
libraries/scrcpy/src/1_24/options.ts
Normal 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_;
|
||||
}
|
6
libraries/scrcpy/src/1_25/impl/index.ts
Normal file
6
libraries/scrcpy/src/1_25/impl/index.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
export * from "../../1_24/impl/index.js";
|
||||
export {
|
||||
InjectScrollControlMessage,
|
||||
ScrollController,
|
||||
SignedFloat,
|
||||
} from "./scroll-controller.js";
|
1
libraries/scrcpy/src/1_25/impl/prev.ts
Normal file
1
libraries/scrcpy/src/1_25/impl/prev.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * as PrevImpl from "../../1_24/impl/index.js";
|
|
@ -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,
|
|
@ -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);
|
||||
}
|
||||
}
|
2
libraries/scrcpy/src/1_25/index.ts
Normal file
2
libraries/scrcpy/src/1_25/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export * as ScrcpyOptions1_25Impl from "./impl/index.js";
|
||||
export * from "./options.js";
|
149
libraries/scrcpy/src/1_25/options.ts
Normal file
149
libraries/scrcpy/src/1_25/options.ts
Normal 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_;
|
||||
}
|
24
libraries/scrcpy/src/2_0/impl/defaults.ts
Normal file
24
libraries/scrcpy/src/2_0/impl/defaults.ts
Normal 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>)();
|
12
libraries/scrcpy/src/2_0/impl/index.ts
Normal file
12
libraries/scrcpy/src/2_0/impl/index.ts
Normal 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";
|
45
libraries/scrcpy/src/2_0/impl/init.ts
Normal file
45
libraries/scrcpy/src/2_0/impl/init.ts
Normal 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;
|
||||
}
|
33
libraries/scrcpy/src/2_0/impl/inject-touch.ts
Normal file
33
libraries/scrcpy/src/2_0/impl/inject-touch.ts
Normal 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);
|
||||
}
|
99
libraries/scrcpy/src/2_0/impl/parse-audio-stream-metadata.ts
Normal file
99
libraries/scrcpy/src/2_0/impl/parse-audio-stream-metadata.ts
Normal 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);
|
||||
}
|
||||
}),
|
||||
};
|
||||
}
|
15
libraries/scrcpy/src/2_0/impl/parse-display.ts
Normal file
15
libraries/scrcpy/src/2_0/impl/parse-display.ts
Normal 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
Loading…
Add table
Add a link
Reference in a new issue