feat(adb-scrcpy): infer type of videoStream from video option

This commit is contained in:
Simon Chan 2025-02-18 15:47:26 +08:00
parent 02f5bd5929
commit 24b65fd2c1
No known key found for this signature in database
GPG key ID: A8B69F750B9BCEDD
61 changed files with 653 additions and 336 deletions

View file

@ -115,7 +115,7 @@
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"explorer.sortOrder": "mixed",
"prettier.prettierPath": "./node_modules/prettier/index.cjs",
"prettier.prettierPath": "./toolchain/eslint-config/node_modules/prettier/index.cjs",
"cSpell.numSuggestions": 4,
"cSpell.ignoreRegExpList": [
"0x[0-9a-f_]+"

View file

@ -29,7 +29,8 @@
"scripts": {
"build": "tsc -b tsconfig.build.json",
"lint": "run-eslint && prettier src/**/*.ts --write --tab-width 4",
"prepublishOnly": "npm run build"
"prepublishOnly": "npm run build",
"test": "run-test"
},
"dependencies": {
"@yume-chan/adb": "workspace:^",
@ -40,7 +41,9 @@
"@yume-chan/struct": "workspace:^"
},
"devDependencies": {
"@types/node": "^22.10.10",
"@yume-chan/eslint-config": "workspace:^",
"@yume-chan/test-runner": "workspace:^",
"@yume-chan/tsconfig": "workspace:^",
"prettier": "^3.4.2",
"typescript": "^5.7.3"

View file

@ -15,7 +15,7 @@ export function createConnection(
adb: Adb,
options: Required<
Pick<
ScrcpyOptions2_1.Init,
ScrcpyOptions2_1.Init<boolean>,
| "tunnelForward"
| "control"
| "sendDummyByte"

View file

@ -10,8 +10,10 @@ import {
import type { AdbScrcpyConnection } from "../connection.js";
import { AdbScrcpyOptions } from "../types.js";
export class AdbScrcpyOptions2_1 extends AdbScrcpyOptions<ScrcpyOptions2_1.Init> {
constructor(init: ScrcpyOptions2_1.Init, version?: string) {
export class AdbScrcpyOptions2_1<
TVideo extends boolean,
> extends AdbScrcpyOptions<ScrcpyOptions2_1.Init<TVideo>> {
constructor(init: ScrcpyOptions2_1.Init<TVideo>, version?: string) {
super(new ScrcpyOptions2_1(init, version));
}
@ -29,5 +31,6 @@ export class AdbScrcpyOptions2_1 extends AdbScrcpyOptions<ScrcpyOptions2_1.Init>
}
export namespace AdbScrcpyOptions2_1 {
export type Init = ScrcpyOptions2_1.Init;
export type Init<TVideo extends boolean = boolean> =
ScrcpyOptions2_1.Init<TVideo>;
}

View file

@ -10,8 +10,10 @@ import {
import type { AdbScrcpyConnection } from "./connection.js";
import { AdbScrcpyOptions } from "./types.js";
export class AdbScrcpyOptions2_1_1 extends AdbScrcpyOptions<ScrcpyOptions2_1_1.Init> {
constructor(init: ScrcpyOptions2_1_1.Init, version?: string) {
export class AdbScrcpyOptions2_1_1<
TVideo extends boolean,
> extends AdbScrcpyOptions<ScrcpyOptions2_1_1.Init<TVideo>> {
constructor(init: ScrcpyOptions2_1_1.Init<TVideo>, version?: string) {
super(new ScrcpyOptions2_1_1(init, version));
}
@ -29,5 +31,6 @@ export class AdbScrcpyOptions2_1_1 extends AdbScrcpyOptions<ScrcpyOptions2_1_1.I
}
export namespace AdbScrcpyOptions2_1_1 {
export type Init = ScrcpyOptions2_1_1.Init;
export type Init<TVideo extends boolean = boolean> =
ScrcpyOptions2_1_1.Init<TVideo>;
}

View file

@ -10,8 +10,10 @@ import {
import type { AdbScrcpyConnection } from "./connection.js";
import { AdbScrcpyOptions } from "./types.js";
export class AdbScrcpyOptions2_2 extends AdbScrcpyOptions<ScrcpyOptions2_2.Init> {
constructor(init: ScrcpyOptions2_2.Init, version?: string) {
export class AdbScrcpyOptions2_2<
TVideo extends boolean,
> extends AdbScrcpyOptions<ScrcpyOptions2_2.Init<TVideo>> {
constructor(init: ScrcpyOptions2_2.Init<TVideo>, version?: string) {
super(new ScrcpyOptions2_2(init, version));
}
@ -29,5 +31,6 @@ export class AdbScrcpyOptions2_2 extends AdbScrcpyOptions<ScrcpyOptions2_2.Init>
}
export namespace AdbScrcpyOptions2_2 {
export type Init = ScrcpyOptions2_2.Init;
export type Init<TVideo extends boolean = boolean> =
ScrcpyOptions2_2.Init<TVideo>;
}

View file

@ -10,8 +10,10 @@ import {
import type { AdbScrcpyConnection } from "./connection.js";
import { AdbScrcpyOptions } from "./types.js";
export class AdbScrcpyOptions2_3 extends AdbScrcpyOptions<ScrcpyOptions2_3.Init> {
constructor(init: ScrcpyOptions2_3.Init, version?: string) {
export class AdbScrcpyOptions2_3<
TVideo extends boolean,
> extends AdbScrcpyOptions<ScrcpyOptions2_3.Init<TVideo>> {
constructor(init: ScrcpyOptions2_3.Init<TVideo>, version?: string) {
super(new ScrcpyOptions2_3(init, version));
}
@ -29,5 +31,6 @@ export class AdbScrcpyOptions2_3 extends AdbScrcpyOptions<ScrcpyOptions2_3.Init>
}
export namespace AdbScrcpyOptions2_3 {
export type Init = ScrcpyOptions2_3.Init;
export type Init<TVideo extends boolean = boolean> =
ScrcpyOptions2_3.Init<TVideo>;
}

View file

@ -10,8 +10,10 @@ import {
import type { AdbScrcpyConnection } from "./connection.js";
import { AdbScrcpyOptions } from "./types.js";
export class AdbScrcpyOptions2_3_1 extends AdbScrcpyOptions<ScrcpyOptions2_3_1.Init> {
constructor(init: ScrcpyOptions2_3_1.Init, version?: string) {
export class AdbScrcpyOptions2_3_1<
TVideo extends boolean,
> extends AdbScrcpyOptions<ScrcpyOptions2_3_1.Init<TVideo>> {
constructor(init: ScrcpyOptions2_3_1.Init<TVideo>, version?: string) {
super(new ScrcpyOptions2_3_1(init, version));
}
@ -29,5 +31,6 @@ export class AdbScrcpyOptions2_3_1 extends AdbScrcpyOptions<ScrcpyOptions2_3_1.I
}
export namespace AdbScrcpyOptions2_3_1 {
export type Init = ScrcpyOptions2_3_1.Init;
export type Init<TVideo extends boolean = boolean> =
ScrcpyOptions2_3_1.Init<TVideo>;
}

View file

@ -10,8 +10,10 @@ import {
import type { AdbScrcpyConnection } from "./connection.js";
import { AdbScrcpyOptions } from "./types.js";
export class AdbScrcpyOptions2_4 extends AdbScrcpyOptions<ScrcpyOptions2_4.Init> {
constructor(init: ScrcpyOptions2_4.Init, version?: string) {
export class AdbScrcpyOptions2_4<
TVideo extends boolean,
> extends AdbScrcpyOptions<ScrcpyOptions2_4.Init<TVideo>> {
constructor(init: ScrcpyOptions2_4.Init<TVideo>, version?: string) {
super(new ScrcpyOptions2_4(init, version));
}
@ -29,5 +31,6 @@ export class AdbScrcpyOptions2_4 extends AdbScrcpyOptions<ScrcpyOptions2_4.Init>
}
export namespace AdbScrcpyOptions2_4 {
export type Init = ScrcpyOptions2_4.Init;
export type Init<TVideo extends boolean = boolean> =
ScrcpyOptions2_4.Init<TVideo>;
}

View file

@ -10,8 +10,10 @@ import {
import type { AdbScrcpyConnection } from "./connection.js";
import { AdbScrcpyOptions } from "./types.js";
export class AdbScrcpyOptions2_5 extends AdbScrcpyOptions<ScrcpyOptions2_5.Init> {
constructor(init: ScrcpyOptions2_5.Init, version?: string) {
export class AdbScrcpyOptions2_5<
TVideo extends boolean,
> extends AdbScrcpyOptions<ScrcpyOptions2_5.Init<TVideo>> {
constructor(init: ScrcpyOptions2_5.Init<TVideo>, version?: string) {
super(new ScrcpyOptions2_5(init, version));
}
@ -29,5 +31,6 @@ export class AdbScrcpyOptions2_5 extends AdbScrcpyOptions<ScrcpyOptions2_5.Init>
}
export namespace AdbScrcpyOptions2_5 {
export type Init = ScrcpyOptions2_5.Init;
export type Init<TVideo extends boolean = boolean> =
ScrcpyOptions2_5.Init<TVideo>;
}

View file

@ -10,8 +10,10 @@ import {
import type { AdbScrcpyConnection } from "./connection.js";
import { AdbScrcpyOptions } from "./types.js";
export class AdbScrcpyOptions2_6 extends AdbScrcpyOptions<ScrcpyOptions2_6.Init> {
constructor(init: ScrcpyOptions2_6.Init, version?: string) {
export class AdbScrcpyOptions2_6<
TVideo extends boolean,
> extends AdbScrcpyOptions<ScrcpyOptions2_6.Init<TVideo>> {
constructor(init: ScrcpyOptions2_6.Init<TVideo>, version?: string) {
super(new ScrcpyOptions2_6(init, version));
}
@ -29,5 +31,6 @@ export class AdbScrcpyOptions2_6 extends AdbScrcpyOptions<ScrcpyOptions2_6.Init>
}
export namespace AdbScrcpyOptions2_6 {
export type Init = ScrcpyOptions2_6.Init;
export type Init<TVideo extends boolean = boolean> =
ScrcpyOptions2_6.Init<TVideo>;
}

View file

@ -10,8 +10,10 @@ import {
import type { AdbScrcpyConnection } from "./connection.js";
import { AdbScrcpyOptions } from "./types.js";
export class AdbScrcpyOptions2_7 extends AdbScrcpyOptions<ScrcpyOptions2_7.Init> {
constructor(init: ScrcpyOptions2_7.Init, version?: string) {
export class AdbScrcpyOptions2_7<
TVideo extends boolean,
> extends AdbScrcpyOptions<ScrcpyOptions2_7.Init<TVideo>> {
constructor(init: ScrcpyOptions2_7.Init<TVideo>, version?: string) {
super(new ScrcpyOptions2_7(init, version));
}
@ -29,5 +31,6 @@ export class AdbScrcpyOptions2_7 extends AdbScrcpyOptions<ScrcpyOptions2_7.Init>
}
export namespace AdbScrcpyOptions2_7 {
export type Init = ScrcpyOptions2_7.Init;
export type Init<TVideo extends boolean = boolean> =
ScrcpyOptions2_7.Init<TVideo>;
}

View file

@ -10,8 +10,10 @@ import {
import type { AdbScrcpyConnection } from "./connection.js";
import { AdbScrcpyOptions } from "./types.js";
export class AdbScrcpyOptions3_0 extends AdbScrcpyOptions<ScrcpyOptions3_0.Init> {
constructor(init: ScrcpyOptions3_0.Init, version?: string) {
export class AdbScrcpyOptions3_0<
TVideo extends boolean,
> extends AdbScrcpyOptions<ScrcpyOptions3_0.Init<TVideo>> {
constructor(init: ScrcpyOptions3_0.Init<TVideo>, version?: string) {
super(new ScrcpyOptions3_0(init, version));
}
@ -29,5 +31,6 @@ export class AdbScrcpyOptions3_0 extends AdbScrcpyOptions<ScrcpyOptions3_0.Init>
}
export namespace AdbScrcpyOptions3_0 {
export type Init = ScrcpyOptions3_0.Init;
export type Init<TVideo extends boolean = boolean> =
ScrcpyOptions3_0.Init<TVideo>;
}

View file

@ -10,8 +10,10 @@ import {
import type { AdbScrcpyConnection } from "./connection.js";
import { AdbScrcpyOptions } from "./types.js";
export class AdbScrcpyOptions3_0_1 extends AdbScrcpyOptions<ScrcpyOptions3_0_1.Init> {
constructor(init: ScrcpyOptions3_0_1.Init, version?: string) {
export class AdbScrcpyOptions3_0_1<
TVideo extends boolean,
> extends AdbScrcpyOptions<ScrcpyOptions3_0_1.Init<TVideo>> {
constructor(init: ScrcpyOptions3_0_1.Init<TVideo>, version?: string) {
super(new ScrcpyOptions3_0_1(init, version));
}
@ -29,5 +31,6 @@ export class AdbScrcpyOptions3_0_1 extends AdbScrcpyOptions<ScrcpyOptions3_0_1.I
}
export namespace AdbScrcpyOptions3_0_1 {
export type Init = ScrcpyOptions3_0_1.Init;
export type Init<TVideo extends boolean = boolean> =
ScrcpyOptions3_0_1.Init<TVideo>;
}

View file

@ -10,8 +10,10 @@ import {
import type { AdbScrcpyConnection } from "./connection.js";
import { AdbScrcpyOptions } from "./types.js";
export class AdbScrcpyOptions3_0_2 extends AdbScrcpyOptions<ScrcpyOptions3_0_2.Init> {
constructor(init: ScrcpyOptions3_0_2.Init, version?: string) {
export class AdbScrcpyOptions3_0_2<
TVideo extends boolean,
> extends AdbScrcpyOptions<ScrcpyOptions3_0_2.Init<TVideo>> {
constructor(init: ScrcpyOptions3_0_2.Init<TVideo>, version?: string) {
super(new ScrcpyOptions3_0_2(init, version));
}
@ -29,5 +31,6 @@ export class AdbScrcpyOptions3_0_2 extends AdbScrcpyOptions<ScrcpyOptions3_0_2.I
}
export namespace AdbScrcpyOptions3_0_2 {
export type Init = ScrcpyOptions3_0_2.Init;
export type Init<TVideo extends boolean = boolean> =
ScrcpyOptions3_0_2.Init<TVideo>;
}

View file

@ -10,8 +10,10 @@ import {
import type { AdbScrcpyConnection } from "./connection.js";
import { AdbScrcpyOptions } from "./types.js";
export class AdbScrcpyOptions3_1 extends AdbScrcpyOptions<ScrcpyOptions3_1.Init> {
constructor(init: ScrcpyOptions3_1.Init, version?: string) {
export class AdbScrcpyOptions3_1<
TVideo extends boolean,
> extends AdbScrcpyOptions<ScrcpyOptions3_1.Init<TVideo>> {
constructor(init: ScrcpyOptions3_1.Init<TVideo>, version?: string) {
super(new ScrcpyOptions3_1(init, version));
}
@ -29,5 +31,6 @@ export class AdbScrcpyOptions3_1 extends AdbScrcpyOptions<ScrcpyOptions3_1.Init>
}
export namespace AdbScrcpyOptions3_1 {
export type Init = ScrcpyOptions3_1.Init;
export type Init<TVideo extends boolean = boolean> =
ScrcpyOptions3_1.Init<TVideo>;
}

View file

@ -0,0 +1,154 @@
import { describe, it } from "node:test";
import type { Adb } from "@yume-chan/adb";
import { DefaultServerPath } from "@yume-chan/scrcpy";
import { AdbScrcpyOptions1_15 } from "./1_15/options.js";
import { AdbScrcpyOptions2_0 } from "./2_0/options.js";
import { AdbScrcpyOptions2_1 } from "./2_1/options.js";
import { AdbScrcpyOptions3_1 } from "./3_1.js";
import { AdbScrcpyClient } from "./client.js";
import type { AdbScrcpyVideoStream } from "./video.js";
const TypeOnlyTest = false;
declare const adb: Adb;
function expect(value: true): void {
void value;
}
function equal<X>(): <Y>(
value: Y,
) => (<T>() => T extends X ? 1 : 2) extends <T>() => T extends Y ? 1 : 2
? true
: false {
return (() => {}) as never;
}
describe("AdbScrcpyClient", () => {
describe("videoStream", () => {
it("should have value in lower versions", async () => {
if (TypeOnlyTest) {
const client = await AdbScrcpyClient.start(
adb,
DefaultServerPath,
new AdbScrcpyOptions1_15({}),
);
expect(
equal<Promise<AdbScrcpyVideoStream>>()(client.videoStream),
);
}
if (TypeOnlyTest) {
const client = await AdbScrcpyClient.start(
adb,
DefaultServerPath,
new AdbScrcpyOptions2_0({}),
);
expect(
equal<Promise<AdbScrcpyVideoStream>>()(client.videoStream),
);
}
});
it("should have value when video: true", async () => {
if (TypeOnlyTest) {
const client = await AdbScrcpyClient.start(
adb,
DefaultServerPath,
new AdbScrcpyOptions2_1({ video: true }),
);
expect(
equal<Promise<AdbScrcpyVideoStream>>()(client.videoStream),
);
}
if (TypeOnlyTest) {
const client = await AdbScrcpyClient.start(
adb,
DefaultServerPath,
new AdbScrcpyOptions3_1({ video: true }),
);
expect(
equal<Promise<AdbScrcpyVideoStream>>()(client.videoStream),
);
}
});
it("should be undefined when video: false", async () => {
if (TypeOnlyTest) {
const client = await AdbScrcpyClient.start(
adb,
DefaultServerPath,
new AdbScrcpyOptions2_1({ video: false }),
);
expect(equal<undefined>()(client.videoStream));
}
if (TypeOnlyTest) {
const client = await AdbScrcpyClient.start(
adb,
DefaultServerPath,
new AdbScrcpyOptions3_1({ video: false }),
);
expect(equal<undefined>()(client.videoStream));
}
});
it("should be a union when video: undefined", async () => {
if (TypeOnlyTest) {
const client = await AdbScrcpyClient.start(
adb,
DefaultServerPath,
new AdbScrcpyOptions2_1({}),
);
expect(
equal<Promise<AdbScrcpyVideoStream> | undefined>()(
client.videoStream,
),
);
}
if (TypeOnlyTest) {
const client = await AdbScrcpyClient.start(
adb,
DefaultServerPath,
new AdbScrcpyOptions3_1({}),
);
expect(
equal<Promise<AdbScrcpyVideoStream> | undefined>()(
client.videoStream,
),
);
}
});
it("should be a union when video: boolean", async () => {
if (TypeOnlyTest) {
const client = await AdbScrcpyClient.start(
adb,
DefaultServerPath,
new AdbScrcpyOptions2_1({ video: true as boolean }),
);
expect(
equal<Promise<AdbScrcpyVideoStream> | undefined>()(
client.videoStream,
),
);
}
if (TypeOnlyTest) {
const client = await AdbScrcpyClient.start(
adb,
DefaultServerPath,
new AdbScrcpyOptions3_1({ video: true as boolean }),
);
expect(
equal<Promise<AdbScrcpyVideoStream> | undefined>()(
client.videoStream,
),
);
}
});
});
});

View file

@ -11,15 +11,10 @@ import type {
ScrcpyEncoder,
ScrcpyMediaStreamPacket,
ScrcpyOptions1_15,
ScrcpyVideoStreamMetadata,
} from "@yume-chan/scrcpy";
import {
Av1,
DefaultServerPath,
ScrcpyControlMessageWriter,
ScrcpyVideoCodecId,
h264ParseConfiguration,
h265ParseConfiguration,
} from "@yume-chan/scrcpy";
import type {
Consumable,
@ -30,7 +25,6 @@ import type {
import {
AbortController,
BufferedReadableStream,
InspectStream,
PushReadableStream,
SplitStringStream,
TextDecoderStream,
@ -40,6 +34,7 @@ import { ExactReadableEndedError } from "@yume-chan/struct";
import type { AdbScrcpyConnection } from "./connection.js";
import type { AdbScrcpyOptions } from "./types.js";
import { AdbScrcpyVideoStream } from "./video.js";
function arrayToStream<T>(array: T[]): ReadableStream<T> {
return new PushReadableStream(async (controller) => {
@ -73,8 +68,8 @@ export class AdbScrcpyExitedError extends Error {
}
}
interface AdbScrcpyClientInit {
options: AdbScrcpyOptions<object>;
interface AdbScrcpyClientInit<TOptions extends AdbScrcpyOptions<object>> {
options: TOptions;
process: AdbSubprocessProtocol;
stdout: ReadableStream<string>;
@ -85,11 +80,6 @@ interface AdbScrcpyClientInit {
| undefined;
}
export interface AdbScrcpyVideoStream {
stream: ReadableStream<ScrcpyMediaStreamPacket>;
metadata: ScrcpyVideoStreamMetadata;
}
export interface AdbScrcpyAudioStreamSuccessMetadata
extends Omit<ScrcpyAudioStreamSuccessMetadata, "stream"> {
readonly stream: ReadableStream<ScrcpyMediaStreamPacket>;
@ -100,7 +90,7 @@ export type AdbScrcpyAudioStreamMetadata =
| ScrcpyAudioStreamErroredMetadata
| AdbScrcpyAudioStreamSuccessMetadata;
export class AdbScrcpyClient {
export class AdbScrcpyClient<TOptions extends AdbScrcpyOptions<object>> {
static async pushServer(
adb: Adb,
file: ReadableStream<MaybeConsumable<Uint8Array>>,
@ -117,13 +107,15 @@ export class AdbScrcpyClient {
}
}
static async start(
adb: Adb,
path: string,
options: AdbScrcpyOptions<
static async start<
TOptions extends AdbScrcpyOptions<
Pick<ScrcpyOptions1_15.Init, "tunnelForward">
>,
) {
>(
adb: Adb,
path: string,
options: TOptions,
): Promise<AdbScrcpyClient<TOptions>> {
let connection: AdbScrcpyConnection | undefined;
let process: AdbSubprocessProtocol | undefined;
@ -239,7 +231,7 @@ export class AdbScrcpyClient {
return options.getDisplays(adb, path);
}
#options: AdbScrcpyOptions<object>;
#options: TOptions;
#process: AdbSubprocessProtocol;
#stdout: ReadableStream<string>;
@ -251,16 +243,6 @@ export class AdbScrcpyClient {
return this.#process.exit;
}
#screenWidth: number | undefined;
get screenWidth() {
return this.#screenWidth;
}
#screenHeight: number | undefined;
get screenHeight() {
return this.#screenHeight;
}
#videoStream: Promise<AdbScrcpyVideoStream> | undefined;
/**
* Gets a `Promise` that resolves to the parsed video stream.
@ -271,8 +253,12 @@ export class AdbScrcpyClient {
* Note: if it's not `undefined`, it must be consumed to prevent
* the connection from being blocked.
*/
get videoStream() {
return this.#videoStream;
get videoStream(): TOptions["value"] extends { video: infer T }
? T extends false
? undefined
: Promise<AdbScrcpyVideoStream>
: Promise<AdbScrcpyVideoStream> {
return this.#videoStream as never;
}
#audioStream: Promise<AdbScrcpyAudioStreamMetadata> | undefined;
@ -312,7 +298,7 @@ export class AdbScrcpyClient {
videoStream,
audioStream,
controlStream,
}: AdbScrcpyClientInit) {
}: AdbScrcpyClientInit<TOptions>) {
this.#options = options;
this.#process = process;
this.#stdout = stdout;
@ -358,65 +344,10 @@ export class AdbScrcpyClient {
}
}
#configureH264(data: Uint8Array) {
const { croppedWidth, croppedHeight } = h264ParseConfiguration(data);
this.#screenWidth = croppedWidth;
this.#screenHeight = croppedHeight;
}
#configureH265(data: Uint8Array) {
const { croppedWidth, croppedHeight } = h265ParseConfiguration(data);
this.#screenWidth = croppedWidth;
this.#screenHeight = croppedHeight;
}
#configureAv1(data: Uint8Array) {
const parser = new Av1(data);
const sequenceHeader = parser.searchSequenceHeaderObu();
if (!sequenceHeader) {
return;
}
const { max_frame_width_minus_1, max_frame_height_minus_1 } =
sequenceHeader;
const width = max_frame_width_minus_1 + 1;
const height = max_frame_height_minus_1 + 1;
this.#screenWidth = width;
this.#screenHeight = height;
}
async #createVideoStream(initialStream: ReadableStream<Uint8Array>) {
const { stream, metadata } =
const { metadata, stream } =
await this.#options.parseVideoStreamMetadata(initialStream);
return {
stream: stream
.pipeThrough(this.#options.createMediaStreamTransformer())
.pipeThrough(
new InspectStream((packet) => {
if (packet.type === "configuration") {
switch (metadata.codec) {
case ScrcpyVideoCodecId.H264:
this.#configureH264(packet.data);
break;
case ScrcpyVideoCodecId.H265:
this.#configureH265(packet.data);
break;
case ScrcpyVideoCodecId.AV1:
// AV1 configuration is in normal stream
break;
}
} else if (metadata.codec === ScrcpyVideoCodecId.AV1) {
this.#configureAv1(packet.data);
}
}),
),
metadata,
};
return new AdbScrcpyVideoStream(this.#options, metadata, stream);
}
async #createAudioStream(

View file

@ -1,11 +1,14 @@
import { AdbScrcpyOptions3_1 } from "./3_1.js";
export class AdbScrcpyOptionsLatest extends AdbScrcpyOptions3_1 {
constructor(init: AdbScrcpyOptions3_1.Init, version: string) {
export class AdbScrcpyOptionsLatest<
TVideo extends boolean,
> extends AdbScrcpyOptions3_1<TVideo> {
constructor(init: AdbScrcpyOptions3_1.Init<TVideo>, version: string) {
super(init, version);
}
}
export namespace AdbScrcpyOptionsLatest {
export type Init = AdbScrcpyOptions3_1.Init;
export type Init<TVideo extends boolean = boolean> =
AdbScrcpyOptions3_1.Init<TVideo>;
}

View file

@ -0,0 +1,108 @@
import { EventEmitter } from "@yume-chan/event";
import type {
ScrcpyMediaStreamPacket,
ScrcpyVideoStreamMetadata,
} from "@yume-chan/scrcpy";
import {
Av1,
h264ParseConfiguration,
h265ParseConfiguration,
ScrcpyVideoCodecId,
} from "@yume-chan/scrcpy";
import type { ReadableStream } from "@yume-chan/stream-extra";
import { InspectStream } from "@yume-chan/stream-extra";
import type { AdbScrcpyOptions } from "./types.js";
export class AdbScrcpyVideoStream {
#options: AdbScrcpyOptions<object>;
#metadata: ScrcpyVideoStreamMetadata;
get metadata(): ScrcpyVideoStreamMetadata {
return this.#metadata;
}
#stream: ReadableStream<ScrcpyMediaStreamPacket>;
get stream(): ReadableStream<ScrcpyMediaStreamPacket> {
return this.#stream;
}
#sizeChanged = new EventEmitter<{ width: number; height: number }>();
get sizeChanged() {
return this.#sizeChanged.event;
}
#width: number = 0;
get width() {
return this.#width;
}
#height: number = 0;
get height() {
return this.#height;
}
constructor(
options: AdbScrcpyOptions<object>,
metadata: ScrcpyVideoStreamMetadata,
stream: ReadableStream<Uint8Array>,
) {
this.#options = options;
this.#metadata = metadata;
this.#stream = stream
.pipeThrough(this.#options.createMediaStreamTransformer())
.pipeThrough(
new InspectStream((packet) => {
if (packet.type === "configuration") {
switch (metadata.codec) {
case ScrcpyVideoCodecId.H264:
this.#configureH264(packet.data);
break;
case ScrcpyVideoCodecId.H265:
this.#configureH265(packet.data);
break;
case ScrcpyVideoCodecId.AV1:
// AV1 configuration is in data packet
break;
}
} else if (metadata.codec === ScrcpyVideoCodecId.AV1) {
this.#configureAv1(packet.data);
}
}),
);
}
#configureH264(data: Uint8Array) {
const { croppedWidth, croppedHeight } = h264ParseConfiguration(data);
this.#width = croppedWidth;
this.#height = croppedHeight;
this.#sizeChanged.fire({ width: croppedWidth, height: croppedHeight });
}
#configureH265(data: Uint8Array) {
const { croppedWidth, croppedHeight } = h265ParseConfiguration(data);
this.#width = croppedWidth;
this.#height = croppedHeight;
this.#sizeChanged.fire({ width: croppedWidth, height: croppedHeight });
}
#configureAv1(data: Uint8Array) {
const parser = new Av1(data);
const sequenceHeader = parser.searchSequenceHeaderObu();
if (!sequenceHeader) {
return;
}
const { max_frame_width_minus_1, max_frame_height_minus_1 } =
sequenceHeader;
const width = max_frame_width_minus_1 + 1;
const height = max_frame_height_minus_1 + 1;
this.#width = width;
this.#height = height;
this.#sizeChanged.fire({ width, height });
}
}

View file

@ -2,6 +2,7 @@
"extends": "./tsconfig.build.json",
"compilerOptions": {
"types": [
"node"
],
},
"exclude": []

View file

@ -50,6 +50,16 @@ export class TinyH264Decoder implements ScrcpyVideoDecoder {
return this.#sizeChanged.event;
}
#width: number = 0;
get width() {
return this.#width;
}
#height: number = 0;
get height() {
return this.#height;
}
#frameRendered = 0;
get framesRendered() {
return this.#frameRendered;
@ -124,12 +134,16 @@ export class TinyH264Decoder implements ScrcpyVideoDecoder {
cropLeft,
cropTop,
} = h264ParseConfiguration(data);
this.#width = croppedWidth;
this.#height = croppedHeight;
this.#sizeChanged.fire({
width: croppedWidth,
height: croppedHeight,
});
// H.264 Baseline profile only supports YUV 420 pixel format
// So chroma width/height is each half of video width/height
const chromaWidth = encodedWidth / 2;
const chromaHeight = encodedHeight / 2;

View file

@ -12,8 +12,12 @@ export interface ScrcpyVideoDecoderCapability {
export interface ScrcpyVideoDecoder extends Disposable {
readonly sizeChanged: Event<{ width: number; height: number }>;
readonly width: number;
readonly height: number;
readonly framesRendered: number;
readonly framesSkipped: number;
readonly writable: WritableStream<ScrcpyMediaStreamPacket>;
}

View file

@ -10,87 +10,9 @@ import { WritableStream } from "@yume-chan/stream-extra";
import { Av1Codec, H264Decoder, H265Decoder } from "./codec/index.js";
import type { CodecDecoder } from "./codec/type.js";
import { Pool } from "./pool.js";
import type { VideoFrameRenderer } from "./render/index.js";
class Pool<T> {
#controller!: ReadableStreamDefaultController<T>;
#readable = new ReadableStream<T>(
{
start: (controller) => {
this.#controller = controller;
},
pull: (controller) => {
controller.enqueue(this.#initializer());
},
},
{ highWaterMark: 0 },
);
#reader = this.#readable.getReader();
#initializer: () => T;
#size = 0;
#capacity: number;
constructor(initializer: () => T, capacity: number) {
this.#initializer = initializer;
this.#capacity = capacity;
}
async borrow() {
const result = await this.#reader.read();
return result.value!;
}
return(value: T) {
if (this.#size < this.#capacity) {
this.#controller.enqueue(value);
this.#size += 1;
}
}
}
class VideoFrameCapturer {
#canvas: OffscreenCanvas | HTMLCanvasElement;
#context: ImageBitmapRenderingContext;
constructor() {
if (typeof OffscreenCanvas !== "undefined") {
this.#canvas = new OffscreenCanvas(1, 1);
} else {
this.#canvas = document.createElement("canvas");
this.#canvas.width = 1;
this.#canvas.height = 1;
}
this.#context = this.#canvas.getContext("bitmaprenderer", {
alpha: false,
})!;
}
async capture(frame: VideoFrame): Promise<Blob> {
this.#canvas.width = frame.displayWidth;
this.#canvas.height = frame.displayHeight;
const bitmap = await createImageBitmap(frame);
this.#context.transferFromImageBitmap(bitmap);
if (this.#canvas instanceof OffscreenCanvas) {
return await this.#canvas.convertToBlob({
type: "image/png",
});
} else {
return new Promise((resolve, reject) => {
(this.#canvas as HTMLCanvasElement).toBlob((blob) => {
if (!blob) {
reject(new Error("Failed to convert canvas to blob"));
} else {
resolve(blob);
}
}, "image/png");
});
}
}
}
import { VideoFrameCapturer } from "./snapshot.js";
const VideoFrameCapturerPool =
/* #__PURE__ */
@ -144,6 +66,16 @@ export class WebCodecsVideoDecoder implements ScrcpyVideoDecoder {
return this.#sizeChanged.event;
}
#width: number = 0;
get width() {
return this.#width;
}
#height: number = 0;
get height() {
return this.#height;
}
#decoder: VideoDecoder;
#drawing = false;
@ -218,7 +150,7 @@ export class WebCodecsVideoDecoder implements ScrcpyVideoDecoder {
},
});
this.#onVerticalSync();
this.#handleAnimationFrame();
}
#setError(error: Error) {
@ -261,16 +193,20 @@ export class WebCodecsVideoDecoder implements ScrcpyVideoDecoder {
#updateSize = (width: number, height: number) => {
this.#renderer.setSize(width, height);
this.#width = width;
this.#height = height;
this.#sizeChanged.fire({ width, height });
};
#onVerticalSync = () => {
#handleAnimationFrame = () => {
if (this.#framesDraw > 0) {
this.#framesPresented += 1;
this.#framesSkipped += this.#framesDraw - 1;
this.#framesDraw = 0;
}
this.#animationFrameId = requestAnimationFrame(this.#onVerticalSync);
this.#animationFrameId = requestAnimationFrame(
this.#handleAnimationFrame,
);
};
async snapshot() {

View file

@ -0,0 +1,37 @@
export class Pool<T> {
#controller!: ReadableStreamDefaultController<T>;
#readable = new ReadableStream<T>(
{
start: (controller) => {
this.#controller = controller;
},
pull: (controller) => {
controller.enqueue(this.#initializer());
},
},
{ highWaterMark: 0 },
);
#reader = this.#readable.getReader();
#initializer: () => T;
#size = 0;
#capacity: number;
constructor(initializer: () => T, capacity: number) {
this.#initializer = initializer;
this.#capacity = capacity;
}
async borrow() {
const result = await this.#reader.read();
return result.value!;
}
return(value: T) {
if (this.#size < this.#capacity) {
this.#controller.enqueue(value);
this.#size += 1;
}
}
}

View file

@ -0,0 +1,41 @@
export class VideoFrameCapturer {
#canvas: OffscreenCanvas | HTMLCanvasElement;
#context: ImageBitmapRenderingContext;
constructor() {
if (typeof OffscreenCanvas !== "undefined") {
this.#canvas = new OffscreenCanvas(1, 1);
} else {
this.#canvas = document.createElement("canvas");
this.#canvas.width = 1;
this.#canvas.height = 1;
}
this.#context = this.#canvas.getContext("bitmaprenderer", {
alpha: false,
})!;
}
async capture(frame: VideoFrame): Promise<Blob> {
this.#canvas.width = frame.displayWidth;
this.#canvas.height = frame.displayHeight;
const bitmap = await createImageBitmap(frame);
this.#context.transferFromImageBitmap(bitmap);
if (this.#canvas instanceof OffscreenCanvas) {
return await this.#canvas.convertToBlob({
type: "image/png",
});
} else {
return new Promise((resolve, reject) => {
(this.#canvas as HTMLCanvasElement).toBlob((blob) => {
if (!blob) {
reject(new Error("Failed to convert canvas to blob"));
} else {
resolve(blob);
}
}, "image/png");
});
}
}
}

View file

@ -36,8 +36,8 @@ export const InjectTouchControlMessage = struct(
pointerId: u64,
pointerX: u32,
pointerY: u32,
screenWidth: u16,
screenHeight: u16,
videoWidth: u16,
videoHeight: u16,
pressure: UnsignedFloat,
buttons: u32,
},

View file

@ -12,8 +12,8 @@ describe("ScrollController", () => {
type: ScrcpyControlMessageType.InjectScroll,
pointerX: 0,
pointerY: 0,
screenWidth: 0,
screenHeight: 0,
videoWidth: 0,
videoHeight: 0,
scrollX: 0.5,
scrollY: 0.5,
buttons: 0,
@ -27,8 +27,8 @@ describe("ScrollController", () => {
type: ScrcpyControlMessageType.InjectScroll,
pointerX: 0,
pointerY: 0,
screenWidth: 0,
screenHeight: 0,
videoWidth: 0,
videoHeight: 0,
scrollX: 1.5,
scrollY: 1.5,
buttons: 0,
@ -43,8 +43,8 @@ describe("ScrollController", () => {
type: ScrcpyControlMessageType.InjectScroll,
pointerX: 0,
pointerY: 0,
screenWidth: 0,
screenHeight: 0,
videoWidth: 0,
videoHeight: 0,
scrollX: 0.5,
scrollY: 0.5,
buttons: 0,
@ -53,8 +53,8 @@ describe("ScrollController", () => {
type: ScrcpyControlMessageType.InjectScroll,
pointerX: 0,
pointerY: 0,
screenWidth: 0,
screenHeight: 0,
videoWidth: 0,
videoHeight: 0,
scrollX: 0.5,
scrollY: 0.5,
buttons: 0,
@ -69,8 +69,8 @@ describe("ScrollController", () => {
type: ScrcpyControlMessageType.InjectScroll,
pointerX: 0,
pointerY: 0,
screenWidth: 0,
screenHeight: 0,
videoWidth: 0,
videoHeight: 0,
scrollX: -0.5,
scrollY: -0.5,
buttons: 0,
@ -79,8 +79,8 @@ describe("ScrollController", () => {
type: ScrcpyControlMessageType.InjectScroll,
pointerX: 0,
pointerY: 0,
screenWidth: 0,
screenHeight: 0,
videoWidth: 0,
videoHeight: 0,
scrollX: -0.5,
scrollY: -0.5,
buttons: 0,

View file

@ -9,8 +9,8 @@ export const InjectScrollControlMessage = struct(
type: u8,
pointerX: u32,
pointerY: u32,
screenWidth: u16,
screenHeight: u16,
videoWidth: u16,
videoHeight: u16,
scrollX: s32,
scrollY: s32,
},

View file

@ -12,8 +12,8 @@ describe("ScrollController", () => {
type: ScrcpyControlMessageType.InjectScroll,
pointerX: 0,
pointerY: 0,
screenWidth: 0,
screenHeight: 0,
videoWidth: 0,
videoHeight: 0,
scrollX: 1.5,
scrollY: 1.5,
buttons: 0,

View file

@ -102,8 +102,8 @@ describe("ScrollController", () => {
type: ScrcpyControlMessageType.InjectScroll,
pointerX: 0,
pointerY: 0,
screenWidth: 0,
screenHeight: 0,
videoWidth: 0,
videoHeight: 0,
scrollX: 0.5,
scrollY: 0.5,
buttons: 0,
@ -115,8 +115,8 @@ describe("ScrollController", () => {
type: ScrcpyControlMessageType.InjectScroll,
pointerX: 0,
pointerY: 0,
screenWidth: 0,
screenHeight: 0,
videoWidth: 0,
videoHeight: 0,
scrollX: 1.5,
scrollY: 1.5,
buttons: 0,

View file

@ -29,8 +29,8 @@ export const InjectScrollControlMessage = /* #__PURE__ */ (() =>
type: u8(ScrcpyControlMessageType.InjectScroll),
pointerX: u32,
pointerY: u32,
screenWidth: u16,
screenHeight: u16,
videoWidth: u16,
videoHeight: u16,
scrollX: SignedFloat,
scrollY: SignedFloat,
buttons: u32,

View file

@ -15,8 +15,8 @@ export const InjectTouchControlMessage = /* #__PURE__ */ (() =>
pointerId: u64,
pointerX: u32,
pointerY: u32,
screenWidth: u16,
screenHeight: u16,
videoWidth: u16,
videoHeight: u16,
pressure: PrevImpl.UnsignedFloat,
actionButton: u32,
buttons: u32,

View file

@ -11,7 +11,7 @@ import { ScrcpyAudioCodec } from "../../base/index.js";
export async function parseAudioStreamMetadata(
stream: ReadableStream<Uint8Array>,
options: Pick<Required<Init>, "sendCodecMeta" | "audioCodec">,
options: Pick<Required<Init<boolean>>, "sendCodecMeta" | "audioCodec">,
): Promise<ScrcpyAudioStreamMetadata> {
const buffered = new BufferedReadableStream(stream);

View file

@ -6,4 +6,4 @@ export const Defaults = /* #__PURE__ */ (() =>
...PrevImpl.Defaults,
video: true,
audioSource: "output",
}) as const satisfies Required<Init>)();
}) as const satisfies Required<Init<true>>)();

View file

@ -1,6 +1,6 @@
import type { PrevImpl } from "./prev.js";
export interface Init extends PrevImpl.Init {
video?: boolean;
export interface Init<TVideo extends boolean> extends PrevImpl.Init {
video?: TVideo;
audioSource?: "output" | "mic";
}

View file

@ -37,12 +37,14 @@ import {
setListEncoders,
} from "./impl/index.js";
export class ScrcpyOptions2_1 implements ScrcpyOptions<Init> {
export class ScrcpyOptions2_1<TVideo extends boolean>
implements ScrcpyOptions<Init<TVideo>>
{
static readonly Defaults = Defaults;
readonly version: string;
readonly value: Required<Init>;
readonly value: Required<Init<TVideo>>;
get controlMessageTypes(): readonly ScrcpyControlMessageType[] {
return ControlMessageTypes;
@ -55,8 +57,8 @@ export class ScrcpyOptions2_1 implements ScrcpyOptions<Init> {
#ackClipboardHandler: AckClipboardHandler | undefined;
constructor(init: Init, version = "2.1") {
this.value = { ...Defaults, ...init };
constructor(init: Init<TVideo>, version = "2.1") {
this.value = { ...Defaults, ...init } as never;
this.version = version;
if (this.value.control && this.value.clipboardAutosync) {
@ -66,7 +68,7 @@ export class ScrcpyOptions2_1 implements ScrcpyOptions<Init> {
}
serialize(): string[] {
return serialize(this.value, Defaults);
return serialize<Init<boolean>>(this.value, Defaults);
}
setListDisplays(): void {
@ -154,8 +156,8 @@ export class ScrcpyOptions2_1 implements ScrcpyOptions<Init> {
}
}
type Init_ = Init;
type Init_<TVideo extends boolean> = Init<TVideo>;
export namespace ScrcpyOptions2_1 {
export type Init = Init_;
export type Init<TVideo extends boolean = boolean> = Init_<TVideo>;
}

View file

@ -1,11 +1,14 @@
import { ScrcpyOptions2_1 } from "./2_1/index.js";
export class ScrcpyOptions2_1_1 extends ScrcpyOptions2_1 {
constructor(init: ScrcpyOptions2_1.Init, version = "2.1.1") {
export class ScrcpyOptions2_1_1<
TVideo extends boolean,
> extends ScrcpyOptions2_1<TVideo> {
constructor(init: ScrcpyOptions2_1.Init<TVideo>, version = "2.1.1") {
super(init, version);
}
}
export namespace ScrcpyOptions2_1_1 {
export type Init = ScrcpyOptions2_1.Init;
export type Init<TVideo extends boolean = boolean> =
ScrcpyOptions2_1.Init<TVideo>;
}

View file

@ -14,4 +14,4 @@ export const Defaults = /* #__PURE__ */ (() =>
cameraHighSpeed: false,
listCameras: false,
listCameraSizes: false,
}) as const satisfies Required<Init>)();
}) as const satisfies Required<Init<true>>)();

View file

@ -1,6 +1,6 @@
import type { PrevImpl } from "./prev.js";
export interface Init extends PrevImpl.Init {
export interface Init<TVideo extends boolean> extends PrevImpl.Init<TVideo> {
videoSource?: "display" | "camera";
cameraId?: string | undefined;
cameraSize?: string | undefined;

View file

@ -37,12 +37,14 @@ import {
setListEncoders,
} from "./impl/index.js";
export class ScrcpyOptions2_2 implements ScrcpyOptions<Init> {
export class ScrcpyOptions2_2<TVideo extends boolean>
implements ScrcpyOptions<Init<TVideo>>
{
static readonly Defaults = Defaults;
readonly version: string;
readonly value: Required<Init>;
readonly value: Required<Init<TVideo>>;
get controlMessageTypes(): readonly ScrcpyControlMessageType[] {
return ControlMessageTypes;
@ -55,8 +57,8 @@ export class ScrcpyOptions2_2 implements ScrcpyOptions<Init> {
#ackClipboardHandler: AckClipboardHandler | undefined;
constructor(init: Init, version = "v2.2") {
this.value = { ...Defaults, ...init };
constructor(init: Init<TVideo>, version = "v2.2") {
this.value = { ...Defaults, ...init } as never;
this.version = version;
if (this.value.videoSource === "camera") {
@ -70,7 +72,7 @@ export class ScrcpyOptions2_2 implements ScrcpyOptions<Init> {
}
serialize(): string[] {
return serialize(this.value, Defaults);
return serialize<Init<boolean>>(this.value, Defaults);
}
setListDisplays(): void {
@ -158,8 +160,8 @@ export class ScrcpyOptions2_2 implements ScrcpyOptions<Init> {
}
}
type Init_ = Init;
type Init_<TVideo extends boolean> = Init<TVideo>;
export namespace ScrcpyOptions2_2 {
export type Init = Init_;
export type Init<TVideo extends boolean = boolean> = Init_<TVideo>;
}

View file

@ -1,5 +1,6 @@
import type { PrevImpl } from "./prev.js";
export interface Init extends Omit<PrevImpl.Init, "audioCodec"> {
audioCodec?: PrevImpl.Init["audioCodec"] | "flac";
export interface Init<TVideo extends boolean>
extends Omit<PrevImpl.Init<TVideo>, "audioCodec"> {
audioCodec?: PrevImpl.Init<TVideo>["audioCodec"] | "flac";
}

View file

@ -37,12 +37,14 @@ import {
setListEncoders,
} from "./impl/index.js";
export class ScrcpyOptions2_3 implements ScrcpyOptions<Init> {
export class ScrcpyOptions2_3<TVideo extends boolean>
implements ScrcpyOptions<Init<TVideo>>
{
static readonly Defaults = Defaults;
readonly version: string;
readonly value: Required<Init>;
readonly value: Required<Init<TVideo>>;
get controlMessageTypes(): readonly ScrcpyControlMessageType[] {
return ControlMessageTypes;
@ -55,8 +57,8 @@ export class ScrcpyOptions2_3 implements ScrcpyOptions<Init> {
#ackClipboardHandler: AckClipboardHandler | undefined;
constructor(init: Init, version = "2.3") {
this.value = { ...Defaults, ...init };
constructor(init: Init<TVideo>, version = "2.3") {
this.value = { ...Defaults, ...init } as never;
this.version = version;
if (this.value.videoSource === "camera") {
@ -70,7 +72,7 @@ export class ScrcpyOptions2_3 implements ScrcpyOptions<Init> {
}
serialize(): string[] {
return serialize(this.value, Defaults);
return serialize<Init<boolean>>(this.value, Defaults);
}
setListDisplays(): void {
@ -158,8 +160,8 @@ export class ScrcpyOptions2_3 implements ScrcpyOptions<Init> {
}
}
type Init_ = Init;
type Init_<TVideo extends boolean> = Init<TVideo>;
export namespace ScrcpyOptions2_3 {
export type Init = Init_;
export type Init<TVideo extends boolean = boolean> = Init_<TVideo>;
}

View file

@ -1,11 +1,14 @@
import { ScrcpyOptions2_3 } from "./2_3/index.js";
export class ScrcpyOptions2_3_1 extends ScrcpyOptions2_3 {
constructor(init: ScrcpyOptions2_3.Init, version = "2.3.1") {
export class ScrcpyOptions2_3_1<
TVideo extends boolean,
> extends ScrcpyOptions2_3<TVideo> {
constructor(init: ScrcpyOptions2_3.Init<TVideo>, version = "2.3.1") {
super(init, version);
}
}
export namespace ScrcpyOptions2_3_1 {
export type Init = ScrcpyOptions2_3.Init;
export type Init<TVideo extends boolean = boolean> =
ScrcpyOptions2_3.Init<TVideo>;
}

View file

@ -41,12 +41,14 @@ import {
UHidOutputStream,
} from "./impl/index.js";
export class ScrcpyOptions2_4 implements ScrcpyOptions<Init> {
export class ScrcpyOptions2_4<TVideo extends boolean>
implements ScrcpyOptions<Init<TVideo>>
{
static readonly Defaults = Defaults;
readonly version: string;
readonly value: Required<Init>;
readonly value: Required<Init<TVideo>>;
get controlMessageTypes(): readonly ScrcpyControlMessageType[] {
return ControlMessageTypes;
@ -66,8 +68,8 @@ export class ScrcpyOptions2_4 implements ScrcpyOptions<Init> {
return this.#uHidOutput;
}
constructor(init: Init, version = "2.4") {
this.value = { ...Defaults, ...init };
constructor(init: Init<TVideo>, version = "2.4") {
this.value = { ...Defaults, ...init } as never;
this.version = version;
if (this.value.videoSource === "camera") {
@ -85,7 +87,7 @@ export class ScrcpyOptions2_4 implements ScrcpyOptions<Init> {
}
serialize(): string[] {
return serialize(this.value, Defaults);
return serialize<Init<boolean>>(this.value, Defaults);
}
setListDisplays(): void {
@ -185,8 +187,8 @@ export class ScrcpyOptions2_4 implements ScrcpyOptions<Init> {
}
}
type Init_ = Init;
type Init_<TVideo extends boolean> = Init<TVideo>;
export namespace ScrcpyOptions2_4 {
export type Init = Init_;
export type Init<TVideo extends boolean = boolean> = Init_<TVideo>;
}

View file

@ -1,11 +1,14 @@
import { ScrcpyOptions2_4 } from "./2_4/index.js";
export class ScrcpyOptions2_5 extends ScrcpyOptions2_4 {
constructor(init: ScrcpyOptions2_4.Init, version = "2.5") {
export class ScrcpyOptions2_5<
TVideo extends boolean,
> extends ScrcpyOptions2_4<TVideo> {
constructor(init: ScrcpyOptions2_4.Init<TVideo>, version = "2.5") {
super(init, version);
}
}
export namespace ScrcpyOptions2_5 {
export type Init = ScrcpyOptions2_4.Init;
export type Init<TVideo extends boolean = boolean> =
ScrcpyOptions2_4.Init<TVideo>;
}

View file

@ -5,4 +5,4 @@ export const Defaults = /* #__PURE__ */ (() =>
({
...PrevImpl.Defaults,
audioDup: false,
}) as const satisfies Required<Init>)();
}) as const satisfies Required<Init<true>>)();

View file

@ -1,6 +1,7 @@
import type { PrevImpl } from "./prev.js";
export interface Init extends Omit<PrevImpl.Init, "audioSource"> {
audioSource?: PrevImpl.Init["audioSource"] | "playback";
export interface Init<TVideo extends boolean>
extends Omit<PrevImpl.Init<TVideo>, "audioSource"> {
audioSource?: PrevImpl.Init<TVideo>["audioSource"] | "playback";
audioDup?: boolean;
}

View file

@ -41,12 +41,14 @@ import {
UHidOutputStream,
} from "./impl/index.js";
export class ScrcpyOptions2_6 implements ScrcpyOptions<Init> {
export class ScrcpyOptions2_6<TVideo extends boolean>
implements ScrcpyOptions<Init<TVideo>>
{
static readonly Defaults = Defaults;
readonly version: string;
readonly value: Required<Init>;
readonly value: Required<Init<TVideo>>;
get controlMessageTypes(): readonly ScrcpyControlMessageType[] {
return ControlMessageTypes;
@ -66,8 +68,8 @@ export class ScrcpyOptions2_6 implements ScrcpyOptions<Init> {
return this.#uHidOutput;
}
constructor(init: Init, version = "2.6") {
this.value = { ...Defaults, ...init };
constructor(init: Init<TVideo>, version = "2.6") {
this.value = { ...Defaults, ...init } as never;
this.version = version;
if (this.value.videoSource === "camera") {
@ -89,7 +91,7 @@ export class ScrcpyOptions2_6 implements ScrcpyOptions<Init> {
}
serialize(): string[] {
return serialize(this.value, Defaults);
return serialize<Init<boolean>>(this.value, Defaults);
}
setListDisplays(): void {
@ -183,8 +185,8 @@ export class ScrcpyOptions2_6 implements ScrcpyOptions<Init> {
}
}
type Init_ = Init;
type Init_<TVideo extends boolean> = Init<TVideo>;
export namespace ScrcpyOptions2_6 {
export type Init = Init_;
export type Init<TVideo extends boolean = boolean> = Init_<TVideo>;
}

View file

@ -1,11 +1,14 @@
import { ScrcpyOptions2_6 } from "./2_6/index.js";
export class ScrcpyOptions2_6_1 extends ScrcpyOptions2_6 {
constructor(init: ScrcpyOptions2_6.Init, version = "2.6.1") {
export class ScrcpyOptions2_6_1<
TVideo extends boolean,
> extends ScrcpyOptions2_6<TVideo> {
constructor(init: ScrcpyOptions2_6.Init<TVideo>, version = "2.6.1") {
super(init, version);
}
}
export namespace ScrcpyOptions2_6_1 {
export type Init = ScrcpyOptions2_6.Init;
export type Init<TVideo extends boolean = boolean> =
ScrcpyOptions2_6.Init<TVideo>;
}

View file

@ -41,12 +41,14 @@ import {
UHidOutputStream,
} from "./impl/index.js";
export class ScrcpyOptions2_7 implements ScrcpyOptions<Init> {
export class ScrcpyOptions2_7<TVideo extends boolean>
implements ScrcpyOptions<Init<TVideo>>
{
static readonly Defaults = Defaults;
readonly version: string;
readonly value: Required<Init>;
readonly value: Required<Init<TVideo>>;
get controlMessageTypes(): readonly ScrcpyControlMessageType[] {
return ControlMessageTypes;
@ -66,8 +68,8 @@ export class ScrcpyOptions2_7 implements ScrcpyOptions<Init> {
return this.#uHidOutput;
}
constructor(init: Init, version = "2.7") {
this.value = { ...Defaults, ...init };
constructor(init: Init<TVideo>, version = "2.7") {
this.value = { ...Defaults, ...init } as never;
this.version = version;
if (this.value.videoSource === "camera") {
@ -89,7 +91,7 @@ export class ScrcpyOptions2_7 implements ScrcpyOptions<Init> {
}
serialize(): string[] {
return serialize(this.value, Defaults);
return serialize<Init<boolean>>(this.value, Defaults);
}
setListDisplays(): void {
@ -183,8 +185,8 @@ export class ScrcpyOptions2_7 implements ScrcpyOptions<Init> {
}
}
type Init_ = Init;
type Init_<TVideo extends boolean> = Init<TVideo>;
export namespace ScrcpyOptions2_7 {
export type Init = Init_;
export type Init<TVideo extends boolean = boolean> = Init_<TVideo>;
}

View file

@ -12,4 +12,4 @@ export const Defaults = /* #__PURE__ */ (() =>
listApps: false,
newDisplay: undefined,
vdSystemDecorations: true,
}) as const satisfies Required<Init>)();
}) as const satisfies Required<Init<true>>)();

View file

@ -106,7 +106,8 @@ export class NewDisplay implements ScrcpyOptionValue {
}
}
export interface Init extends Omit<PrevImpl.Init, "lockVideoOrientation"> {
export interface Init<TVideo extends boolean>
extends Omit<PrevImpl.Init<TVideo>, "lockVideoOrientation"> {
captureOrientation?: CaptureOrientation | string | undefined;
angle?: number;
screenOffTimeout?: number | undefined;

View file

@ -41,12 +41,14 @@ import {
UHidOutputStream,
} from "./impl/index.js";
export class ScrcpyOptions3_0 implements ScrcpyOptions<Init> {
export class ScrcpyOptions3_0<TVideo extends boolean>
implements ScrcpyOptions<Init<TVideo>>
{
static readonly Defaults = Defaults;
readonly version: string;
readonly value: Required<Init>;
readonly value: Required<Init<TVideo>>;
get controlMessageTypes(): readonly ScrcpyControlMessageType[] {
return ControlMessageTypes;
@ -66,8 +68,8 @@ export class ScrcpyOptions3_0 implements ScrcpyOptions<Init> {
return this.#uHidOutput;
}
constructor(init: Init, version = "3.0") {
this.value = { ...Defaults, ...init };
constructor(init: Init<TVideo>, version = "3.0") {
this.value = { ...Defaults, ...init } as never;
this.version = version;
if (this.value.videoSource === "camera") {
@ -89,7 +91,7 @@ export class ScrcpyOptions3_0 implements ScrcpyOptions<Init> {
}
serialize(): string[] {
return serialize(this.value, Defaults);
return serialize<Init<boolean>>(this.value, Defaults);
}
setListDisplays(): void {
@ -183,8 +185,8 @@ export class ScrcpyOptions3_0 implements ScrcpyOptions<Init> {
}
}
type Init_ = Init;
type Init_<TVideo extends boolean> = Init<TVideo>;
export namespace ScrcpyOptions3_0 {
export type Init = Init_;
export type Init<TVideo extends boolean = boolean> = Init_<TVideo>;
}

View file

@ -1,11 +1,14 @@
import { ScrcpyOptions3_0 } from "./3_0/index.js";
export class ScrcpyOptions3_0_1 extends ScrcpyOptions3_0 {
constructor(init: ScrcpyOptions3_0.Init, version = "3.0.1") {
export class ScrcpyOptions3_0_1<
TVideo extends boolean,
> extends ScrcpyOptions3_0<TVideo> {
constructor(init: ScrcpyOptions3_0.Init<TVideo>, version = "3.0.1") {
super(init, version);
}
}
export namespace ScrcpyOptions3_0_1 {
export type Init = ScrcpyOptions3_0.Init;
export type Init<TVideo extends boolean = boolean> =
ScrcpyOptions3_0.Init<TVideo>;
}

View file

@ -1,11 +1,14 @@
import { ScrcpyOptions3_0 } from "./3_0/index.js";
export class ScrcpyOptions3_0_2 extends ScrcpyOptions3_0 {
constructor(init: ScrcpyOptions3_0.Init, version = "3.0.2") {
export class ScrcpyOptions3_0_2<
TVideo extends boolean,
> extends ScrcpyOptions3_0<TVideo> {
constructor(init: ScrcpyOptions3_0.Init<TVideo>, version = "3.0.2") {
super(init, version);
}
}
export namespace ScrcpyOptions3_0_2 {
export type Init = ScrcpyOptions3_0.Init;
export type Init<TVideo extends boolean = boolean> =
ScrcpyOptions3_0.Init<TVideo>;
}

View file

@ -5,4 +5,4 @@ export const Defaults = /* #__PURE__ */ (() =>
({
...PrevImpl.Defaults,
vdDestroyContent: false,
}) as const satisfies Required<Init>)();
}) as const satisfies Required<Init<true>>)();

View file

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

View file

@ -41,12 +41,14 @@ import {
UHidOutputStream,
} from "./impl/index.js";
export class ScrcpyOptions3_1 implements ScrcpyOptions<Init> {
export class ScrcpyOptions3_1<TVideo extends boolean>
implements ScrcpyOptions<Init<TVideo>>
{
static readonly Defaults = Defaults;
readonly version: string;
readonly value: Required<Init>;
readonly value: Required<Init<TVideo>>;
get controlMessageTypes(): readonly ScrcpyControlMessageType[] {
return ControlMessageTypes;
@ -66,8 +68,8 @@ export class ScrcpyOptions3_1 implements ScrcpyOptions<Init> {
return this.#uHidOutput;
}
constructor(init: Init, version = "3.1") {
this.value = { ...Defaults, ...init };
constructor(init: Init<TVideo>, version = "3.1") {
this.value = { ...Defaults, ...init } as never;
this.version = version;
if (this.value.videoSource === "camera") {
@ -89,7 +91,7 @@ export class ScrcpyOptions3_1 implements ScrcpyOptions<Init> {
}
serialize(): string[] {
return serialize(this.value, Defaults);
return serialize<Init<boolean>>(this.value, Defaults);
}
setListDisplays(): void {
@ -183,8 +185,8 @@ export class ScrcpyOptions3_1 implements ScrcpyOptions<Init> {
}
}
type Init_ = Init;
type Init_<TVideo extends boolean> = Init<TVideo>;
export namespace ScrcpyOptions3_1 {
export type Init = Init_;
export type Init<TVideo extends boolean = boolean> = Init_<TVideo>;
}

View file

@ -1,13 +1,16 @@
import { ScrcpyOptions3_1 } from "./3_1/options.js";
export class ScrcpyOptionsLatest extends ScrcpyOptions3_1 {
constructor(init: ScrcpyOptions3_1.Init, version: string) {
export class ScrcpyOptionsLatest<
TVideo extends boolean,
> extends ScrcpyOptions3_1<TVideo> {
constructor(init: ScrcpyOptions3_1.Init<TVideo>, version: string) {
super(init, version);
}
}
export namespace ScrcpyOptionsLatest {
export type Init = ScrcpyOptions3_1.Init;
export type Init<TVideo extends boolean = boolean> =
ScrcpyOptions3_1.Init<TVideo>;
}
export {

6
pnpm-lock.yaml generated
View file

@ -163,9 +163,15 @@ importers:
specifier: workspace:^
version: link:../struct
devDependencies:
'@types/node':
specifier: ^22.10.10
version: 22.10.10
'@yume-chan/eslint-config':
specifier: workspace:^
version: link:../../toolchain/eslint-config
'@yume-chan/test-runner':
specifier: workspace:^
version: link:../../toolchain/test-runner
'@yume-chan/tsconfig':
specifier: workspace:^
version: link:../../toolchain/tsconfig