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

This commit is contained in:
Simon Chan 2024-11-27 14:43:33 +08:00
parent 92472007db
commit cc5d52912e
No known key found for this signature in database
GPG key ID: A8B69F750B9BCEDD
63 changed files with 595 additions and 325 deletions

View file

@ -46,6 +46,8 @@ export class AdbScrcpyOptions1_16 extends AdbScrcpyOptions<
const client = await AdbScrcpyClient.start(adb, path, version, options);
const encoders: ScrcpyEncoder[] = [];
// `client.stdout` is supplied by user and may not support async iteration
await client.stdout.pipeTo(
new WritableStream({
write: (line) => {

View file

@ -2,7 +2,6 @@ import type { MaybeConsumable, WritableStream } from "@yume-chan/stream-extra";
import { ReadableStream } from "@yume-chan/stream-extra";
import type { Adb, AdbSocket } from "../../../adb.js";
import { unreachable } from "../../../utils/index.js";
import type { AdbSubprocessProtocol } from "./types.js";
@ -64,10 +63,9 @@ export class AdbSubprocessNoneProtocol implements AdbSubprocessProtocol {
this.#socket = socket;
this.#stderr = new ReadableStream({
start: (controller) => {
this.#socket.closed
.then(() => controller.close())
.catch(unreachable);
start: async (controller) => {
await this.#socket.closed;
controller.close();
},
});
this.#exit = socket.closed.then(() => 0);

View file

@ -15,23 +15,25 @@ export interface AdbSyncEntry extends AdbSyncStat {
name: string;
}
export const AdbSyncEntryResponse = struct(
/* #__PURE__ */ (() => ({
export const AdbSyncEntryResponse = /* #__PURE__ */ (() =>
struct(
{
...AdbSyncLstatResponse.fields,
name: string(u32),
}))(),
},
{ littleEndian: true, extra: AdbSyncLstatResponse.extra },
);
))();
export type AdbSyncEntryResponse = StructValue<typeof AdbSyncEntryResponse>;
export const AdbSyncEntry2Response = struct(
/* #__PURE__ */ (() => ({
export const AdbSyncEntry2Response = /* #__PURE__ */ (() =>
struct(
{
...AdbSyncStatResponse.fields,
name: string(u32),
}))(),
},
{ littleEndian: true, extra: AdbSyncStatResponse.extra },
);
))();
export type AdbSyncEntry2Response = StructValue<typeof AdbSyncEntry2Response>;

View file

@ -43,11 +43,13 @@ export interface AdbCredentialStore {
iterateKeys(): AdbKeyIterable;
}
export enum AdbAuthType {
Token = 1,
Signature = 2,
PublicKey = 3,
}
export const AdbAuthType = {
Token: 1,
Signature: 2,
PublicKey: 3,
} as const;
export type AdbAuthType = (typeof AdbAuthType)[keyof typeof AdbAuthType];
export interface AdbAuthenticator {
/**

View file

@ -28,7 +28,8 @@ import { AdbCommand, calculateChecksum } from "./packet.js";
export const ADB_DAEMON_VERSION_OMIT_CHECKSUM = 0x01000001;
// https://android.googlesource.com/platform/packages/modules/adb/+/79010dc6d5ca7490c493df800d4421730f5466ca/transport.cpp#1252
// There are some other feature constants, but some of them are only used by ADB server, not devices (daemons).
export const ADB_DAEMON_DEFAULT_FEATURES = [
export const ADB_DAEMON_DEFAULT_FEATURES = /* #__PURE__ */ (() =>
[
AdbFeature.ShellV2,
AdbFeature.Cmd,
AdbFeature.StatV2,
@ -48,7 +49,7 @@ export const ADB_DAEMON_DEFAULT_FEATURES = [
"sendrecv_v2_zstd",
"sendrecv_v2_dry_run_send",
AdbFeature.DelayedAck,
] as AdbFeature[];
] as AdbFeature[])();
export const ADB_DAEMON_DEFAULT_INITIAL_PAYLOAD_SIZE = 32 * 1024 * 1024;
export type AdbDaemonConnection = ReadableWritablePair<

View file

@ -211,12 +211,10 @@ export class BugReport extends AdbCommandBase {
let filename: string | undefined;
let error: string | undefined;
await process.stdout
for await (const line of process.stdout
.pipeThrough(new TextDecoderStream())
.pipeThrough(new SplitStringStream("\n"))
.pipeTo(
new WritableStream<string>({
write(line) {
// Each chunk should contain one or several full lines
.pipeThrough(new SplitStringStream("\n"))) {
// `BEGIN:` and `PROGRESS:` only appear when `-p` is specified.
let match = line.match(BugReport.PROGRESS_REGEX);
if (match) {
@ -239,9 +237,7 @@ export class BugReport extends AdbCommandBase {
// We want to gather all output.
error = match[1];
}
},
}),
);
}
if (error) {
throw new Error(error);

View file

@ -8,7 +8,6 @@ import {
SplitStringStream,
TextDecoderStream,
WrapReadableStream,
WritableStream,
} from "@yume-chan/stream-extra";
import type { AsyncExactReadable, StructValue } from "@yume-chan/struct";
import { decodeUtf8, struct, u16, u32 } from "@yume-chan/struct";
@ -437,13 +436,10 @@ export class Logcat extends AdbCommandBase {
]);
const result: LogSize[] = [];
await stdout
for await (const line of stdout
.pipeThrough(new TextDecoderStream())
.pipeThrough(new SplitStringStream("\n"))
.pipeTo(
new WritableStream({
write(chunk) {
let match = chunk.match(Logcat.LOG_SIZE_REGEX_11);
.pipeThrough(new SplitStringStream("\n"))) {
let match = line.match(Logcat.LOG_SIZE_REGEX_11);
if (match) {
result.push({
id: Logcat.logNameToId(match[1]!),
@ -462,10 +458,10 @@ export class Logcat extends AdbCommandBase {
maxEntrySize: parseInt(match[8]!, 10),
maxPayloadSize: parseInt(match[9]!, 10),
});
return;
break;
}
match = chunk.match(Logcat.LOG_SIZE_REGEX_10);
match = line.match(Logcat.LOG_SIZE_REGEX_10);
if (match) {
result.push({
id: Logcat.logNameToId(match[1]!),
@ -481,9 +477,7 @@ export class Logcat extends AdbCommandBase {
maxPayloadSize: parseInt(match[7]!, 10),
});
}
},
}),
);
}
return result;
}

View file

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

View file

@ -6,7 +6,7 @@ export const VideoOrientation = {
Landscape: 1,
PortraitFlipped: 2,
LandscapeFlipped: 3,
};
} as const;
export type VideoOrientation =
(typeof VideoOrientation)[keyof typeof VideoOrientation];
@ -85,6 +85,10 @@ export class CodecOptions implements ScrcpyOptionValue {
}
}
export namespace CodecOptions {
export type Init = CodecOptionsInit;
}
export interface Init {
logLevel?: LogLevel;

View file

@ -4,18 +4,7 @@ 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) {
return min;
}
if (value > max) {
return max;
}
return value;
}
import { clamp } from "../../utils/index.js";
export const UnsignedFloat: Field<number, never, never> = {
size: 2,
@ -33,6 +22,13 @@ export const UnsignedFloat: Field<number, never, never> = {
}),
};
export const PointerId = {
Mouse: -1n,
Finger: -2n,
VirtualMouse: -3n,
VirtualFinger: -4n,
} as const;
export const InjectTouchControlMessage = struct(
{
type: u8,

View file

@ -5,6 +5,7 @@ export {
} 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 { VideoOrientation } from "./init.js";
export type { Init, LogLevel } from "./init.js";
export { EncoderRegex } from "./parse-encoder.js";
export { SerializeOrder } from "./serialize-order.js";

View file

@ -9,7 +9,7 @@ export const VideoOrientation = {
Landscape: 1,
PortraitFlipped: 2,
LandscapeFlipped: 3,
};
} as const;
export type VideoOrientation =
(typeof VideoOrientation)[keyof typeof VideoOrientation];

View file

@ -4,15 +4,16 @@ function toSnakeCase(input: string): string {
return input.replace(/([A-Z])/g, "_$1").toLowerCase();
}
// 1.21 changed the format of arguments
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) {
// v3.0 `new_display` option needs to send empty strings to server
if (serializedValue === undefined) {
continue;
}
@ -20,7 +21,7 @@ export function serialize<T extends object>(
defaults[key as keyof T],
undefined,
);
if (serializedValue == defaultValue) {
if (serializedValue === defaultValue) {
continue;
}

View file

@ -1,6 +1,8 @@
import type { StructInit } from "@yume-chan/struct";
import { s32, struct } from "@yume-chan/struct";
import type { ScrcpyInjectScrollControlMessage } from "../../latest.js";
import { PrevImpl } from "./prev.js";
export const InjectScrollControlMessage = /* #__PURE__ */ (() =>
@ -18,7 +20,7 @@ export type InjectScrollControlMessage = StructInit<
export class ScrollController extends PrevImpl.ScrollController {
override serializeScrollMessage(
message: InjectScrollControlMessage,
message: ScrcpyInjectScrollControlMessage,
): Uint8Array | undefined {
const processed = this.processMessage(message);
if (!processed) {

View file

@ -2,16 +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 { ScrcpyControlMessageType } from "../../base/index.js";
import type { ScrcpyScrollController } from "../../base/index.js";
import type { ScrcpyInjectScrollControlMessage } from "../../latest.js";
import { PrevImpl } from "./prev.js";
import { clamp } from "../../utils/index.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 = PrevImpl.clamp(value, -1, 1);
value = clamp(value, -1, 1);
value = value === 1 ? 0x7fff : value * 0x8000;
setInt16(buffer, index, value, littleEndian);
},
@ -23,9 +23,10 @@ export const SignedFloat: Field<number, never, never> = {
}),
};
export const InjectScrollControlMessage = struct(
export const InjectScrollControlMessage = /* #__PURE__ */ (() =>
struct(
{
type: u8,
type: u8(ScrcpyControlMessageType.InjectScroll),
pointerX: u32,
pointerY: u32,
screenWidth: u16,
@ -35,7 +36,7 @@ export const InjectScrollControlMessage = struct(
buttons: u32,
},
{ littleEndian: false },
);
))();
export type InjectScrollControlMessage = StructInit<
typeof InjectScrollControlMessage

View file

@ -1,10 +1,12 @@
import { omit } from "../../utils/index.js";
import type { Init } from "./init.js";
import { InstanceId } from "./init.js";
import { PrevImpl } from "./prev.js";
export const Defaults = /* #__PURE__ */ (() =>
({
...PrevImpl.Defaults,
...omit(PrevImpl.Defaults, "bitRate", "codecOptions", "encoderName"),
scid: InstanceId.NONE,
videoCodec: "h264",

View file

@ -6,6 +6,7 @@ export {
serializeInjectTouchControlMessage,
} from "./inject-touch.js";
export * from "./parse-audio-stream-metadata.js";
export { parseDisplay } from "./parse-display.js";
export { parseEncoder } from "./parse-encoder.js";
export { parseVideoStreamMetadata } from "./parse-video-stream-metadata.js";
export { setListDisplays } from "./set-list-display.js";

View file

@ -2,13 +2,15 @@ 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 { ScrcpyControlMessageType } from "../../base/control-message-type.js";
import type { ScrcpyInjectTouchControlMessage } from "../../latest.js";
import { PrevImpl } from "./prev.js";
export const InjectTouchControlMessage = struct(
export const InjectTouchControlMessage = /* #__PURE__ */ (() =>
struct(
{
type: u8,
type: u8(ScrcpyControlMessageType.InjectTouch),
action: u8<AndroidMotionEventAction>(),
pointerId: u64,
pointerX: u32,
@ -20,7 +22,7 @@ export const InjectTouchControlMessage = struct(
buttons: u32,
},
{ littleEndian: false },
);
))();
export type InjectTouchControlMessage = StructInit<
typeof InjectTouchControlMessage

View file

@ -1,6 +1,8 @@
import type { ScrcpyDisplay } from "../../base/index.js";
export function parseDisplay(line: string): ScrcpyDisplay | undefined {
// The client-side option name is `--display`
// but the server-side option name is always `display_id`
const match = line.match(/^\s+--display=(\d+)\s+\(([^)]+)\)$/);
if (match) {
const display: ScrcpyDisplay = {

View file

@ -41,7 +41,7 @@ async function parseAsync(
let width: number | undefined;
let height: number | undefined;
if (options.sendCodecMeta) {
codec = await PrevImpl.readU32(buffered);
codec = (await PrevImpl.readU32(buffered)) as ScrcpyVideoCodecId;
width = await PrevImpl.readU32(buffered);
height = await PrevImpl.readU32(buffered);
} else {
@ -64,9 +64,7 @@ export function parseVideoStreamMetadata(
if (!options.sendDeviceMeta && !options.sendCodecMeta) {
return {
stream,
metadata: {
codec: toCodecId(options.videoCodec),
},
metadata: { codec: toCodecId(options.videoCodec) },
};
}

View file

@ -1,4 +1,4 @@
export * from "../../2_0/impl/index.js";
export * from "../../2_1/impl/index.js";
export { Defaults } from "./defaults.js";
export type { Init } from "./init.js";
export { parseDisplay } from "./parse-display.js";

View file

@ -1,8 +1,7 @@
import type { PrevImpl } from "./prev.js";
export interface Init extends Omit<PrevImpl.Init, "display"> {
export interface Init extends PrevImpl.Init {
videoSource?: "display" | "camera";
displayId?: number;
cameraId?: string | undefined;
cameraSize?: string | undefined;
cameraFacing?: "front" | "back" | "external" | undefined;

View file

@ -1 +1 @@
export * as PrevImpl from "../../2_0/impl/index.js";
export * as PrevImpl from "../../2_1/impl/index.js";

View file

@ -54,6 +54,10 @@ export class ScrcpyOptions2_2 implements ScrcpyOptions<Init> {
constructor(init: Init) {
this.value = { ...Defaults, ...init };
if (this.value.videoSource === "camera") {
this.value.control = false;
}
if (this.value.control && this.value.clipboardAutosync) {
this.#clipboard = new ClipboardStream();
this.#ackClipboardHandler = new AckClipboardHandler();

View file

@ -1,2 +1,2 @@
export * from "../../2_0/impl/index.js";
export * from "../../2_2/impl/index.js";
export type { Init } from "./init.js";

View file

@ -1 +1 @@
export * as PrevImpl from "../../2_0/impl/index.js";
export * as PrevImpl from "../../2_2/impl/index.js";

View file

@ -54,6 +54,10 @@ export class ScrcpyOptions2_3 implements ScrcpyOptions<Init> {
constructor(init: Init) {
this.value = { ...Defaults, ...init };
if (this.value.videoSource === "camera") {
this.value.control = false;
}
if (this.value.control && this.value.clipboardAutosync) {
this.#clipboard = new ClipboardStream();
this.#ackClipboardHandler = new AckClipboardHandler();

View file

@ -17,6 +17,7 @@ import type {
ScrcpyInjectTouchControlMessage,
ScrcpySetClipboardControlMessage,
ScrcpyUHidCreateControlMessage,
ScrcpyUHidOutputDeviceMessage,
} from "../latest.js";
import type { Init } from "./impl/index.js";
@ -55,13 +56,19 @@ export class ScrcpyOptions2_4 implements ScrcpyOptions<Init> {
#ackClipboardHandler: AckClipboardHandler | undefined;
#uHidOutput: UHidOutputStream | undefined;
get uHidOutput(): UHidOutputStream | undefined {
get uHidOutput():
| ReadableStream<ScrcpyUHidOutputDeviceMessage>
| undefined {
return this.#uHidOutput;
}
constructor(init: Init) {
this.value = { ...Defaults, ...init };
if (this.value.videoSource === "camera") {
this.value.control = false;
}
if (this.value.control) {
if (this.value.clipboardAutosync) {
this.#clipboard = new ClipboardStream();

View file

@ -17,9 +17,9 @@ import type {
ScrcpyInjectTouchControlMessage,
ScrcpySetClipboardControlMessage,
ScrcpyUHidCreateControlMessage,
ScrcpyUHidOutputDeviceMessage,
} from "../latest.js";
import type { Init } from "./impl/index.js";
import {
AckClipboardHandler,
ClipboardStream,
@ -37,7 +37,10 @@ import {
serializeUHidCreateControlMessage,
setListDisplays,
setListEncoders,
UHidOutputStream
} from "./impl/index.js";
import type {Init} from "./impl/index.js";
export class ScrcpyOptions2_6 implements ScrcpyOptions<Init> {
readonly value: Required<Init>;
@ -53,13 +56,28 @@ export class ScrcpyOptions2_6 implements ScrcpyOptions<Init> {
#ackClipboardHandler: AckClipboardHandler | undefined;
#uHidOutput: UHidOutputStream | undefined;
get uHidOutput():
| ReadableStream<ScrcpyUHidOutputDeviceMessage>
| undefined {
return this.#uHidOutput;
}
constructor(init: Init) {
this.value = { ...Defaults, ...init };
if (this.value.control && this.value.clipboardAutosync) {
if (this.value.videoSource === "camera") {
this.value.control = false;
}
if (this.value.control) {
if (this.value.clipboardAutosync) {
this.#clipboard = new ClipboardStream();
this.#ackClipboardHandler = new AckClipboardHandler();
}
this.#uHidOutput = new UHidOutputStream();
}
}
serialize(): string[] {

View file

@ -1,17 +1,19 @@
import type { StructInit } from "@yume-chan/struct";
import { buffer, string, struct, u16, u8 } from "@yume-chan/struct";
import { ScrcpyControlMessageType } from "../../base/control-message-type.js";
import type { ScrcpyUHidCreateControlMessage } from "../../latest.js";
export const UHidCreateControlMessage = struct(
export const UHidCreateControlMessage = /* #__PURE__ */ (() =>
struct(
{
type: u8,
type: u8(ScrcpyControlMessageType.UHidCreate),
id: u16,
name: string(u8),
data: buffer(u16),
},
{ littleEndian: false },
);
))();
export type UHidCreateControlMessage = StructInit<
typeof UHidCreateControlMessage

View file

@ -17,6 +17,7 @@ import type {
ScrcpyInjectTouchControlMessage,
ScrcpySetClipboardControlMessage,
ScrcpyUHidCreateControlMessage,
ScrcpyUHidOutputDeviceMessage,
} from "../latest.js";
import type { Init } from "./impl/index.js";
@ -37,6 +38,7 @@ import {
serializeUHidCreateControlMessage,
setListDisplays,
setListEncoders,
UHidOutputStream,
} from "./impl/index.js";
export class ScrcpyOptions2_7 implements ScrcpyOptions<Init> {
@ -53,13 +55,28 @@ export class ScrcpyOptions2_7 implements ScrcpyOptions<Init> {
#ackClipboardHandler: AckClipboardHandler | undefined;
#uHidOutput: UHidOutputStream | undefined;
get uHidOutput():
| ReadableStream<ScrcpyUHidOutputDeviceMessage>
| undefined {
return this.#uHidOutput;
}
constructor(init: Init) {
this.value = { ...Defaults, ...init };
if (this.value.control && this.value.clipboardAutosync) {
if (this.value.videoSource === "camera") {
this.value.control = false;
}
if (this.value.control) {
if (this.value.clipboardAutosync) {
this.#clipboard = new ClipboardStream();
this.#ackClipboardHandler = new AckClipboardHandler();
}
this.#uHidOutput = new UHidOutputStream();
}
}
serialize(): string[] {

View file

@ -2,8 +2,9 @@ import { ScrcpyControlMessageType } from "../../base/index.js";
import { PrevImpl } from "./prev.js";
export const ControlMessageTypes: readonly ScrcpyControlMessageType[] = [
export const ControlMessageTypes: readonly ScrcpyControlMessageType[] =
/* #__PURE__ */ (() => [
...PrevImpl.ControlMessageTypes,
ScrcpyControlMessageType.StartApp,
ScrcpyControlMessageType.ResetVideo,
];
])();

View file

@ -1,13 +1,16 @@
import { omit } from "../../utils/index.js";
import type { Init } from "./init.js";
import { CaptureOrientation, NewDisplay } from "./init.js";
import { CaptureOrientation } from "./init.js";
import { PrevImpl } from "./prev.js";
export const Defaults = {
...PrevImpl.Defaults,
captureOrientation: CaptureOrientation.Default,
export const Defaults = /* #__PURE__ */ (() =>
({
...omit(PrevImpl.Defaults, "lockVideoOrientation"),
captureOrientation: CaptureOrientation.Unlocked,
angle: 0,
screenOffTimeout: undefined,
listApps: false,
newDisplay: NewDisplay.Empty,
newDisplay: undefined,
vdSystemDecorations: true,
} as const satisfies Required<Init>;
}) as const satisfies Required<Init>)();

View file

@ -1,5 +1,10 @@
export * from "../../2_7/impl/index.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 {
CaptureOrientation,
LockOrientation,
NewDisplay,
Orientation,
type Init,
} from "./init.js";

View file

@ -6,7 +6,7 @@ export const LockOrientation = {
Unlocked: 0,
LockedInitial: 1,
LockedValue: 2,
};
} as const;
export type LockOrientation =
(typeof LockOrientation)[keyof typeof LockOrientation];
@ -16,16 +16,17 @@ export const Orientation = {
Orient90: 90,
Orient180: 180,
Orient270: 270,
};
} as const;
export type Orientation = (typeof Orientation)[keyof typeof Orientation];
export class CaptureOrientation implements ScrcpyOptionValue {
static Default = /* #__PURE__ */ new CaptureOrientation(
static Unlocked = /* #__PURE__ */ (() =>
new CaptureOrientation(
LockOrientation.Unlocked,
Orientation.Orient0,
false,
);
))();
lock: LockOrientation;
orientation: Orientation;
@ -59,7 +60,7 @@ export class CaptureOrientation implements ScrcpyOptionValue {
}
export class NewDisplay implements ScrcpyOptionValue {
static Empty = /* #__PURE__ */ new NewDisplay();
static Default = /* #__PURE__ */ new NewDisplay();
width?: number | undefined;
height?: number | undefined;
@ -90,7 +91,7 @@ export class NewDisplay implements ScrcpyOptionValue {
this.height === undefined &&
this.dpi === undefined
) {
return undefined;
return "";
}
if (this.width === undefined) {
@ -112,6 +113,9 @@ export interface Init extends Omit<PrevImpl.Init, "lockVideoOrientation"> {
listApps?: boolean;
newDisplay?: NewDisplay;
// `display_id` and `new_display` can't be specified at the same time
// but `serialize` method will exclude options that are same as the default value
// so `displayId: 0` will be ignored
newDisplay?: NewDisplay | undefined;
vdSystemDecorations?: boolean;
}

View file

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

View file

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

View file

@ -17,6 +17,7 @@ import type {
ScrcpyInjectTouchControlMessage,
ScrcpySetClipboardControlMessage,
ScrcpyUHidCreateControlMessage,
ScrcpyUHidOutputDeviceMessage,
} from "../latest.js";
import type { Init } from "./impl/index.js";
@ -37,9 +38,10 @@ import {
serializeUHidCreateControlMessage,
setListDisplays,
setListEncoders,
UHidOutputStream,
} from "./impl/index.js";
export class ScrcpyOptionsX_X implements ScrcpyOptions<Init> {
export class ScrcpyOptions3_0 implements ScrcpyOptions<Init> {
readonly value: Required<Init>;
get controlMessageTypes(): readonly ScrcpyControlMessageType[] {
@ -53,13 +55,28 @@ export class ScrcpyOptionsX_X implements ScrcpyOptions<Init> {
#ackClipboardHandler: AckClipboardHandler | undefined;
#uHidOutput: UHidOutputStream | undefined;
get uHidOutput():
| ReadableStream<ScrcpyUHidOutputDeviceMessage>
| undefined {
return this.#uHidOutput;
}
constructor(init: Init) {
this.value = { ...Defaults, ...init };
if (this.value.control && this.value.clipboardAutosync) {
if (this.value.videoSource === "camera") {
this.value.control = false;
}
if (this.value.control) {
if (this.value.clipboardAutosync) {
this.#clipboard = new ClipboardStream();
this.#ackClipboardHandler = new AckClipboardHandler();
}
this.#uHidOutput = new UHidOutputStream();
}
}
serialize(): string[] {

View file

@ -13,7 +13,7 @@ export const AndroidMotionEventAction = {
HoverExit: 10,
ButtonPress: 11,
ButtonRelease: 12,
};
} as const;
export type AndroidMotionEventAction =
(typeof AndroidMotionEventAction)[keyof typeof AndroidMotionEventAction];
@ -27,7 +27,7 @@ export const AndroidMotionEventButton = {
Forward: 16,
StylusPrimary: 32,
StylusSecondary: 64,
};
} as const;
export type AndroidMotionEventButton =
(typeof AndroidMotionEventButton)[keyof typeof AndroidMotionEventButton];

View file

@ -18,7 +18,7 @@ export const ScrcpyControlMessageType = {
OpenHardKeyboardSettings: 15,
StartApp: 16,
ResetVideo: 17,
};
} as const;
export type ScrcpyControlMessageType =
(typeof ScrcpyControlMessageType)[keyof typeof ScrcpyControlMessageType];

View file

@ -7,6 +7,7 @@ import type {
ScrcpyInjectTouchControlMessage,
ScrcpySetClipboardControlMessage,
ScrcpyUHidCreateControlMessage,
ScrcpyUHidOutputDeviceMessage,
} from "../latest.js";
import type { ScrcpyAudioStreamMetadata } from "./audio.js";
@ -22,7 +23,11 @@ export interface ScrcpyOptions<T extends object> {
value: Required<T>;
get clipboard(): ReadableStream<string> | undefined;
readonly clipboard?: ReadableStream<string> | undefined;
readonly uHidOutput?:
| ReadableStream<ScrcpyUHidOutputDeviceMessage>
| undefined;
serialize(): string[];

View file

@ -16,7 +16,17 @@ export const ScrcpyVideoCodecId = {
H264: 0x68_32_36_34,
H265: 0x68_32_36_35,
AV1: 0x00_61_76_31,
};
} as const;
export type ScrcpyVideoCodecId =
(typeof ScrcpyVideoCodecId)[keyof typeof ScrcpyVideoCodecId];
export const ScrcpyVideoCodecNameMap = /* #__PURE__ */ (() => {
const result = new Map<number, string>();
for (const key in ScrcpyVideoCodecId) {
const value =
ScrcpyVideoCodecId[key as keyof typeof ScrcpyVideoCodecId];
result.set(value, key);
}
return result;
})();

View file

@ -136,7 +136,7 @@ const ObuType = {
RedundantFrameHeader: 7,
TileList: 8,
Padding: 15,
};
} as const;
type ObuType = (typeof ObuType)[keyof typeof ObuType];
@ -153,7 +153,7 @@ const ColorPrimaries = {
Smpte431: 11,
Smpte432: 12,
Ebu3213: 22,
};
} as const;
const TransferCharacteristics = {
Bt709: 1,
@ -173,7 +173,7 @@ const TransferCharacteristics = {
Smpte2084: 16,
Smpte428: 17,
Hlg: 18,
};
} as const;
const MatrixCoefficients = {
Identity: 0,
@ -190,7 +190,7 @@ const MatrixCoefficients = {
ChromatNcl: 12,
ChromatCl: 13,
ICtCp: 14,
};
} as const;
export class Av1 extends BitReader {
static ObuType = ObuType;
@ -623,13 +623,16 @@ export class Av1 extends BitReader {
// const NumPlanes = mono_chrome ? 1 : 3;
const color_description_present_flag = !!this.f1();
let color_primaries = Av1.ColorPrimaries.Unspecified;
let transfer_characteristics = Av1.TransferCharacteristics.Unspecified;
let matrix_coefficients = Av1.MatrixCoefficients.Unspecified;
let color_primaries: Av1.ColorPrimaries =
Av1.ColorPrimaries.Unspecified;
let transfer_characteristics: Av1.TransferCharacteristics =
Av1.TransferCharacteristics.Unspecified;
let matrix_coefficients: Av1.MatrixCoefficients =
Av1.MatrixCoefficients.Unspecified;
if (color_description_present_flag) {
color_primaries = this.f(8);
transfer_characteristics = this.f(8);
matrix_coefficients = this.f(8);
color_primaries = this.f(8) as Av1.ColorPrimaries;
transfer_characteristics = this.f(8) as Av1.TransferCharacteristics;
matrix_coefficients = this.f(8) as Av1.MatrixCoefficients;
}
let color_range = false;

View file

@ -6,17 +6,19 @@ import type {
AndroidKeyEventAction,
AndroidKeyEventMeta,
} from "../android/index.js";
import { ScrcpyControlMessageType } from "../base/index.js";
export const ScrcpyInjectKeyCodeControlMessage = struct(
export const ScrcpyInjectKeyCodeControlMessage = /* #__PURE__ */ (() =>
struct(
{
type: u8,
type: u8(ScrcpyControlMessageType.InjectKeyCode),
action: u8<AndroidKeyEventAction>(),
keyCode: u32<AndroidKeyCode>(),
repeat: u32,
metaState: u32<AndroidKeyEventMeta>(),
},
{ littleEndian: false },
);
))();
export type ScrcpyInjectKeyCodeControlMessage = StructInit<
typeof ScrcpyInjectKeyCodeControlMessage

View file

@ -24,7 +24,7 @@ export class ScrcpyControlMessageTypeMap {
message: Omit<T, "type">,
type: T["type"],
): T {
(message as T).type = this.get(type);
(message as T).type = this.get(type) as ScrcpyControlMessageType;
return message as T;
}
}

View file

@ -1,14 +1,17 @@
import type { StructInit } from "@yume-chan/struct";
import { buffer, struct, u16, u8 } from "@yume-chan/struct";
export const ScrcpyUHidInputControlMessage = struct(
import { ScrcpyControlMessageType } from "../base/index.js";
export const ScrcpyUHidInputControlMessage = /* #__PURE__ */ (() =>
struct(
{
type: u8,
type: u8(ScrcpyControlMessageType.UHidInput),
id: u16,
data: buffer(u16),
},
{ littleEndian: false },
);
))();
export type ScrcpyUHidInputControlMessage = StructInit<
typeof ScrcpyUHidInputControlMessage

View file

@ -10,10 +10,12 @@ import type {
ScrcpyInjectScrollControlMessage,
ScrcpyInjectTouchControlMessage,
ScrcpySetClipboardControlMessage,
ScrcpyUHidCreateControlMessage,
} from "../latest.js";
import type { ScrcpyInjectKeyCodeControlMessage } from "./inject-key-code.js";
import { ScrcpyControlMessageSerializer } from "./serializer.js";
import type { ScrcpyUHidInputControlMessage } from "./uhid.js";
export class ScrcpyControlMessageWriter {
#writer: WritableStreamDefaultWriter<Consumable<Uint8Array>>;
@ -27,25 +29,23 @@ export class ScrcpyControlMessageWriter {
this.#serializer = new ScrcpyControlMessageSerializer(options);
}
async write(message: Uint8Array) {
await Consumable.WritableStream.write(this.#writer, message);
write(message: Uint8Array) {
return Consumable.WritableStream.write(this.#writer, message);
}
async injectKeyCode(
message: Omit<ScrcpyInjectKeyCodeControlMessage, "type">,
) {
await this.write(this.#serializer.injectKeyCode(message));
injectKeyCode(message: Omit<ScrcpyInjectKeyCodeControlMessage, "type">) {
return this.write(this.#serializer.injectKeyCode(message));
}
async injectText(text: string) {
await this.write(this.#serializer.injectText(text));
injectText(text: string) {
return this.write(this.#serializer.injectText(text));
}
/**
* `pressure` is a float value between 0 and 1.
*/
async injectTouch(message: Omit<ScrcpyInjectTouchControlMessage, "type">) {
await this.write(this.#serializer.injectTouch(message));
injectTouch(message: Omit<ScrcpyInjectTouchControlMessage, "type">) {
return this.write(this.#serializer.injectTouch(message));
}
/**
@ -67,24 +67,24 @@ export class ScrcpyControlMessageWriter {
}
}
async setScreenPowerMode(mode: AndroidScreenPowerMode) {
await this.write(this.#serializer.setDisplayPower(mode));
setScreenPowerMode(mode: AndroidScreenPowerMode) {
return this.write(this.#serializer.setDisplayPower(mode));
}
async expandNotificationPanel() {
await this.write(this.#serializer.expandNotificationPanel());
expandNotificationPanel() {
return this.write(this.#serializer.expandNotificationPanel());
}
async expandSettingPanel() {
await this.write(this.#serializer.expandSettingPanel());
expandSettingPanel() {
return this.write(this.#serializer.expandSettingPanel());
}
async collapseNotificationPanel() {
await this.write(this.#serializer.collapseNotificationPanel());
collapseNotificationPanel() {
return this.write(this.#serializer.collapseNotificationPanel());
}
async rotateDevice() {
await this.write(this.#serializer.rotateDevice());
rotateDevice() {
return this.write(this.#serializer.rotateDevice());
}
async setClipboard(
@ -99,6 +99,29 @@ export class ScrcpyControlMessageWriter {
}
}
uHidCreate(message: Omit<ScrcpyUHidCreateControlMessage, "type">) {
return this.write(this.#serializer.uHidCreate(message));
}
uHidInput(message: Omit<ScrcpyUHidInputControlMessage, "type">) {
return this.write(this.#serializer.uHidInput(message));
}
uHidDestroy(id: number) {
return this.write(this.#serializer.uHidDestroy(id));
}
startApp(
name: string,
options?: { forceStop?: boolean; searchByName?: boolean },
) {
return this.write(this.#serializer.startApp(name, options));
}
resetVideo() {
return this.write(this.#serializer.resetVideo());
}
releaseLock() {
this.#writer.releaseLock();
}

View file

@ -17,6 +17,7 @@ export * from "./2_4/index.js";
export * from "./2_5.js";
export * from "./2_6/index.js";
export * from "./2_7/index.js";
export * from "./3_0/index.js";
export * from "./android/index.js";
export * from "./base/index.js";
export * from "./codec/index.js";

View file

@ -1,8 +1,15 @@
export {
BackOrScreenOnControlMessage as ScrcpyBackOrScreenOnControlMessage,
CaptureOrientation as ScrcpyCaptureOrientation,
CodecOptions as ScrcpyCodecOptions,
InjectScrollControlMessage as ScrcpyInjectScrollControlMessage,
InjectTouchControlMessage as ScrcpyInjectTouchControlMessage,
InstanceId as ScrcpyInstanceId,
LockOrientation as ScrcpyLockOrientation,
NewDisplay as ScrcpyNewDisplay,
Orientation as ScrcpyOrientation,
PointerId as ScrcpyPointerId,
SetClipboardControlMessage as ScrcpySetClipboardControlMessage,
UHidCreateControlMessage as ScrcpyUHidCreateControlMessage,
} from "./2_7/impl/index.js";
UHidOutputDeviceMessage as ScrcpyUHidOutputDeviceMessage,
} from "./3_0/impl/index.js";

View file

@ -0,0 +1,11 @@
export function clamp(value: number, min: number, max: number): number {
if (value < min) {
return min;
}
if (value > max) {
return max;
}
return value;
}

View file

@ -1,2 +1,4 @@
export * from "./clamp.js";
export * from "./constants.js";
export * from "./omit.js";
export * from "./wrapper.js";

View file

@ -0,0 +1,11 @@
/* #__NO_SIDE_EFFECTS__ */
export function omit<T extends Record<string, unknown>, K extends (keyof T)[]>(
value: T,
...keys: K
): T {
return Object.fromEntries(
Object.entries(value).filter(
([key]) => !keys.includes(key as K[number]),
),
) as never;
}

View file

@ -32,6 +32,10 @@ export class ScrcpyOptionsWrapper<T extends object>
return this.#base.clipboard;
}
get uHidOutput() {
return this.#base.uHidOutput;
}
constructor(options: ScrcpyOptions<T>) {
this.#base = options;
}

View file

@ -1,11 +1,13 @@
import type {
AbortSignal,
ReadableStreamIteratorOptions,
ReadableStream as ReadableStreamType,
TransformStream as TransformStreamType,
WritableStream as WritableStreamType,
} from "./types.js";
export * from "./types.js";
export { ReadableStream };
/** A controller object that allows you to abort one or more DOM requests as and when desired. */
export interface AbortController {
@ -32,13 +34,70 @@ interface GlobalExtension {
TransformStream: typeof TransformStreamType;
}
export const { AbortController } = globalThis as unknown as GlobalExtension;
export type ReadableStream<T> = ReadableStreamType<T>;
export type WritableStream<T> = WritableStreamType<T>;
export type TransformStream<I, O> = TransformStreamType<I, O>;
export const {
AbortController,
ReadableStream,
WritableStream,
TransformStream,
} = globalThis as unknown as GlobalExtension;
const ReadableStream = /* #__PURE__ */ (() => {
const { ReadableStream } = globalThis as unknown as GlobalExtension;
if (!ReadableStream.from) {
ReadableStream.from = function (iterable) {
const iterator =
Symbol.asyncIterator in iterable
? iterable[Symbol.asyncIterator]()
: iterable[Symbol.iterator]();
return new ReadableStream({
async pull(controller) {
const result = await iterator.next();
if (result.done) {
controller.close();
return;
}
controller.enqueue(result.value);
},
async cancel(reason) {
await iterator.return?.(reason);
},
});
};
}
if (
!ReadableStream.prototype[Symbol.asyncIterator] ||
!ReadableStream.prototype.values
) {
ReadableStream.prototype.values = async function* <R>(
this: ReadableStream<R>,
options?: ReadableStreamIteratorOptions,
) {
const reader = this.getReader();
try {
while (true) {
const { done, value } = await reader.read();
if (done) {
return;
}
yield value;
}
} finally {
if (!options?.preventCancel) {
await reader.cancel();
}
reader.releaseLock();
}
};
ReadableStream.prototype[Symbol.asyncIterator] =
// eslint-disable-next-line @typescript-eslint/unbound-method
ReadableStream.prototype.values;
}
return ReadableStream;
})();
export const { WritableStream, TransformStream } =
globalThis as unknown as GlobalExtension;

View file

@ -242,7 +242,7 @@ export declare class ReadableStream<out R> implements AsyncIterable<R> {
* such as an array, an async generator, or a Node.js readable stream.
*/
static from<R>(
asyncIterable: Iterable<R> | AsyncIterable<R> | ReadableStreamLike<R>,
asyncIterable: Iterable<R> | AsyncIterable<R>,
): ReadableStream<R>;
}
@ -253,7 +253,7 @@ export declare class ReadableStream<out R> implements AsyncIterable<R> {
*/
export declare interface ReadableStreamAsyncIterator<R>
extends AsyncIterableIterator<R> {
next(): Promise<IteratorResult<R, undefined>>;
next(): Promise<IteratorResult<R, void>>;
return(value?: R): Promise<IteratorResult<R>>;
}

View file

@ -42,9 +42,7 @@ export interface BufferLike {
export const EmptyUint8Array = new Uint8Array(0);
// Rollup doesn't support `/* #__NO_SIDE_EFFECTS__ */ export const a = () => {}
/* #__NO_SIDE_EFFECTS__ */
function _buffer(
export const buffer: BufferLike = function (
lengthOrField:
| string
| number
@ -259,6 +257,4 @@ function _buffer(
return reader.readExactly(length);
},
};
}
export const buffer: BufferLike = _buffer as never;
} as never;

View file

@ -18,11 +18,11 @@ import { bipedal } from "./bipedal.js";
import type { Field } from "./field.js";
export interface NumberField<T> extends Field<T, never, never> {
<U>(infer?: T): Field<U, never, never>;
<const U>(infer?: U): Field<U, never, never>;
}
/* #__NO_SIDE_EFFECTS__ */
function number<T>(
function factory<T>(
size: number,
serialize: Field<T, never, never>["serialize"],
deserialize: Field<T, never, never>["deserialize"],
@ -34,7 +34,7 @@ function number<T>(
return result as never;
}
export const u8 = number<number>(
export const u8 = factory<number>(
1,
(value, { buffer, index }) => {
buffer[index] = value;
@ -45,7 +45,7 @@ export const u8 = number<number>(
}),
);
export const s8 = number<number>(
export const s8 = factory<number>(
1,
(value, { buffer, index }) => {
buffer[index] = value;
@ -56,7 +56,7 @@ export const s8 = number<number>(
}),
);
export const u16 = number<number>(
export const u16 = factory<number>(
2,
(value, { buffer, index, littleEndian }) => {
setUint16(buffer, index, value, littleEndian);
@ -67,7 +67,7 @@ export const u16 = number<number>(
}),
);
export const s16 = number<number>(
export const s16 = factory<number>(
2,
(value, { buffer, index, littleEndian }) => {
setInt16(buffer, index, value, littleEndian);
@ -78,7 +78,7 @@ export const s16 = number<number>(
}),
);
export const u32 = number<number>(
export const u32 = factory<number>(
4,
(value, { buffer, index, littleEndian }) => {
setUint32(buffer, index, value, littleEndian);
@ -89,7 +89,7 @@ export const u32 = number<number>(
}),
);
export const s32 = number<number>(
export const s32 = factory<number>(
4,
(value, { buffer, index, littleEndian }) => {
setInt32(buffer, index, value, littleEndian);
@ -100,7 +100,7 @@ export const s32 = number<number>(
}),
);
export const u64 = number<bigint>(
export const u64 = factory<bigint>(
8,
(value, { buffer, index, littleEndian }) => {
setUint64(buffer, index, value, littleEndian);
@ -111,7 +111,7 @@ export const u64 = number<bigint>(
}),
);
export const s64 = number<bigint>(
export const s64 = factory<bigint>(
8,
(value, { buffer, index, littleEndian }) => {
setInt64(buffer, index, value, littleEndian);

View file

@ -33,6 +33,7 @@ export function encodeUtf8(input: string): Uint8Array {
return SharedEncoder.encode(input);
}
/* #__NO_SIDE_EFFECTS__ */
export function decodeUtf8(buffer: ArrayBufferView | ArrayBuffer): string {
// `TextDecoder` has internal states in stream mode,
// but this method is not for stream mode, so the instance can be reused

40
pnpm-lock.yaml generated
View file

@ -439,21 +439,6 @@ importers:
specifier: ^5.6.3
version: 5.6.3
libraries/scrcpy/side-effect-test:
devDependencies:
'@rollup/plugin-node-resolve':
specifier: ^15.3.0
version: 15.3.0(rollup@4.27.4)
'@rollup/plugin-terser':
specifier: ^0.4.4
version: 0.4.4(rollup@4.27.4)
'@rollup/plugin-typescript':
specifier: ^12.1.1
version: 12.1.1(rollup@4.27.4)(tslib@2.8.1)(typescript@5.6.3)
rollup:
specifier: ^4.27.4
version: 4.27.4
libraries/stream-extra:
dependencies:
'@yume-chan/async':
@ -544,6 +529,31 @@ importers:
specifier: ^2.2.3
version: 2.2.3
toolchain/side-effect-test:
dependencies:
'@yume-chan/adb':
specifier: workspace:^
version: link:../../libraries/adb
'@yume-chan/struct':
specifier: workspace:^
version: link:../../libraries/struct
devDependencies:
'@rollup/plugin-node-resolve':
specifier: ^15.3.0
version: 15.3.0(rollup@4.27.4)
'@rollup/plugin-terser':
specifier: ^0.4.4
version: 0.4.4(rollup@4.27.4)
'@rollup/plugin-typescript':
specifier: ^12.1.1
version: 12.1.1(rollup@4.27.4)(tslib@2.8.1)(typescript@5.6.3)
rollup:
specifier: ^4.27.4
version: 4.27.4
tslib:
specifier: ^2.8.1
version: 2.8.1
toolchain/test-runner:
devDependencies:
'@types/node':

View file

@ -14,6 +14,11 @@
"@rollup/plugin-node-resolve": "^15.3.0",
"@rollup/plugin-terser": "^0.4.4",
"@rollup/plugin-typescript": "^12.1.1",
"rollup": "^4.27.4"
"rollup": "^4.27.4",
"tslib": "^2.8.1"
},
"dependencies": {
"@yume-chan/adb": "workspace:^",
"@yume-chan/struct": "workspace:^"
}
}

View file

@ -0,0 +1,33 @@
import {
bipedal,
buffer,
decodeUtf8,
encodeUtf8,
s16,
s32,
s64,
s8,
string,
struct,
u16,
u32,
u64,
u8,
} from "@yume-chan/struct";
bipedal(function () {});
buffer(u8);
decodeUtf8(new Uint8Array());
encodeUtf8("");
s16(1);
s32(1);
s64(1);
s8(1);
string(1);
u16(1);
u32(1);
u64(1);
u8(1);
struct({}, {});
export * from "@yume-chan/struct";