refactor: migrate to ES private fields

This commit is contained in:
Simon Chan 2023-07-11 21:05:39 +08:00
parent 433f9b986f
commit 7056feb3b1
No known key found for this signature in database
GPG key ID: A8B69F750B9BCEDD
108 changed files with 1264 additions and 1294 deletions

View file

@ -71,7 +71,7 @@ export default class AdbWebCredentialStore implements AdbCredentialStore {
* *
* @returns The private key in PKCS #8 format. * @returns The private key in PKCS #8 format.
*/ */
public async generateKey(): Promise<Uint8Array> { async generateKey(): Promise<Uint8Array> {
const { privateKey: cryptoKey } = await crypto.subtle.generateKey( const { privateKey: cryptoKey } = await crypto.subtle.generateKey(
{ {
name: "RSASSA-PKCS1-v1_5", name: "RSASSA-PKCS1-v1_5",
@ -97,7 +97,7 @@ export default class AdbWebCredentialStore implements AdbCredentialStore {
* *
* This method returns a generator, so `for await...of...` loop should be used to read the key. * This method returns a generator, so `for await...of...` loop should be used to read the key.
*/ */
public async *iterateKeys(): AsyncGenerator<Uint8Array, void, void> { async *iterateKeys(): AsyncGenerator<Uint8Array, void, void> {
for (const key of await getAllKeys()) { for (const key of await getAllKeys()) {
yield key; yield key;
} }

View file

@ -46,26 +46,26 @@ declare global {
} }
export default class AdbDaemonDirectSocketsDevice implements AdbDaemonDevice { export default class AdbDaemonDirectSocketsDevice implements AdbDaemonDevice {
public static isSupported(): boolean { static isSupported(): boolean {
return typeof globalThis.TCPSocket !== "undefined"; return typeof globalThis.TCPSocket !== "undefined";
} }
public readonly serial: string; readonly serial: string;
public readonly host: string; readonly host: string;
public readonly port: number; readonly port: number;
public name: string | undefined; name: string | undefined;
public constructor(host: string, port = 5555, name?: string) { constructor(host: string, port = 5555, name?: string) {
this.host = host; this.host = host;
this.port = port; this.port = port;
this.serial = `${host}:${port}`; this.serial = `${host}:${port}`;
this.name = name; this.name = name;
} }
public async connect() { async connect() {
const socket = new globalThis.TCPSocket(this.host, this.port, { const socket = new globalThis.TCPSocket(this.host, this.port, {
noDelay: true, noDelay: true,
}); });

View file

@ -77,16 +77,16 @@ class Uint8ArrayExactReadable implements ExactReadable {
#data: Uint8Array; #data: Uint8Array;
#position: number; #position: number;
public get position() { get position() {
return this.#position; return this.#position;
} }
public constructor(data: Uint8Array) { constructor(data: Uint8Array) {
this.#data = data; this.#data = data;
this.#position = 0; this.#position = 0;
} }
public readExactly(length: number): Uint8Array { readExactly(length: number): Uint8Array {
const result = this.#data.subarray( const result = this.#data.subarray(
this.#position, this.#position,
this.#position + length, this.#position + length,
@ -100,16 +100,16 @@ export class AdbDaemonWebUsbConnection
implements ReadableWritablePair<AdbPacketData, Consumable<AdbPacketInit>> implements ReadableWritablePair<AdbPacketData, Consumable<AdbPacketInit>>
{ {
#readable: ReadableStream<AdbPacketData>; #readable: ReadableStream<AdbPacketData>;
public get readable() { get readable() {
return this.#readable; return this.#readable;
} }
#writable: WritableStream<Consumable<AdbPacketInit>>; #writable: WritableStream<Consumable<AdbPacketInit>>;
public get writable() { get writable() {
return this.#writable; return this.#writable;
} }
public constructor( constructor(
device: USBDevice, device: USBDevice,
inEndpoint: USBEndpoint, inEndpoint: USBEndpoint,
outEndpoint: USBEndpoint, outEndpoint: USBEndpoint,
@ -249,15 +249,15 @@ export class AdbDaemonWebUsbDevice implements AdbDaemonDevice {
#usbManager: USB; #usbManager: USB;
#raw: USBDevice; #raw: USBDevice;
public get raw() { get raw() {
return this.#raw; return this.#raw;
} }
public get serial(): string { get serial(): string {
return this.#raw.serialNumber!; return this.#raw.serialNumber!;
} }
public get name(): string { get name(): string {
return this.#raw.productName!; return this.#raw.productName!;
} }
@ -267,7 +267,7 @@ export class AdbDaemonWebUsbDevice implements AdbDaemonDevice {
* @param device The `USBDevice` instance obtained elsewhere. * @param device The `USBDevice` instance obtained elsewhere.
* @param filters The filters to use when searching for ADB interface. Defaults to {@link ADB_DEFAULT_DEVICE_FILTER}. * @param filters The filters to use when searching for ADB interface. Defaults to {@link ADB_DEFAULT_DEVICE_FILTER}.
*/ */
public constructor( constructor(
device: USBDevice, device: USBDevice,
filters: AdbDeviceFilter[] = [ADB_DEFAULT_DEVICE_FILTER], filters: AdbDeviceFilter[] = [ADB_DEFAULT_DEVICE_FILTER],
usbManager: USB, usbManager: USB,
@ -281,7 +281,7 @@ export class AdbDaemonWebUsbDevice implements AdbDaemonDevice {
* Claim the device and create a pair of `AdbPacket` streams to the ADB interface. * Claim the device and create a pair of `AdbPacket` streams to the ADB interface.
* @returns The pair of `AdbPacket` streams. * @returns The pair of `AdbPacket` streams.
*/ */
public async connect(): Promise< async connect(): Promise<
ReadableWritablePair<AdbPacketData, Consumable<AdbPacketInit>> ReadableWritablePair<AdbPacketData, Consumable<AdbPacketInit>>
> { > {
if (!this.#raw.opened) { if (!this.#raw.opened) {

View file

@ -8,7 +8,7 @@ export class AdbDaemonWebUsbDeviceManager {
* *
* May be `undefined` if current runtime does not support WebUSB. * May be `undefined` if current runtime does not support WebUSB.
*/ */
public static readonly BROWSER = static readonly BROWSER =
typeof globalThis.navigator !== "undefined" && typeof globalThis.navigator !== "undefined" &&
!!globalThis.navigator.usb !!globalThis.navigator.usb
? new AdbDaemonWebUsbDeviceManager(globalThis.navigator.usb) ? new AdbDaemonWebUsbDeviceManager(globalThis.navigator.usb)
@ -20,7 +20,7 @@ export class AdbDaemonWebUsbDeviceManager {
* Create a new instance of {@link AdbDaemonWebUsbDeviceManager} using the specified WebUSB implementation. * Create a new instance of {@link AdbDaemonWebUsbDeviceManager} using the specified WebUSB implementation.
* @param usbManager A WebUSB compatible interface. * @param usbManager A WebUSB compatible interface.
*/ */
public constructor(usbManager: USB) { constructor(usbManager: USB) {
this.#usbManager = usbManager; this.#usbManager = usbManager;
} }
@ -37,7 +37,7 @@ export class AdbDaemonWebUsbDeviceManager {
* @returns An {@link AdbDaemonWebUsbDevice} instance if the user selected a device, * @returns An {@link AdbDaemonWebUsbDevice} instance if the user selected a device,
* or `undefined` if the user cancelled the device picker. * or `undefined` if the user cancelled the device picker.
*/ */
public async requestDevice( async requestDevice(
filters: AdbDeviceFilter[] = [ADB_DEFAULT_DEVICE_FILTER], filters: AdbDeviceFilter[] = [ADB_DEFAULT_DEVICE_FILTER],
): Promise<AdbDaemonWebUsbDevice | undefined> { ): Promise<AdbDaemonWebUsbDevice | undefined> {
try { try {
@ -67,7 +67,7 @@ export class AdbDaemonWebUsbDeviceManager {
* Defaults to {@link ADB_DEFAULT_DEVICE_FILTER}. * Defaults to {@link ADB_DEFAULT_DEVICE_FILTER}.
* @returns An array of {@link AdbDaemonWebUsbDevice} instances for all connected and authenticated devices. * @returns An array of {@link AdbDaemonWebUsbDevice} instances for all connected and authenticated devices.
*/ */
public async getDevices( async getDevices(
filters: AdbDeviceFilter[] = [ADB_DEFAULT_DEVICE_FILTER], filters: AdbDeviceFilter[] = [ADB_DEFAULT_DEVICE_FILTER],
): Promise<AdbDaemonWebUsbDevice[]> { ): Promise<AdbDaemonWebUsbDevice[]> {
const devices = await this.#usbManager.getDevices(); const devices = await this.#usbManager.getDevices();

View file

@ -2,27 +2,27 @@ export class AdbDaemonWebUsbDeviceWatcher {
#callback: (newDeviceSerial?: string) => void; #callback: (newDeviceSerial?: string) => void;
#usbManager: USB; #usbManager: USB;
public constructor(callback: (newDeviceSerial?: string) => void, usb: USB) { constructor(callback: (newDeviceSerial?: string) => void, usb: USB) {
this.#callback = callback; this.#callback = callback;
this.#usbManager = usb; this.#usbManager = usb;
this.#usbManager.addEventListener("connect", this.handleConnect); this.#usbManager.addEventListener("connect", this.#handleConnect);
this.#usbManager.addEventListener("disconnect", this.handleDisconnect); this.#usbManager.addEventListener("disconnect", this.#handleDisconnect);
} }
public dispose(): void { dispose(): void {
this.#usbManager.removeEventListener("connect", this.handleConnect); this.#usbManager.removeEventListener("connect", this.#handleConnect);
this.#usbManager.removeEventListener( this.#usbManager.removeEventListener(
"disconnect", "disconnect",
this.handleDisconnect, this.#handleDisconnect,
); );
} }
private handleConnect = (e: USBConnectionEvent) => { #handleConnect = (e: USBConnectionEvent) => {
this.#callback(e.device.serialNumber); this.#callback(e.device.serialNumber);
}; };
private handleDisconnect = () => { #handleDisconnect = () => {
this.#callback(); this.#callback();
}; };
} }

View file

@ -14,16 +14,16 @@ import {
} from "@yume-chan/stream-extra"; } from "@yume-chan/stream-extra";
export default class AdbDaemonWebSocketDevice implements AdbDaemonDevice { export default class AdbDaemonWebSocketDevice implements AdbDaemonDevice {
public readonly serial: string; readonly serial: string;
public name: string | undefined; name: string | undefined;
public constructor(url: string, name?: string) { constructor(url: string, name?: string) {
this.serial = url; this.serial = url;
this.name = name; this.name = name;
} }
public async connect() { async connect() {
const socket = new WebSocket(this.serial); const socket = new WebSocket(this.serial);
socket.binaryType = "arraybuffer"; socket.binaryType = "arraybuffer";

View file

@ -62,9 +62,9 @@ function concatStreams<T>(...streams: ReadableStream<T>[]): ReadableStream<T> {
} }
export class AdbScrcpyExitedError extends Error { export class AdbScrcpyExitedError extends Error {
public output: string[]; output: string[];
public constructor(output: string[]) { constructor(output: string[]) {
super("scrcpy server exited prematurely"); super("scrcpy server exited prematurely");
this.output = output; this.output = output;
} }
@ -98,7 +98,7 @@ export type AdbScrcpyAudioStreamMetadata =
| AdbScrcpyAudioStreamSuccessMetadata; | AdbScrcpyAudioStreamSuccessMetadata;
export class AdbScrcpyClient { export class AdbScrcpyClient {
public static async pushServer( static async pushServer(
adb: Adb, adb: Adb,
file: ReadableStream<Consumable<Uint8Array>>, file: ReadableStream<Consumable<Uint8Array>>,
filename = DEFAULT_SERVER_PATH, filename = DEFAULT_SERVER_PATH,
@ -114,7 +114,7 @@ export class AdbScrcpyClient {
} }
} }
public static async start( static async start(
adb: Adb, adb: Adb,
path: string, path: string,
version: string, version: string,
@ -213,7 +213,7 @@ export class AdbScrcpyClient {
* This method will modify the given `options`, * This method will modify the given `options`,
* so don't reuse it elsewhere. * so don't reuse it elsewhere.
*/ */
public static async getEncoders( static async getEncoders(
adb: Adb, adb: Adb,
path: string, path: string,
version: string, version: string,
@ -227,7 +227,7 @@ export class AdbScrcpyClient {
* This method will modify the given `options`, * This method will modify the given `options`,
* so don't reuse it elsewhere. * so don't reuse it elsewhere.
*/ */
public static async getDisplays( static async getDisplays(
adb: Adb, adb: Adb,
path: string, path: string,
version: string, version: string,
@ -237,51 +237,49 @@ export class AdbScrcpyClient {
return await options.getDisplays(adb, path, version); return await options.getDisplays(adb, path, version);
} }
private _options: AdbScrcpyOptions<object>; #options: AdbScrcpyOptions<object>;
private _process: AdbSubprocessProtocol; #process: AdbSubprocessProtocol;
private _stdout: ReadableStream<string>; #stdout: ReadableStream<string>;
public get stdout() { get stdout() {
return this._stdout; return this.#stdout;
} }
public get exit() { get exit() {
return this._process.exit; return this.#process.exit;
} }
private _screenWidth: number | undefined; #screenWidth: number | undefined;
public get screenWidth() { get screenWidth() {
return this._screenWidth; return this.#screenWidth;
} }
private _screenHeight: number | undefined; #screenHeight: number | undefined;
public get screenHeight() { get screenHeight() {
return this._screenHeight; return this.#screenHeight;
} }
private _videoStream: Promise<AdbScrcpyVideoStream> | undefined; #videoStream: Promise<AdbScrcpyVideoStream> | undefined;
public get videoStream() { get videoStream() {
return this._videoStream; return this.#videoStream;
} }
private _audioStream: Promise<AdbScrcpyAudioStreamMetadata> | undefined; #audioStream: Promise<AdbScrcpyAudioStreamMetadata> | undefined;
public get audioStream() { get audioStream() {
return this._audioStream; return this.#audioStream;
} }
private _controlMessageWriter: ScrcpyControlMessageWriter | undefined; #controlMessageWriter: ScrcpyControlMessageWriter | undefined;
public get controlMessageWriter() { get controlMessageWriter() {
return this._controlMessageWriter; return this.#controlMessageWriter;
} }
private _deviceMessageStream: #deviceMessageStream: ReadableStream<ScrcpyDeviceMessage> | undefined;
| ReadableStream<ScrcpyDeviceMessage> get deviceMessageStream() {
| undefined; return this.#deviceMessageStream;
public get deviceMessageStream() {
return this._deviceMessageStream;
} }
public constructor({ constructor({
options, options,
process, process,
stdout, stdout,
@ -289,36 +287,36 @@ export class AdbScrcpyClient {
audioStream, audioStream,
controlStream, controlStream,
}: AdbScrcpyClientInit) { }: AdbScrcpyClientInit) {
this._options = options; this.#options = options;
this._process = process; this.#process = process;
this._stdout = stdout; this.#stdout = stdout;
this._videoStream = videoStream this.#videoStream = videoStream
? this.createVideoStream(videoStream) ? this.#createVideoStream(videoStream)
: undefined; : undefined;
this._audioStream = audioStream this.#audioStream = audioStream
? this.createAudioStream(audioStream) ? this.#createAudioStream(audioStream)
: undefined; : undefined;
if (controlStream) { if (controlStream) {
this._controlMessageWriter = new ScrcpyControlMessageWriter( this.#controlMessageWriter = new ScrcpyControlMessageWriter(
controlStream.writable.getWriter(), controlStream.writable.getWriter(),
options, options,
); );
this._deviceMessageStream = controlStream.readable.pipeThrough( this.#deviceMessageStream = controlStream.readable.pipeThrough(
new ScrcpyDeviceMessageDeserializeStream(), new ScrcpyDeviceMessageDeserializeStream(),
); );
} }
} }
private async createVideoStream(initialStream: ReadableStream<Uint8Array>) { async #createVideoStream(initialStream: ReadableStream<Uint8Array>) {
const { stream, metadata } = const { stream, metadata } =
await this._options.parseVideoStreamMetadata(initialStream); await this.#options.parseVideoStreamMetadata(initialStream);
return { return {
stream: stream stream: stream
.pipeThrough(this._options.createMediaStreamTransformer()) .pipeThrough(this.#options.createMediaStreamTransformer())
.pipeThrough( .pipeThrough(
new InspectStream((packet) => { new InspectStream((packet) => {
if (packet.type === "configuration") { if (packet.type === "configuration") {
@ -327,16 +325,16 @@ export class AdbScrcpyClient {
const { croppedWidth, croppedHeight } = const { croppedWidth, croppedHeight } =
h264ParseConfiguration(packet.data); h264ParseConfiguration(packet.data);
this._screenWidth = croppedWidth; this.#screenWidth = croppedWidth;
this._screenHeight = croppedHeight; this.#screenHeight = croppedHeight;
break; break;
} }
case ScrcpyVideoCodecId.H265: { case ScrcpyVideoCodecId.H265: {
const { croppedWidth, croppedHeight } = const { croppedWidth, croppedHeight } =
h265ParseConfiguration(packet.data); h265ParseConfiguration(packet.data);
this._screenWidth = croppedWidth; this.#screenWidth = croppedWidth;
this._screenHeight = croppedHeight; this.#screenHeight = croppedHeight;
break; break;
} }
} }
@ -347,10 +345,10 @@ export class AdbScrcpyClient {
}; };
} }
private async createAudioStream( async #createAudioStream(
initialStream: ReadableStream<Uint8Array>, initialStream: ReadableStream<Uint8Array>,
): Promise<AdbScrcpyAudioStreamMetadata> { ): Promise<AdbScrcpyAudioStreamMetadata> {
const metadata = await this._options.parseAudioStreamMetadata( const metadata = await this.#options.parseAudioStreamMetadata(
initialStream, initialStream,
); );
@ -362,7 +360,7 @@ export class AdbScrcpyClient {
return { return {
...metadata, ...metadata,
stream: metadata.stream.pipeThrough( stream: metadata.stream.pipeThrough(
this._options.createMediaStreamTransformer(), this.#options.createMediaStreamTransformer(),
), ),
}; };
default: default:
@ -374,7 +372,7 @@ export class AdbScrcpyClient {
} }
} }
public async close() { async close() {
await this._process.kill(); await this.#process.kill();
} }
} }

View file

@ -47,13 +47,13 @@ export abstract class AdbScrcpyConnection implements Disposable {
protected socketName: string; protected socketName: string;
public constructor(adb: Adb, options: AdbScrcpyConnectionOptions) { constructor(adb: Adb, options: AdbScrcpyConnectionOptions) {
this.adb = adb; this.adb = adb;
this.options = options; this.options = options;
this.socketName = this.getSocketName(); this.socketName = this.getSocketName();
} }
public initialize(): ValueOrPromise<void> { initialize(): ValueOrPromise<void> {
// pure virtual method // pure virtual method
} }
@ -65,28 +65,28 @@ export abstract class AdbScrcpyConnection implements Disposable {
return socketName; return socketName;
} }
public abstract getStreams(): ValueOrPromise<AdbScrcpyConnectionStreams>; abstract getStreams(): ValueOrPromise<AdbScrcpyConnectionStreams>;
public dispose(): void { dispose(): void {
// pure virtual method // pure virtual method
} }
} }
export class AdbScrcpyForwardConnection extends AdbScrcpyConnection { export class AdbScrcpyForwardConnection extends AdbScrcpyConnection {
private _disposed = false; #disposed = false;
private connect(): Promise< #connect(): Promise<
ReadableWritablePair<Uint8Array, Consumable<Uint8Array>> ReadableWritablePair<Uint8Array, Consumable<Uint8Array>>
> { > {
return this.adb.createSocket(this.socketName); return this.adb.createSocket(this.socketName);
} }
private async connectAndRetry( async #connectAndRetry(
sendDummyByte: boolean, sendDummyByte: boolean,
): Promise<ReadableWritablePair<Uint8Array, Consumable<Uint8Array>>> { ): Promise<ReadableWritablePair<Uint8Array, Consumable<Uint8Array>>> {
for (let i = 0; !this._disposed && i < 100; i += 1) { for (let i = 0; !this.#disposed && i < 100; i += 1) {
try { try {
const stream = await this.connect(); const stream = await this.#connect();
if (sendDummyByte) { if (sendDummyByte) {
// Can't guarantee the stream will preserve message boundaries, // Can't guarantee the stream will preserve message boundaries,
// so buffer the stream // so buffer the stream
@ -108,25 +108,25 @@ export class AdbScrcpyForwardConnection extends AdbScrcpyConnection {
throw new Error(`Can't connect to server after 100 retries`); throw new Error(`Can't connect to server after 100 retries`);
} }
public override async getStreams(): Promise<AdbScrcpyConnectionStreams> { override async getStreams(): Promise<AdbScrcpyConnectionStreams> {
let { sendDummyByte } = this.options; let { sendDummyByte } = this.options;
const streams: AdbScrcpyConnectionStreams = {}; const streams: AdbScrcpyConnectionStreams = {};
if (this.options.video) { if (this.options.video) {
const video = await this.connectAndRetry(sendDummyByte); const video = await this.#connectAndRetry(sendDummyByte);
streams.video = video.readable; streams.video = video.readable;
sendDummyByte = false; sendDummyByte = false;
} }
if (this.options.audio) { if (this.options.audio) {
const audio = await this.connectAndRetry(sendDummyByte); const audio = await this.#connectAndRetry(sendDummyByte);
streams.audio = audio.readable; streams.audio = audio.readable;
sendDummyByte = false; sendDummyByte = false;
} }
if (this.options.control) { if (this.options.control) {
const control = await this.connectAndRetry(sendDummyByte); const control = await this.#connectAndRetry(sendDummyByte);
sendDummyByte = false; sendDummyByte = false;
streams.control = control; streams.control = control;
} }
@ -134,19 +134,19 @@ export class AdbScrcpyForwardConnection extends AdbScrcpyConnection {
return streams; return streams;
} }
public override dispose(): void { override dispose(): void {
this._disposed = true; this.#disposed = true;
} }
} }
export class AdbScrcpyReverseConnection extends AdbScrcpyConnection { export class AdbScrcpyReverseConnection extends AdbScrcpyConnection {
private streams!: ReadableStreamDefaultReader< #streams!: ReadableStreamDefaultReader<
ReadableWritablePair<Uint8Array, Consumable<Uint8Array>> ReadableWritablePair<Uint8Array, Consumable<Uint8Array>>
>; >;
private address!: string; #address!: string;
public override async initialize(): Promise<void> { override async initialize(): Promise<void> {
// try to unbind first // try to unbind first
await this.adb.reverse.remove(this.socketName).catch((e) => { await this.adb.reverse.remove(this.socketName).catch((e) => {
if (e instanceof AdbReverseNotSupportedError) { if (e instanceof AdbReverseNotSupportedError) {
@ -160,45 +160,48 @@ export class AdbScrcpyReverseConnection extends AdbScrcpyConnection {
ReadableWritablePair<Uint8Array, Consumable<Uint8Array>>, ReadableWritablePair<Uint8Array, Consumable<Uint8Array>>,
ReadableWritablePair<Uint8Array, Consumable<Uint8Array>> ReadableWritablePair<Uint8Array, Consumable<Uint8Array>>
>(); >();
this.streams = queue.readable.getReader(); this.#streams = queue.readable.getReader();
const writer = queue.writable.getWriter(); const writer = queue.writable.getWriter();
this.address = await this.adb.reverse.add(this.socketName, (socket) => { this.#address = await this.adb.reverse.add(
this.socketName,
(socket) => {
void writer.write(socket); void writer.write(socket);
}); },
);
} }
private async accept(): Promise< async #accept(): Promise<
ReadableWritablePair<Uint8Array, Consumable<Uint8Array>> ReadableWritablePair<Uint8Array, Consumable<Uint8Array>>
> { > {
return (await this.streams.read()).value!; return (await this.#streams.read()).value!;
} }
public async getStreams(): Promise<AdbScrcpyConnectionStreams> { async getStreams(): Promise<AdbScrcpyConnectionStreams> {
const streams: AdbScrcpyConnectionStreams = {}; const streams: AdbScrcpyConnectionStreams = {};
if (this.options.video) { if (this.options.video) {
const video = await this.accept(); const video = await this.#accept();
streams.video = video.readable; streams.video = video.readable;
} }
if (this.options.audio) { if (this.options.audio) {
const audio = await this.accept(); const audio = await this.#accept();
streams.audio = audio.readable; streams.audio = audio.readable;
} }
if (this.options.control) { if (this.options.control) {
const control = await this.accept(); const control = await this.#accept();
streams.control = control; streams.control = control;
} }
return streams; return streams;
} }
public override dispose() { override dispose() {
// Don't await this! // Don't await this!
// `reverse.remove`'s response will never arrive // `reverse.remove`'s response will never arrive
// before we read all pending data from Scrcpy streams // before we read all pending data from Scrcpy streams
// NOOP: failed to remove reverse tunnel is not a big deal // NOOP: failed to remove reverse tunnel is not a big deal
this.adb.reverse.remove(this.address).catch(NOOP); this.adb.reverse.remove(this.#address).catch(NOOP);
} }
} }

View file

@ -20,7 +20,7 @@ import type { AdbScrcpyOptions } from "./types.js";
import { AdbScrcpyOptionsBase } from "./types.js"; import { AdbScrcpyOptionsBase } from "./types.js";
export class AdbScrcpyOptions1_16 extends AdbScrcpyOptionsBase<ScrcpyOptionsInit1_16> { export class AdbScrcpyOptions1_16 extends AdbScrcpyOptionsBase<ScrcpyOptionsInit1_16> {
public static createConnection( static createConnection(
adb: Adb, adb: Adb,
connectionOptions: AdbScrcpyConnectionOptions, connectionOptions: AdbScrcpyConnectionOptions,
tunnelForward: boolean, tunnelForward: boolean,
@ -32,7 +32,7 @@ export class AdbScrcpyOptions1_16 extends AdbScrcpyOptionsBase<ScrcpyOptionsInit
} }
} }
public static async getEncoders( static async getEncoders(
adb: Adb, adb: Adb,
path: string, path: string,
version: string, version: string,
@ -55,7 +55,7 @@ export class AdbScrcpyOptions1_16 extends AdbScrcpyOptionsBase<ScrcpyOptionsInit
return encoders; return encoders;
} }
public static async getDisplays( static async getDisplays(
adb: Adb, adb: Adb,
path: string, path: string,
version: string, version: string,
@ -90,7 +90,7 @@ export class AdbScrcpyOptions1_16 extends AdbScrcpyOptionsBase<ScrcpyOptionsInit
} }
} }
public override getEncoders( override getEncoders(
adb: Adb, adb: Adb,
path: string, path: string,
version: string, version: string,
@ -98,7 +98,7 @@ export class AdbScrcpyOptions1_16 extends AdbScrcpyOptionsBase<ScrcpyOptionsInit
return AdbScrcpyOptions1_16.getEncoders(adb, path, version, this); return AdbScrcpyOptions1_16.getEncoders(adb, path, version, this);
} }
public override getDisplays( override getDisplays(
adb: Adb, adb: Adb,
path: string, path: string,
version: string, version: string,
@ -106,7 +106,7 @@ export class AdbScrcpyOptions1_16 extends AdbScrcpyOptionsBase<ScrcpyOptionsInit
return AdbScrcpyOptions1_16.getDisplays(adb, path, version, this); return AdbScrcpyOptions1_16.getDisplays(adb, path, version, this);
} }
public override createConnection(adb: Adb): AdbScrcpyConnection { override createConnection(adb: Adb): AdbScrcpyConnection {
return AdbScrcpyOptions1_16.createConnection( return AdbScrcpyOptions1_16.createConnection(
adb, adb,
{ {

View file

@ -11,7 +11,7 @@ import { AdbScrcpyOptions1_16 } from "./1_16.js";
import { AdbScrcpyOptionsBase } from "./types.js"; import { AdbScrcpyOptionsBase } from "./types.js";
export class AdbScrcpyOptions1_22 extends AdbScrcpyOptionsBase<ScrcpyOptionsInit1_22> { export class AdbScrcpyOptions1_22 extends AdbScrcpyOptionsBase<ScrcpyOptionsInit1_22> {
public override getEncoders( override getEncoders(
adb: Adb, adb: Adb,
path: string, path: string,
version: string, version: string,
@ -19,7 +19,7 @@ export class AdbScrcpyOptions1_22 extends AdbScrcpyOptionsBase<ScrcpyOptionsInit
return AdbScrcpyOptions1_16.getEncoders(adb, path, version, this); return AdbScrcpyOptions1_16.getEncoders(adb, path, version, this);
} }
public override getDisplays( override getDisplays(
adb: Adb, adb: Adb,
path: string, path: string,
version: string, version: string,
@ -27,7 +27,7 @@ export class AdbScrcpyOptions1_22 extends AdbScrcpyOptionsBase<ScrcpyOptionsInit
return AdbScrcpyOptions1_16.getDisplays(adb, path, version, this); return AdbScrcpyOptions1_16.getDisplays(adb, path, version, this);
} }
public override createConnection(adb: Adb): AdbScrcpyConnection { override createConnection(adb: Adb): AdbScrcpyConnection {
return AdbScrcpyOptions1_16.createConnection( return AdbScrcpyOptions1_16.createConnection(
adb, adb,
{ {

View file

@ -13,7 +13,7 @@ import type { AdbScrcpyOptions } from "./types.js";
import { AdbScrcpyOptionsBase } from "./types.js"; import { AdbScrcpyOptionsBase } from "./types.js";
export class AdbScrcpyOptions2_0 extends AdbScrcpyOptionsBase<ScrcpyOptionsInit2_0> { export class AdbScrcpyOptions2_0 extends AdbScrcpyOptionsBase<ScrcpyOptionsInit2_0> {
public static async getEncoders( static async getEncoders(
adb: Adb, adb: Adb,
path: string, path: string,
version: string, version: string,
@ -48,7 +48,7 @@ export class AdbScrcpyOptions2_0 extends AdbScrcpyOptionsBase<ScrcpyOptionsInit2
} }
} }
public override async getEncoders( override async getEncoders(
adb: Adb, adb: Adb,
path: string, path: string,
version: string, version: string,
@ -56,7 +56,7 @@ export class AdbScrcpyOptions2_0 extends AdbScrcpyOptionsBase<ScrcpyOptionsInit2
return AdbScrcpyOptions2_0.getEncoders(adb, path, version, this); return AdbScrcpyOptions2_0.getEncoders(adb, path, version, this);
} }
public override getDisplays( override getDisplays(
adb: Adb, adb: Adb,
path: string, path: string,
version: string, version: string,
@ -64,7 +64,7 @@ export class AdbScrcpyOptions2_0 extends AdbScrcpyOptionsBase<ScrcpyOptionsInit2
return AdbScrcpyOptions1_16.getDisplays(adb, path, version, this); return AdbScrcpyOptions1_16.getDisplays(adb, path, version, this);
} }
public override createConnection(adb: Adb): AdbScrcpyConnection { override createConnection(adb: Adb): AdbScrcpyConnection {
return AdbScrcpyOptions1_16.createConnection( return AdbScrcpyOptions1_16.createConnection(
adb, adb,
{ {

View file

@ -12,7 +12,7 @@ import { AdbScrcpyOptions2_0 } from "./2_0.js";
import { AdbScrcpyOptionsBase } from "./types.js"; import { AdbScrcpyOptionsBase } from "./types.js";
export class AdbScrcpyOptions2_1 extends AdbScrcpyOptionsBase<ScrcpyOptionsInit2_1> { export class AdbScrcpyOptions2_1 extends AdbScrcpyOptionsBase<ScrcpyOptionsInit2_1> {
public override async getEncoders( override async getEncoders(
adb: Adb, adb: Adb,
path: string, path: string,
version: string, version: string,
@ -20,7 +20,7 @@ export class AdbScrcpyOptions2_1 extends AdbScrcpyOptionsBase<ScrcpyOptionsInit2
return AdbScrcpyOptions2_0.getEncoders(adb, path, version, this); return AdbScrcpyOptions2_0.getEncoders(adb, path, version, this);
} }
public override getDisplays( override getDisplays(
adb: Adb, adb: Adb,
path: string, path: string,
version: string, version: string,
@ -28,7 +28,7 @@ export class AdbScrcpyOptions2_1 extends AdbScrcpyOptionsBase<ScrcpyOptionsInit2
return AdbScrcpyOptions1_16.getDisplays(adb, path, version, this); return AdbScrcpyOptions1_16.getDisplays(adb, path, version, this);
} }
public override createConnection(adb: Adb): AdbScrcpyConnection { override createConnection(adb: Adb): AdbScrcpyConnection {
return AdbScrcpyOptions1_16.createConnection( return AdbScrcpyOptions1_16.createConnection(
adb, adb,
{ {

View file

@ -34,31 +34,31 @@ export abstract class AdbScrcpyOptionsBase<T extends object>
extends ScrcpyOptionsBase<T, ScrcpyOptions<T>> extends ScrcpyOptionsBase<T, ScrcpyOptions<T>>
implements AdbScrcpyOptions<T> implements AdbScrcpyOptions<T>
{ {
public override get defaults(): Required<T> { override get defaults(): Required<T> {
return this._base.defaults; return this._base.defaults;
} }
public tunnelForwardOverride = false; tunnelForwardOverride = false;
public constructor(base: ScrcpyOptions<T>) { constructor(base: ScrcpyOptions<T>) {
super(base, base.value); super(base, base.value);
} }
public serialize(): string[] { serialize(): string[] {
return this._base.serialize(); return this._base.serialize();
} }
public abstract getEncoders( abstract getEncoders(
adb: Adb, adb: Adb,
path: string, path: string,
version: string, version: string,
): Promise<ScrcpyEncoder[]>; ): Promise<ScrcpyEncoder[]>;
public abstract getDisplays( abstract getDisplays(
adb: Adb, adb: Adb,
path: string, path: string,
version: string, version: string,
): Promise<ScrcpyDisplay[]>; ): Promise<ScrcpyDisplay[]>;
public abstract createConnection(adb: Adb): AdbScrcpyConnection; abstract createConnection(adb: Adb): AdbScrcpyConnection;
} }

View file

@ -58,15 +58,15 @@ function nodeSocketToStreamPair(socket: Socket) {
} }
export class AdbServerNodeTcpConnection implements AdbServerConnection { export class AdbServerNodeTcpConnection implements AdbServerConnection {
public readonly spec: SocketConnectOpts; readonly spec: SocketConnectOpts;
private readonly _listeners = new Map<string, Server>(); readonly #listeners = new Map<string, Server>();
public constructor(spec: SocketConnectOpts) { constructor(spec: SocketConnectOpts) {
this.spec = spec; this.spec = spec;
} }
public async connect( async connect(
{ unref }: AdbServerConnectionOptions = { unref: false } { unref }: AdbServerConnectionOptions = { unref: false }
): Promise<ReadableWritablePair<Uint8Array, Uint8Array>> { ): Promise<ReadableWritablePair<Uint8Array, Uint8Array>> {
const socket = new Socket(); const socket = new Socket();
@ -81,7 +81,7 @@ export class AdbServerNodeTcpConnection implements AdbServerConnection {
return nodeSocketToStreamPair(socket); return nodeSocketToStreamPair(socket);
} }
public async addReverseTunnel( async addReverseTunnel(
handler: AdbIncomingSocketHandler, handler: AdbIncomingSocketHandler,
address?: string address?: string
): Promise<string> { ): Promise<string> {
@ -127,23 +127,23 @@ export class AdbServerNodeTcpConnection implements AdbServerConnection {
address = `tcp:${info.address}:${info.port}`; address = `tcp:${info.address}:${info.port}`;
} }
this._listeners.set(address, server); this.#listeners.set(address, server);
return address; return address;
} }
removeReverseTunnel(address: string): ValueOrPromise<void> { removeReverseTunnel(address: string): ValueOrPromise<void> {
const server = this._listeners.get(address); const server = this.#listeners.get(address);
if (!server) { if (!server) {
return; return;
} }
server.close(); server.close();
this._listeners.delete(address); this.#listeners.delete(address);
} }
clearReverseTunnels(): ValueOrPromise<void> { clearReverseTunnels(): ValueOrPromise<void> {
for (const server of this._listeners.values()) { for (const server of this.#listeners.values()) {
server.close(); server.close();
} }
this._listeners.clear(); this.#listeners.clear();
} }
} }

View file

@ -51,30 +51,30 @@ export interface AdbTransport extends Closeable {
} }
export class Adb implements Closeable { export class Adb implements Closeable {
public readonly transport: AdbTransport; readonly transport: AdbTransport;
public get serial() { get serial() {
return this.transport.serial; return this.transport.serial;
} }
public get maxPayloadSize() { get maxPayloadSize() {
return this.transport.maxPayloadSize; return this.transport.maxPayloadSize;
} }
public get banner() { get banner() {
return this.transport.banner; return this.transport.banner;
} }
public get disconnected() { get disconnected() {
return this.transport.disconnected; return this.transport.disconnected;
} }
public readonly subprocess: AdbSubprocess; readonly subprocess: AdbSubprocess;
public readonly power: AdbPower; readonly power: AdbPower;
public readonly reverse: AdbReverseCommand; readonly reverse: AdbReverseCommand;
public readonly tcpip: AdbTcpIpCommand; readonly tcpip: AdbTcpIpCommand;
public constructor(transport: AdbTransport) { constructor(transport: AdbTransport) {
this.transport = transport; this.transport = transport;
this.subprocess = new AdbSubprocess(this); this.subprocess = new AdbSubprocess(this);
@ -83,22 +83,22 @@ export class Adb implements Closeable {
this.tcpip = new AdbTcpIpCommand(this); this.tcpip = new AdbTcpIpCommand(this);
} }
public supportsFeature(feature: AdbFeature): boolean { supportsFeature(feature: AdbFeature): boolean {
return this.banner.features.includes(feature); return this.banner.features.includes(feature);
} }
public async createSocket(service: string): Promise<AdbSocket> { async createSocket(service: string): Promise<AdbSocket> {
return this.transport.connect(service); return this.transport.connect(service);
} }
public async createSocketAndWait(service: string): Promise<string> { async createSocketAndWait(service: string): Promise<string> {
const socket = await this.createSocket(service); const socket = await this.createSocket(service);
return await socket.readable return await socket.readable
.pipeThrough(new DecodeUtf8Stream()) .pipeThrough(new DecodeUtf8Stream())
.pipeThrough(new ConcatStringStream()); .pipeThrough(new ConcatStringStream());
} }
public async getProp(key: string): Promise<string> { async getProp(key: string): Promise<string> {
const stdout = await this.subprocess.spawnAndWaitLegacy([ const stdout = await this.subprocess.spawnAndWaitLegacy([
"getprop", "getprop",
key, key,
@ -106,7 +106,7 @@ export class Adb implements Closeable {
return stdout.trim(); return stdout.trim();
} }
public async rm(...filenames: string[]): Promise<string> { async rm(...filenames: string[]): Promise<string> {
// https://android.googlesource.com/platform/packages/modules/adb/+/1a0fb8846d4e6b671c8aa7f137a8c21d7b248716/client/adb_install.cpp#984 // https://android.googlesource.com/platform/packages/modules/adb/+/1a0fb8846d4e6b671c8aa7f137a8c21d7b248716/client/adb_install.cpp#984
const stdout = await this.subprocess.spawnAndWaitLegacy([ const stdout = await this.subprocess.spawnAndWaitLegacy([
"rm", "rm",
@ -116,16 +116,16 @@ export class Adb implements Closeable {
return stdout; return stdout;
} }
public async sync(): Promise<AdbSync> { async sync(): Promise<AdbSync> {
const socket = await this.createSocket("sync:"); const socket = await this.createSocket("sync:");
return new AdbSync(this, socket); return new AdbSync(this, socket);
} }
public async framebuffer(): Promise<AdbFrameBuffer> { async framebuffer(): Promise<AdbFrameBuffer> {
return framebuffer(this); return framebuffer(this);
} }
public async close(): Promise<void> { async close(): Promise<void> {
await this.transport.close(); await this.transport.close();
} }
} }

View file

@ -8,7 +8,7 @@ export enum AdbBannerKey {
} }
export class AdbBanner { export class AdbBanner {
public static parse(banner: string) { static parse(banner: string) {
let product: string | undefined; let product: string | undefined;
let model: string | undefined; let model: string | undefined;
let device: string | undefined; let device: string | undefined;
@ -49,26 +49,26 @@ export class AdbBanner {
} }
#product: string | undefined; #product: string | undefined;
public get product() { get product() {
return this.#product; return this.#product;
} }
#model: string | undefined; #model: string | undefined;
public get model() { get model() {
return this.#model; return this.#model;
} }
#device: string | undefined; #device: string | undefined;
public get device() { get device() {
return this.#device; return this.#device;
} }
#features: AdbFeature[] = []; #features: AdbFeature[] = [];
public get features() { get features() {
return this.#features; return this.#features;
} }
public constructor( constructor(
product: string | undefined, product: string | undefined,
model: string | undefined, model: string | undefined,
device: string | undefined, device: string | undefined,

View file

@ -5,7 +5,7 @@ import type { Adb } from "../adb.js";
export class AdbCommandBase extends AutoDisposable { export class AdbCommandBase extends AutoDisposable {
protected adb: Adb; protected adb: Adb;
public constructor(adb: Adb) { constructor(adb: Adb) {
super(); super();
this.adb = adb; this.adb = adb;
} }

View file

@ -6,23 +6,23 @@
import { AdbCommandBase } from "./base.js"; import { AdbCommandBase } from "./base.js";
export class AdbPower extends AdbCommandBase { export class AdbPower extends AdbCommandBase {
public reboot(mode = "") { reboot(mode = "") {
return this.adb.createSocketAndWait(`reboot:${mode}`); return this.adb.createSocketAndWait(`reboot:${mode}`);
} }
public bootloader() { bootloader() {
return this.reboot("bootloader"); return this.reboot("bootloader");
} }
public fastboot() { fastboot() {
return this.reboot("fastboot"); return this.reboot("fastboot");
} }
public recovery() { recovery() {
return this.reboot("recovery"); return this.reboot("recovery");
} }
public sideload() { sideload() {
return this.reboot("sideload"); return this.reboot("sideload");
} }
@ -31,15 +31,15 @@ export class AdbPower extends AdbCommandBase {
* *
* Only works on some Qualcomm devices. * Only works on some Qualcomm devices.
*/ */
public qualcommEdlMode() { qualcommEdlMode() {
return this.reboot("edl"); return this.reboot("edl");
} }
public powerOff() { powerOff() {
return this.adb.subprocess.spawnAndWaitLegacy(["reboot", "-p"]); return this.adb.subprocess.spawnAndWaitLegacy(["reboot", "-p"]);
} }
public powerButton(longPress = false) { powerButton(longPress = false) {
return this.adb.subprocess.spawnAndWaitLegacy([ return this.adb.subprocess.spawnAndWaitLegacy([
"input", "input",
"keyevent", "keyevent",
@ -52,7 +52,7 @@ export class AdbPower extends AdbCommandBase {
* *
* Only works on Samsung devices. * Only works on Samsung devices.
*/ */
public samsungOdin() { samsungOdin() {
return this.reboot("download"); return this.reboot("download");
} }
} }

View file

@ -20,14 +20,14 @@ const AdbReverseStringResponse = new Struct()
.string("content", { lengthField: "length", lengthFieldRadix: 16 }); .string("content", { lengthField: "length", lengthFieldRadix: 16 });
export class AdbReverseError extends Error { export class AdbReverseError extends Error {
public constructor(message: string) { constructor(message: string) {
super(message); super(message);
Object.setPrototypeOf(this, new.target.prototype); Object.setPrototypeOf(this, new.target.prototype);
} }
} }
export class AdbReverseNotSupportedError extends AdbReverseError { export class AdbReverseNotSupportedError extends AdbReverseError {
public constructor() { constructor() {
super( super(
"ADB reverse tunnel is not supported on this device when connected wirelessly.", "ADB reverse tunnel is not supported on this device when connected wirelessly.",
); );
@ -57,7 +57,7 @@ export class AdbReverseCommand extends AutoDisposable {
readonly #deviceAddressToLocalAddress = new Map<string, string>(); readonly #deviceAddressToLocalAddress = new Map<string, string>();
public constructor(adb: Adb) { constructor(adb: Adb) {
super(); super();
this.adb = adb; this.adb = adb;
@ -77,7 +77,7 @@ export class AdbReverseCommand extends AutoDisposable {
return stream; return stream;
} }
public async list(): Promise<AdbForwardListener[]> { async list(): Promise<AdbForwardListener[]> {
const stream = await this.createBufferedStream("reverse:list-forward"); const stream = await this.createBufferedStream("reverse:list-forward");
const response = await AdbReverseStringResponse.deserialize(stream); const response = await AdbReverseStringResponse.deserialize(stream);
@ -99,7 +99,7 @@ export class AdbReverseCommand extends AutoDisposable {
* @param localAddress The address that listens on the local machine. * @param localAddress The address that listens on the local machine.
* @returns `tcp:{ACTUAL_LISTENING_PORT}`, If `deviceAddress` is `tcp:0`; otherwise, `deviceAddress`. * @returns `tcp:{ACTUAL_LISTENING_PORT}`, If `deviceAddress` is `tcp:0`; otherwise, `deviceAddress`.
*/ */
public async addExternal(deviceAddress: string, localAddress: string) { async addExternal(deviceAddress: string, localAddress: string) {
const stream = await this.sendRequest( const stream = await this.sendRequest(
`reverse:forward:${deviceAddress};${localAddress}`, `reverse:forward:${deviceAddress};${localAddress}`,
); );
@ -137,7 +137,7 @@ export class AdbReverseCommand extends AutoDisposable {
* @throws {AdbReverseNotSupportedError} If ADB reverse tunnel is not supported on this device when connected wirelessly. * @throws {AdbReverseNotSupportedError} If ADB reverse tunnel is not supported on this device when connected wirelessly.
* @throws {AdbReverseError} If ADB daemon returns an error. * @throws {AdbReverseError} If ADB daemon returns an error.
*/ */
public async add( async add(
deviceAddress: string, deviceAddress: string,
handler: AdbIncomingSocketHandler, handler: AdbIncomingSocketHandler,
localAddress?: string, localAddress?: string,
@ -157,7 +157,7 @@ export class AdbReverseCommand extends AutoDisposable {
} }
} }
public async remove(deviceAddress: string): Promise<void> { async remove(deviceAddress: string): Promise<void> {
const localAddress = const localAddress =
this.#deviceAddressToLocalAddress.get(deviceAddress); this.#deviceAddressToLocalAddress.get(deviceAddress);
if (localAddress) { if (localAddress) {
@ -169,7 +169,7 @@ export class AdbReverseCommand extends AutoDisposable {
// No need to close the stream, device will close it // No need to close the stream, device will close it
} }
public async removeAll(): Promise<void> { async removeAll(): Promise<void> {
await this.adb.transport.clearReverseTunnels(); await this.adb.transport.clearReverseTunnels();
this.#deviceAddressToLocalAddress.clear(); this.#deviceAddressToLocalAddress.clear();

View file

@ -37,7 +37,7 @@ export interface AdbSubprocessWaitResult {
} }
export class AdbSubprocess extends AdbCommandBase { export class AdbSubprocess extends AdbCommandBase {
private async createProtocol( async #createProtocol(
mode: "pty" | "raw", mode: "pty" | "raw",
command?: string | string[], command?: string | string[],
options?: Partial<AdbSubprocessOptions>, options?: Partial<AdbSubprocessOptions>,
@ -75,11 +75,11 @@ export class AdbSubprocess extends AdbCommandBase {
* @param options The options for creating the `AdbSubprocessProtocol` * @param options The options for creating the `AdbSubprocessProtocol`
* @returns A new `AdbSubprocessProtocol` instance connecting to the spawned process. * @returns A new `AdbSubprocessProtocol` instance connecting to the spawned process.
*/ */
public shell( shell(
command?: string | string[], command?: string | string[],
options?: Partial<AdbSubprocessOptions>, options?: Partial<AdbSubprocessOptions>,
): Promise<AdbSubprocessProtocol> { ): Promise<AdbSubprocessProtocol> {
return this.createProtocol("pty", command, options); return this.#createProtocol("pty", command, options);
} }
/** /**
@ -91,11 +91,11 @@ export class AdbSubprocess extends AdbCommandBase {
* @param options The options for creating the `AdbSubprocessProtocol` * @param options The options for creating the `AdbSubprocessProtocol`
* @returns A new `AdbSubprocessProtocol` instance connecting to the spawned process. * @returns A new `AdbSubprocessProtocol` instance connecting to the spawned process.
*/ */
public spawn( spawn(
command: string | string[], command: string | string[],
options?: Partial<AdbSubprocessOptions>, options?: Partial<AdbSubprocessOptions>,
): Promise<AdbSubprocessProtocol> { ): Promise<AdbSubprocessProtocol> {
return this.createProtocol("raw", command, options); return this.#createProtocol("raw", command, options);
} }
/** /**
@ -104,7 +104,7 @@ export class AdbSubprocess extends AdbCommandBase {
* @param options The options for creating the `AdbSubprocessProtocol` * @param options The options for creating the `AdbSubprocessProtocol`
* @returns The entire output of the command * @returns The entire output of the command
*/ */
public async spawnAndWait( async spawnAndWait(
command: string | string[], command: string | string[],
options?: Partial<AdbSubprocessOptions>, options?: Partial<AdbSubprocessOptions>,
): Promise<AdbSubprocessWaitResult> { ): Promise<AdbSubprocessWaitResult> {
@ -132,9 +132,7 @@ export class AdbSubprocess extends AdbCommandBase {
* @param command The command to run * @param command The command to run
* @returns The entire output of the command * @returns The entire output of the command
*/ */
public async spawnAndWaitLegacy( async spawnAndWaitLegacy(command: string | string[]): Promise<string> {
command: string | string[],
): Promise<string> {
const { stdout } = await this.spawnAndWait(command, { const { stdout } = await this.spawnAndWait(command, {
protocols: [AdbSubprocessNoneProtocol], protocols: [AdbSubprocessNoneProtocol],
}); });

View file

@ -13,17 +13,17 @@ import type { AdbSubprocessProtocol } from "./types.js";
* * `resize`: No * * `resize`: No
*/ */
export class AdbSubprocessNoneProtocol implements AdbSubprocessProtocol { export class AdbSubprocessNoneProtocol implements AdbSubprocessProtocol {
public static isSupported() { static isSupported() {
return true; return true;
} }
public static async pty(adb: Adb, command: string) { static async pty(adb: Adb, command: string) {
return new AdbSubprocessNoneProtocol( return new AdbSubprocessNoneProtocol(
await adb.createSocket(`shell:${command}`), await adb.createSocket(`shell:${command}`),
); );
} }
public static async raw(adb: Adb, command: string) { static async raw(adb: Adb, command: string) {
// `shell,raw:${command}` also triggers raw mode, // `shell,raw:${command}` also triggers raw mode,
// But is not supported on Android version <7. // But is not supported on Android version <7.
return new AdbSubprocessNoneProtocol( return new AdbSubprocessNoneProtocol(
@ -36,7 +36,7 @@ export class AdbSubprocessNoneProtocol implements AdbSubprocessProtocol {
readonly #duplex: DuplexStreamFactory<Uint8Array, Uint8Array>; readonly #duplex: DuplexStreamFactory<Uint8Array, Uint8Array>;
// Legacy shell forwards all data to stdin. // Legacy shell forwards all data to stdin.
public get stdin() { get stdin() {
return this.#socket.writable; return this.#socket.writable;
} }
@ -44,7 +44,7 @@ export class AdbSubprocessNoneProtocol implements AdbSubprocessProtocol {
/** /**
* Legacy shell mixes stdout and stderr. * Legacy shell mixes stdout and stderr.
*/ */
public get stdout() { get stdout() {
return this.#stdout; return this.#stdout;
} }
@ -52,16 +52,16 @@ export class AdbSubprocessNoneProtocol implements AdbSubprocessProtocol {
/** /**
* `stderr` will always be empty. * `stderr` will always be empty.
*/ */
public get stderr() { get stderr() {
return this.#stderr; return this.#stderr;
} }
#exit: Promise<number>; #exit: Promise<number>;
public get exit() { get exit() {
return this.#exit; return this.#exit;
} }
public constructor(socket: AdbSocket) { constructor(socket: AdbSocket) {
this.#socket = socket; this.#socket = socket;
// Link `stdout`, `stderr` and `stdin` together, // Link `stdout`, `stderr` and `stdin` together,
@ -77,11 +77,11 @@ export class AdbSubprocessNoneProtocol implements AdbSubprocessProtocol {
this.#exit = this.#duplex.closed.then(() => 0); this.#exit = this.#duplex.closed.then(() => 0);
} }
public resize() { resize() {
// Not supported, but don't throw. // Not supported, but don't throw.
} }
public kill() { kill() {
return this.#duplex.close(); return this.#duplex.close();
} }
} }

View file

@ -61,38 +61,38 @@ class StdinSerializeStream extends ConsumableTransformStream<
} }
class MultiplexStream<T> { class MultiplexStream<T> {
private _readable: PushReadableStream<T>; #readable: PushReadableStream<T>;
private _readableController!: PushReadableStreamController<T>; #readableController!: PushReadableStreamController<T>;
public get readable() { get readable() {
return this._readable; return this.#readable;
} }
private _activeCount = 0; #activeCount = 0;
constructor() { constructor() {
this._readable = new PushReadableStream((controller) => { this.#readable = new PushReadableStream((controller) => {
this._readableController = controller; this.#readableController = controller;
}); });
} }
public createWriteable() { createWriteable() {
return new WritableStream<T>({ return new WritableStream<T>({
start: () => { start: () => {
this._activeCount += 1; this.#activeCount += 1;
}, },
write: async (chunk) => { write: async (chunk) => {
await this._readableController.enqueue(chunk); await this.#readableController.enqueue(chunk);
}, },
abort: () => { abort: () => {
this._activeCount -= 1; this.#activeCount -= 1;
if (this._activeCount === 0) { if (this.#activeCount === 0) {
this._readableController.close(); this.#readableController.close();
} }
}, },
close: () => { close: () => {
this._activeCount -= 1; this.#activeCount -= 1;
if (this._activeCount === 0) { if (this.#activeCount === 0) {
this._readableController.close(); this.#readableController.close();
} }
}, },
}); });
@ -108,18 +108,18 @@ class MultiplexStream<T> {
* * `resize`: Yes * * `resize`: Yes
*/ */
export class AdbSubprocessShellProtocol implements AdbSubprocessProtocol { export class AdbSubprocessShellProtocol implements AdbSubprocessProtocol {
public static isSupported(adb: Adb) { static isSupported(adb: Adb) {
return adb.supportsFeature(AdbFeature.ShellV2); return adb.supportsFeature(AdbFeature.ShellV2);
} }
public static async pty(adb: Adb, command: string) { static async pty(adb: Adb, command: string) {
// TODO: AdbShellSubprocessProtocol: Support setting `XTERM` environment variable // TODO: AdbShellSubprocessProtocol: Support setting `XTERM` environment variable
return new AdbSubprocessShellProtocol( return new AdbSubprocessShellProtocol(
await adb.createSocket(`shell,v2,pty:${command}`), await adb.createSocket(`shell,v2,pty:${command}`),
); );
} }
public static async raw(adb: Adb, command: string) { static async raw(adb: Adb, command: string) {
return new AdbSubprocessShellProtocol( return new AdbSubprocessShellProtocol(
await adb.createSocket(`shell,v2,raw:${command}`), await adb.createSocket(`shell,v2,raw:${command}`),
); );
@ -131,26 +131,26 @@ export class AdbSubprocessShellProtocol implements AdbSubprocessProtocol {
>; >;
#stdin: WritableStream<Consumable<Uint8Array>>; #stdin: WritableStream<Consumable<Uint8Array>>;
public get stdin() { get stdin() {
return this.#stdin; return this.#stdin;
} }
#stdout: ReadableStream<Uint8Array>; #stdout: ReadableStream<Uint8Array>;
public get stdout() { get stdout() {
return this.#stdout; return this.#stdout;
} }
#stderr: ReadableStream<Uint8Array>; #stderr: ReadableStream<Uint8Array>;
public get stderr() { get stderr() {
return this.#stderr; return this.#stderr;
} }
readonly #exit = new PromiseResolver<number>(); readonly #exit = new PromiseResolver<number>();
public get exit() { get exit() {
return this.#exit.promise; return this.#exit.promise;
} }
public constructor(socket: AdbSocket) { constructor(socket: AdbSocket) {
this.#socket = socket; this.#socket = socket;
// Check this image to help you understand the stream graph // Check this image to help you understand the stream graph
@ -225,7 +225,7 @@ export class AdbSubprocessShellProtocol implements AdbSubprocessProtocol {
this.#socketWriter = multiplexer.createWriteable().getWriter(); this.#socketWriter = multiplexer.createWriteable().getWriter();
} }
public async resize(rows: number, cols: number) { async resize(rows: number, cols: number) {
await ConsumableWritableStream.write(this.#socketWriter, { await ConsumableWritableStream.write(this.#socketWriter, {
id: AdbShellProtocolId.WindowSizeChange, id: AdbShellProtocolId.WindowSizeChange,
data: encodeUtf8( data: encodeUtf8(
@ -237,7 +237,7 @@ export class AdbSubprocessShellProtocol implements AdbSubprocessProtocol {
}); });
} }
public kill() { kill() {
return this.#socket.close(); return this.#socket.close();
} }
} }

View file

@ -19,11 +19,11 @@ export class AdbSyncSocketLocked implements AsyncExactReadable {
readonly #writeLock = new AutoResetEvent(); readonly #writeLock = new AutoResetEvent();
readonly #combiner: BufferCombiner; readonly #combiner: BufferCombiner;
public get position() { get position() {
return this.#readable.position; return this.#readable.position;
} }
public constructor( constructor(
writer: WritableStreamDefaultWriter<Consumable<Uint8Array>>, writer: WritableStreamDefaultWriter<Consumable<Uint8Array>>,
readable: BufferedReadableStream, readable: BufferedReadableStream,
bufferSize: number, bufferSize: number,
@ -35,39 +35,39 @@ export class AdbSyncSocketLocked implements AsyncExactReadable {
this.#combiner = new BufferCombiner(bufferSize); this.#combiner = new BufferCombiner(bufferSize);
} }
private async writeInnerStream(buffer: Uint8Array) { async #writeInnerStream(buffer: Uint8Array) {
await ConsumableWritableStream.write(this.#writer, buffer); await ConsumableWritableStream.write(this.#writer, buffer);
} }
public async flush() { async flush() {
try { try {
await this.#writeLock.wait(); await this.#writeLock.wait();
const buffer = this.#combiner.flush(); const buffer = this.#combiner.flush();
if (buffer) { if (buffer) {
await this.writeInnerStream(buffer); await this.#writeInnerStream(buffer);
} }
} finally { } finally {
this.#writeLock.notifyOne(); this.#writeLock.notifyOne();
} }
} }
public async write(data: Uint8Array) { async write(data: Uint8Array) {
try { try {
await this.#writeLock.wait(); await this.#writeLock.wait();
for (const buffer of this.#combiner.push(data)) { for (const buffer of this.#combiner.push(data)) {
await this.writeInnerStream(buffer); await this.#writeInnerStream(buffer);
} }
} finally { } finally {
this.#writeLock.notifyOne(); this.#writeLock.notifyOne();
} }
} }
public async readExactly(length: number) { async readExactly(length: number) {
await this.flush(); await this.flush();
return await this.#readable.readExactly(length); return await this.#readable.readExactly(length);
} }
public release(): void { release(): void {
this.#combiner.flush(); this.#combiner.flush();
this.#socketLock.notifyOne(); this.#socketLock.notifyOne();
} }
@ -78,7 +78,7 @@ export class AdbSyncSocket {
readonly #socket: AdbSocket; readonly #socket: AdbSocket;
readonly #locked: AdbSyncSocketLocked; readonly #locked: AdbSyncSocketLocked;
public constructor(socket: AdbSocket, bufferSize: number) { constructor(socket: AdbSocket, bufferSize: number) {
this.#socket = socket; this.#socket = socket;
this.#locked = new AdbSyncSocketLocked( this.#locked = new AdbSyncSocketLocked(
socket.writable.getWriter(), socket.writable.getWriter(),
@ -88,12 +88,12 @@ export class AdbSyncSocket {
); );
} }
public async lock() { async lock() {
await this.#lock.wait(); await this.#lock.wait();
return this.#locked; return this.#locked;
} }
public async close() { async close() {
await this.#socket.close(); await this.#socket.close();
} }
} }

View file

@ -48,27 +48,27 @@ export class AdbSync extends AutoDisposable {
readonly #supportsSendReceiveV2: boolean; readonly #supportsSendReceiveV2: boolean;
readonly #needPushMkdirWorkaround: boolean; readonly #needPushMkdirWorkaround: boolean;
public get supportsStat(): boolean { get supportsStat(): boolean {
return this.#supportsStat; return this.#supportsStat;
} }
public get supportsListV2(): boolean { get supportsListV2(): boolean {
return this.#supportsListV2; return this.#supportsListV2;
} }
public get fixedPushMkdir(): boolean { get fixedPushMkdir(): boolean {
return this.#fixedPushMkdir; return this.#fixedPushMkdir;
} }
public get supportsSendReceiveV2(): boolean { get supportsSendReceiveV2(): boolean {
return this.#supportsSendReceiveV2; return this.#supportsSendReceiveV2;
} }
public get needPushMkdirWorkaround(): boolean { get needPushMkdirWorkaround(): boolean {
return this.#needPushMkdirWorkaround; return this.#needPushMkdirWorkaround;
} }
public constructor(adb: Adb, socket: AdbSocket) { constructor(adb: Adb, socket: AdbSocket) {
super(); super();
this._adb = adb; this._adb = adb;
@ -86,11 +86,11 @@ export class AdbSync extends AutoDisposable {
!this.fixedPushMkdir; !this.fixedPushMkdir;
} }
public async lstat(path: string): Promise<AdbSyncStat> { async lstat(path: string): Promise<AdbSyncStat> {
return await adbSyncLstat(this._socket, path, this.supportsStat); return await adbSyncLstat(this._socket, path, this.supportsStat);
} }
public async stat(path: string) { async stat(path: string) {
if (!this.supportsStat) { if (!this.supportsStat) {
throw new Error("Not supported"); throw new Error("Not supported");
} }
@ -98,7 +98,7 @@ export class AdbSync extends AutoDisposable {
return await adbSyncStat(this._socket, path); return await adbSyncStat(this._socket, path);
} }
public async isDirectory(path: string): Promise<boolean> { async isDirectory(path: string): Promise<boolean> {
try { try {
await this.lstat(path + "/"); await this.lstat(path + "/");
return true; return true;
@ -107,11 +107,11 @@ export class AdbSync extends AutoDisposable {
} }
} }
public opendir(path: string): AsyncGenerator<AdbSyncEntry, void, void> { opendir(path: string): AsyncGenerator<AdbSyncEntry, void, void> {
return adbSyncOpenDir(this._socket, path, this.supportsListV2); return adbSyncOpenDir(this._socket, path, this.supportsListV2);
} }
public async readdir(path: string) { async readdir(path: string) {
const results: AdbSyncEntry[] = []; const results: AdbSyncEntry[] = [];
for await (const entry of this.opendir(path)) { for await (const entry of this.opendir(path)) {
results.push(entry); results.push(entry);
@ -125,7 +125,7 @@ export class AdbSync extends AutoDisposable {
* @param filename The full path of the file on device to read. * @param filename The full path of the file on device to read.
* @returns A `ReadableStream` that contains the file content. * @returns A `ReadableStream` that contains the file content.
*/ */
public read(filename: string): ReadableStream<Uint8Array> { read(filename: string): ReadableStream<Uint8Array> {
return adbSyncPull(this._socket, filename); return adbSyncPull(this._socket, filename);
} }
@ -134,7 +134,7 @@ export class AdbSync extends AutoDisposable {
* *
* @param options The content and options of the file to write. * @param options The content and options of the file to write.
*/ */
public async write(options: AdbSyncWriteOptions): Promise<void> { async write(options: AdbSyncWriteOptions): Promise<void> {
if (this.needPushMkdirWorkaround) { if (this.needPushMkdirWorkaround) {
// It may fail if the path is already existed. // It may fail if the path is already existed.
// Ignore the result. // Ignore the result.
@ -153,7 +153,7 @@ export class AdbSync extends AutoDisposable {
}); });
} }
public override async dispose() { override async dispose() {
super.dispose(); super.dispose();
await this._socket.close(); await this._socket.close();
} }

View file

@ -1,7 +1,7 @@
import { AdbCommandBase } from "./base.js"; import { AdbCommandBase } from "./base.js";
export class AdbTcpIpCommand extends AdbCommandBase { export class AdbTcpIpCommand extends AdbCommandBase {
public async setPort(port: number): Promise<string> { async setPort(port: number): Promise<string> {
if (port <= 0) { if (port <= 0) {
throw new Error(`Invalid port ${port}`); throw new Error(`Invalid port ${port}`);
} }
@ -13,7 +13,7 @@ export class AdbTcpIpCommand extends AdbCommandBase {
return output; return output;
} }
public async disable(): Promise<string> { async disable(): Promise<string> {
const output = await this.adb.createSocketAndWait("usb:"); const output = await this.adb.createSocketAndWait("usb:");
if (output !== "restarting in USB mode\n") { if (output !== "restarting in USB mode\n") {
throw new Error(output); throw new Error(output);

View file

@ -120,34 +120,30 @@ export const ADB_DEFAULT_AUTHENTICATORS: AdbAuthenticator[] = [
]; ];
export class AdbAuthenticationProcessor implements Disposable { export class AdbAuthenticationProcessor implements Disposable {
public readonly authenticators: readonly AdbAuthenticator[]; readonly authenticators: readonly AdbAuthenticator[];
private readonly credentialStore: AdbCredentialStore; readonly #credentialStore: AdbCredentialStore;
#pendingRequest = new PromiseResolver<AdbPacketData>(); #pendingRequest = new PromiseResolver<AdbPacketData>();
#iterator: AsyncIterator<AdbPacketData, void, void> | undefined; #iterator: AsyncIterator<AdbPacketData, void, void> | undefined;
public constructor( constructor(
authenticators: readonly AdbAuthenticator[], authenticators: readonly AdbAuthenticator[],
credentialStore: AdbCredentialStore, credentialStore: AdbCredentialStore,
) { ) {
this.authenticators = authenticators; this.authenticators = authenticators;
this.credentialStore = credentialStore; this.#credentialStore = credentialStore;
} }
private getNextRequest = (): Promise<AdbPacketData> => { #getNextRequest = (): Promise<AdbPacketData> => {
return this.#pendingRequest.promise; return this.#pendingRequest.promise;
}; };
private async *invokeAuthenticator(): AsyncGenerator< async *#invokeAuthenticator(): AsyncGenerator<AdbPacketData, void, void> {
AdbPacketData,
void,
void
> {
for (const authenticator of this.authenticators) { for (const authenticator of this.authenticators) {
for await (const packet of authenticator( for await (const packet of authenticator(
this.credentialStore, this.#credentialStore,
this.getNextRequest, this.#getNextRequest,
)) { )) {
// If the authenticator yielded a response // If the authenticator yielded a response
// Prepare `nextRequest` for next authentication request // Prepare `nextRequest` for next authentication request
@ -162,9 +158,9 @@ export class AdbAuthenticationProcessor implements Disposable {
} }
} }
public async process(packet: AdbPacketData): Promise<AdbPacketData> { async process(packet: AdbPacketData): Promise<AdbPacketData> {
if (!this.#iterator) { if (!this.#iterator) {
this.#iterator = this.invokeAuthenticator(); this.#iterator = this.#invokeAuthenticator();
} }
this.#pendingRequest.resolve(packet); this.#pendingRequest.resolve(packet);
@ -177,7 +173,7 @@ export class AdbAuthenticationProcessor implements Disposable {
return result.value; return result.value;
} }
public dispose() { dispose() {
void this.#iterator?.return?.(); void this.#iterator?.return?.();
} }
} }

View file

@ -51,18 +51,18 @@ export class AdbPacketDispatcher implements Closeable {
#writer: WritableStreamDefaultWriter<Consumable<AdbPacketInit>>; #writer: WritableStreamDefaultWriter<Consumable<AdbPacketInit>>;
public readonly options: AdbPacketDispatcherOptions; readonly options: AdbPacketDispatcherOptions;
#closed = false; #closed = false;
#disconnected = new PromiseResolver<void>(); #disconnected = new PromiseResolver<void>();
public get disconnected() { get disconnected() {
return this.#disconnected.promise; return this.#disconnected.promise;
} }
#incomingSocketHandlers = new Map<string, AdbIncomingSocketHandler>(); #incomingSocketHandlers = new Map<string, AdbIncomingSocketHandler>();
#readAbortController = new AbortController(); #readAbortController = new AbortController();
public constructor( constructor(
connection: ReadableWritablePair< connection: ReadableWritablePair<
AdbPacketData, AdbPacketData,
Consumable<AdbPacketInit> Consumable<AdbPacketInit>
@ -77,10 +77,10 @@ export class AdbPacketDispatcher implements Closeable {
write: async (packet) => { write: async (packet) => {
switch (packet.command) { switch (packet.command) {
case AdbCommand.OK: case AdbCommand.OK:
this.handleOk(packet); this.#handleOk(packet);
break; break;
case AdbCommand.Close: case AdbCommand.Close:
await this.handleClose(packet); await this.#handleClose(packet);
break; break;
case AdbCommand.Write: case AdbCommand.Write:
if (this.#sockets.has(packet.arg1)) { if (this.#sockets.has(packet.arg1)) {
@ -98,7 +98,7 @@ export class AdbPacketDispatcher implements Closeable {
`Unknown local socket id: ${packet.arg1}`, `Unknown local socket id: ${packet.arg1}`,
); );
case AdbCommand.Open: case AdbCommand.Open:
await this.handleOpen(packet); await this.#handleOpen(packet);
break; break;
default: default:
// Junk data may only appear in the authentication phase, // Junk data may only appear in the authentication phase,
@ -125,20 +125,20 @@ export class AdbPacketDispatcher implements Closeable {
) )
.then( .then(
() => { () => {
this.dispose(); this.#dispose();
}, },
(e) => { (e) => {
if (!this.#closed) { if (!this.#closed) {
this.#disconnected.reject(e); this.#disconnected.reject(e);
} }
this.dispose(); this.#dispose();
}, },
); );
this.#writer = connection.writable.getWriter(); this.#writer = connection.writable.getWriter();
} }
private handleOk(packet: AdbPacketData) { #handleOk(packet: AdbPacketData) {
if (this.#initializers.resolve(packet.arg1, packet.arg0)) { if (this.#initializers.resolve(packet.arg1, packet.arg0)) {
// Device successfully created the socket // Device successfully created the socket
return; return;
@ -156,7 +156,7 @@ export class AdbPacketDispatcher implements Closeable {
void this.sendPacket(AdbCommand.Close, packet.arg1, packet.arg0); void this.sendPacket(AdbCommand.Close, packet.arg1, packet.arg0);
} }
private async handleClose(packet: AdbPacketData) { async #handleClose(packet: AdbPacketData) {
// If the socket is still pending // If the socket is still pending
if ( if (
packet.arg0 === 0 && packet.arg0 === 0 &&
@ -201,22 +201,19 @@ export class AdbPacketDispatcher implements Closeable {
// the device may also respond with two `CLSE` packets. // the device may also respond with two `CLSE` packets.
} }
public addReverseTunnel( addReverseTunnel(service: string, handler: AdbIncomingSocketHandler) {
service: string,
handler: AdbIncomingSocketHandler,
) {
this.#incomingSocketHandlers.set(service, handler); this.#incomingSocketHandlers.set(service, handler);
} }
public removeReverseTunnel(address: string) { removeReverseTunnel(address: string) {
this.#incomingSocketHandlers.delete(address); this.#incomingSocketHandlers.delete(address);
} }
public clearReverseTunnels() { clearReverseTunnels() {
this.#incomingSocketHandlers.clear(); this.#incomingSocketHandlers.clear();
} }
private async handleOpen(packet: AdbPacketData) { async #handleOpen(packet: AdbPacketData) {
// `AsyncOperationManager` doesn't support skipping IDs // `AsyncOperationManager` doesn't support skipping IDs
// Use `add` + `resolve` to simulate this behavior // Use `add` + `resolve` to simulate this behavior
const [localId] = this.#initializers.add<number>(); const [localId] = this.#initializers.add<number>();
@ -251,7 +248,7 @@ export class AdbPacketDispatcher implements Closeable {
} }
} }
public async createSocket(service: string): Promise<AdbSocket> { async createSocket(service: string): Promise<AdbSocket> {
if (this.options.appendNullToServiceString) { if (this.options.appendNullToServiceString) {
service += "\0"; service += "\0";
} }
@ -273,7 +270,7 @@ export class AdbPacketDispatcher implements Closeable {
return controller.socket; return controller.socket;
} }
public async sendPacket( async sendPacket(
command: AdbCommand, command: AdbCommand,
arg0: number, arg0: number,
arg1: number, arg1: number,
@ -299,7 +296,7 @@ export class AdbPacketDispatcher implements Closeable {
}); });
} }
public async close() { async close() {
// Send `CLSE` packets for all sockets // Send `CLSE` packets for all sockets
await Promise.all( await Promise.all(
Array.from(this.#sockets.values(), (socket) => socket.close()), Array.from(this.#sockets.values(), (socket) => socket.close()),
@ -315,7 +312,7 @@ export class AdbPacketDispatcher implements Closeable {
// `pipe().then()` will call `dispose` // `pipe().then()` will call `dispose`
} }
private dispose() { #dispose() {
for (const socket of this.#sockets.values()) { for (const socket of this.#sockets.values()) {
socket.dispose().catch(unreachable); socket.dispose().catch(unreachable);
} }

View file

@ -53,7 +53,7 @@ export class AdbPacketSerializeStream extends ConsumableTransformStream<
AdbPacketInit, AdbPacketInit,
Uint8Array Uint8Array
> { > {
public constructor() { constructor() {
const headerBuffer = new Uint8Array(AdbPacketHeader.size); const headerBuffer = new Uint8Array(AdbPacketHeader.size);
super({ super({
transform: async (chunk, controller) => { transform: async (chunk, controller) => {

View file

@ -42,23 +42,23 @@ export class AdbDaemonSocketController
Closeable, Closeable,
Disposable Disposable
{ {
private readonly dispatcher!: AdbPacketDispatcher; readonly #dispatcher!: AdbPacketDispatcher;
public readonly localId!: number; readonly localId!: number;
public readonly remoteId!: number; readonly remoteId!: number;
public readonly localCreated!: boolean; readonly localCreated!: boolean;
public readonly service!: string; readonly service!: string;
#duplex: DuplexStreamFactory<Uint8Array, Consumable<Uint8Array>>; #duplex: DuplexStreamFactory<Uint8Array, Consumable<Uint8Array>>;
#readable: ReadableStream<Uint8Array>; #readable: ReadableStream<Uint8Array>;
#readableController!: PushReadableStreamController<Uint8Array>; #readableController!: PushReadableStreamController<Uint8Array>;
public get readable() { get readable() {
return this.#readable; return this.#readable;
} }
#writePromise: PromiseResolver<void> | undefined; #writePromise: PromiseResolver<void> | undefined;
public readonly writable: WritableStream<Consumable<Uint8Array>>; readonly writable: WritableStream<Consumable<Uint8Array>>;
#closed = false; #closed = false;
/** /**
@ -66,17 +66,21 @@ export class AdbDaemonSocketController
* *
* It's only used by dispatcher to avoid sending another `CLSE` packet to remote. * It's only used by dispatcher to avoid sending another `CLSE` packet to remote.
*/ */
public get closed() { get closed() {
return this.#closed; return this.#closed;
} }
private _socket: AdbDaemonSocket; #socket: AdbDaemonSocket;
public get socket() { get socket() {
return this._socket; return this.#socket;
} }
public constructor(options: AdbDaemonSocketConstructionOptions) { constructor(options: AdbDaemonSocketConstructionOptions) {
Object.assign(this, options); this.#dispatcher = options.dispatcher;
this.localId = options.localId;
this.remoteId = options.remoteId;
this.localCreated = options.localCreated;
this.service = options.service;
// Check this image to help you understand the stream graph // Check this image to help you understand the stream graph
// cspell: disable-next-line // cspell: disable-next-line
@ -89,10 +93,10 @@ export class AdbDaemonSocketController
close: async () => { close: async () => {
this.#closed = true; this.#closed = true;
await this.dispatcher.sendPacket( await this.#dispatcher.sendPacket(
AdbCommand.Close, AdbCommand.Close,
this.localId, this.localId,
this.remoteId, this.remoteId
); );
// Don't `dispose` here, we need to wait for `CLSE` response packet. // Don't `dispose` here, we need to wait for `CLSE` response packet.
@ -114,8 +118,8 @@ export class AdbDaemonSocketController
size(chunk) { size(chunk) {
return chunk.byteLength; return chunk.byteLength;
}, },
}, }
), )
); );
this.writable = pipeFrom( this.writable = pipeFrom(
@ -124,23 +128,23 @@ export class AdbDaemonSocketController
write: async (chunk) => { write: async (chunk) => {
// Wait for an ack packet // Wait for an ack packet
this.#writePromise = new PromiseResolver(); this.#writePromise = new PromiseResolver();
await this.dispatcher.sendPacket( await this.#dispatcher.sendPacket(
AdbCommand.Write, AdbCommand.Write,
this.localId, this.localId,
this.remoteId, this.remoteId,
chunk, chunk
); );
await this.#writePromise.promise; await this.#writePromise.promise;
}, },
}), })
), ),
new DistributionStream(this.dispatcher.options.maxPayloadSize), new DistributionStream(this.#dispatcher.options.maxPayloadSize)
); );
this._socket = new AdbDaemonSocket(this); this.#socket = new AdbDaemonSocket(this);
} }
public async enqueue(data: Uint8Array) { async enqueue(data: Uint8Array) {
// Consumer may abort the `ReadableStream` to close the socket, // Consumer may abort the `ReadableStream` to close the socket,
// it's OK to throw away further packets in this case. // it's OK to throw away further packets in this case.
if (this.#readableController.abortSignal.aborted) { if (this.#readableController.abortSignal.aborted) {
@ -150,15 +154,15 @@ export class AdbDaemonSocketController
await this.#readableController.enqueue(data); await this.#readableController.enqueue(data);
} }
public ack() { ack() {
this.#writePromise?.resolve(); this.#writePromise?.resolve();
} }
public async close(): Promise<void> { async close(): Promise<void> {
await this.#duplex.close(); await this.#duplex.close();
} }
public dispose() { dispose() {
return this.#duplex.dispose(); return this.#duplex.dispose();
} }
} }
@ -178,35 +182,35 @@ export class AdbDaemonSocket
{ {
#controller: AdbDaemonSocketController; #controller: AdbDaemonSocketController;
public get localId(): number { get localId(): number {
return this.#controller.localId; return this.#controller.localId;
} }
public get remoteId(): number { get remoteId(): number {
return this.#controller.remoteId; return this.#controller.remoteId;
} }
public get localCreated(): boolean { get localCreated(): boolean {
return this.#controller.localCreated; return this.#controller.localCreated;
} }
public get service(): string { get service(): string {
return this.#controller.service; return this.#controller.service;
} }
public get readable(): ReadableStream<Uint8Array> { get readable(): ReadableStream<Uint8Array> {
return this.#controller.readable; return this.#controller.readable;
} }
public get writable(): WritableStream<Consumable<Uint8Array>> { get writable(): WritableStream<Consumable<Uint8Array>> {
return this.#controller.writable; return this.#controller.writable;
} }
public get closed(): boolean { get closed(): boolean {
return this.#controller.closed; return this.#controller.closed;
} }
public constructor(controller: AdbDaemonSocketController) { constructor(controller: AdbDaemonSocketController) {
this.#controller = controller; this.#controller = controller;
} }
public close() { close() {
return this.#controller.close(); return this.#controller.close();
} }
} }

View file

@ -51,7 +51,7 @@ export class AdbDaemonTransport implements AdbTransport {
* on the same connection. Because every time the device receives a `CNXN` packet, * on the same connection. Because every time the device receives a `CNXN` packet,
* it resets all internal state, and starts a new authentication process. * it resets all internal state, and starts a new authentication process.
*/ */
public static async authenticate({ static async authenticate({
serial, serial,
connection, connection,
credentialStore, credentialStore,
@ -186,30 +186,30 @@ export class AdbDaemonTransport implements AdbTransport {
readonly #dispatcher: AdbPacketDispatcher; readonly #dispatcher: AdbPacketDispatcher;
#serial: string; #serial: string;
public get serial() { get serial() {
return this.#serial; return this.#serial;
} }
#protocolVersion: number; #protocolVersion: number;
public get protocolVersion() { get protocolVersion() {
return this.#protocolVersion; return this.#protocolVersion;
} }
#maxPayloadSize: number; #maxPayloadSize: number;
public get maxPayloadSize() { get maxPayloadSize() {
return this.#maxPayloadSize; return this.#maxPayloadSize;
} }
#banner: AdbBanner; #banner: AdbBanner;
public get banner() { get banner() {
return this.#banner; return this.#banner;
} }
public get disconnected() { get disconnected() {
return this.#dispatcher.disconnected; return this.#dispatcher.disconnected;
} }
public constructor({ constructor({
serial, serial,
connection, connection,
version, version,
@ -239,11 +239,11 @@ export class AdbDaemonTransport implements AdbTransport {
this.#maxPayloadSize = maxPayloadSize; this.#maxPayloadSize = maxPayloadSize;
} }
public connect(service: string): ValueOrPromise<AdbSocket> { connect(service: string): ValueOrPromise<AdbSocket> {
return this.#dispatcher.createSocket(service); return this.#dispatcher.createSocket(service);
} }
public addReverseTunnel( addReverseTunnel(
handler: AdbIncomingSocketHandler, handler: AdbIncomingSocketHandler,
address?: string, address?: string,
): string { ): string {
@ -255,15 +255,15 @@ export class AdbDaemonTransport implements AdbTransport {
return address; return address;
} }
public removeReverseTunnel(address: string): void { removeReverseTunnel(address: string): void {
this.#dispatcher.removeReverseTunnel(address); this.#dispatcher.removeReverseTunnel(address);
} }
public clearReverseTunnels(): void { clearReverseTunnels(): void {
this.#dispatcher.clearReverseTunnels(); this.#dispatcher.clearReverseTunnels();
} }
public close(): ValueOrPromise<void> { close(): ValueOrPromise<void> {
return this.#dispatcher.close(); return this.#dispatcher.close();
} }
} }

View file

@ -74,17 +74,17 @@ export interface AdbServerDevice {
} }
export class AdbServerClient { export class AdbServerClient {
public static readonly VERSION = 41; static readonly VERSION = 41;
public readonly connection: AdbServerConnection; readonly connection: AdbServerConnection;
public constructor(connection: AdbServerConnection) { constructor(connection: AdbServerConnection) {
this.connection = connection; this.connection = connection;
} }
public static readString(stream: ExactReadable): string; static readString(stream: ExactReadable): string;
public static readString(stream: AsyncExactReadable): PromiseLike<string>; static readString(stream: AsyncExactReadable): PromiseLike<string>;
public static readString( static readString(
stream: ExactReadable | AsyncExactReadable, stream: ExactReadable | AsyncExactReadable,
): string | PromiseLike<string> { ): string | PromiseLike<string> {
return SyncPromise.try(() => stream.readExactly(4)) return SyncPromise.try(() => stream.readExactly(4))
@ -98,7 +98,7 @@ export class AdbServerClient {
.valueOrPromise(); .valueOrPromise();
} }
public static async writeString( static async writeString(
writer: WritableStreamDefaultWriter<Uint8Array>, writer: WritableStreamDefaultWriter<Uint8Array>,
value: string, value: string,
): Promise<void> { ): Promise<void> {
@ -109,7 +109,7 @@ export class AdbServerClient {
await writer.write(buffer); await writer.write(buffer);
} }
public static async readOkay( static async readOkay(
stream: ExactReadable | AsyncExactReadable, stream: ExactReadable | AsyncExactReadable,
): Promise<void> { ): Promise<void> {
const response = decodeUtf8(await stream.readExactly(4)); const response = decodeUtf8(await stream.readExactly(4));
@ -125,7 +125,7 @@ export class AdbServerClient {
throw new Error(`Unexpected response: ${response}`); throw new Error(`Unexpected response: ${response}`);
} }
public async connect( async connect(
request: string, request: string,
options?: AdbServerConnectionOptions, options?: AdbServerConnectionOptions,
): Promise<ReadableWritablePair<Uint8Array, Uint8Array>> { ): Promise<ReadableWritablePair<Uint8Array, Uint8Array>> {
@ -156,7 +156,7 @@ export class AdbServerClient {
} }
} }
public async getVersion(): Promise<number> { async getVersion(): Promise<number> {
const connection = await this.connect("host:version"); const connection = await this.connect("host:version");
const readable = new BufferedReadableStream(connection.readable); const readable = new BufferedReadableStream(connection.readable);
try { try {
@ -169,7 +169,7 @@ export class AdbServerClient {
} }
} }
public async validateVersion() { async validateVersion() {
const version = await this.getVersion(); const version = await this.getVersion();
if (version !== AdbServerClient.VERSION) { if (version !== AdbServerClient.VERSION) {
throw new Error( throw new Error(
@ -178,13 +178,13 @@ export class AdbServerClient {
} }
} }
public async killServer(): Promise<void> { async killServer(): Promise<void> {
const connection = await this.connect("host:kill"); const connection = await this.connect("host:kill");
connection.writable.close().catch(NOOP); connection.writable.close().catch(NOOP);
connection.readable.cancel().catch(NOOP); connection.readable.cancel().catch(NOOP);
} }
public async getServerFeatures(): Promise<AdbFeature[]> { async getServerFeatures(): Promise<AdbFeature[]> {
const connection = await this.connect("host:host-features"); const connection = await this.connect("host:host-features");
const readable = new BufferedReadableStream(connection.readable); const readable = new BufferedReadableStream(connection.readable);
try { try {
@ -196,7 +196,7 @@ export class AdbServerClient {
} }
} }
public async getDevices(): Promise<AdbServerDevice[]> { async getDevices(): Promise<AdbServerDevice[]> {
const connection = await this.connect("host:devices-l"); const connection = await this.connect("host:devices-l");
const readable = new BufferedReadableStream(connection.readable); const readable = new BufferedReadableStream(connection.readable);
try { try {
@ -253,10 +253,7 @@ export class AdbServerClient {
} }
} }
public formatDeviceService( formatDeviceService(device: AdbServerDeviceSelector, command: string) {
device: AdbServerDeviceSelector,
command: string,
) {
if (!device) { if (!device) {
return `host:${command}`; return `host:${command}`;
} }
@ -282,7 +279,7 @@ export class AdbServerClient {
* @param device The device selector * @param device The device selector
* @returns The transport ID of the selected device, and the features supported by the device. * @returns The transport ID of the selected device, and the features supported by the device.
*/ */
public async getDeviceFeatures( async getDeviceFeatures(
device: AdbServerDeviceSelector, device: AdbServerDeviceSelector,
): Promise<{ transportId: bigint; features: AdbFeature[] }> { ): Promise<{ transportId: bigint; features: AdbFeature[] }> {
// Usually the client sends a device command using `connectDevice`, // Usually the client sends a device command using `connectDevice`,
@ -309,7 +306,7 @@ export class AdbServerClient {
* @param service The service to forward * @param service The service to forward
* @returns An `AdbServerSocket` that can be used to communicate with the service * @returns An `AdbServerSocket` that can be used to communicate with the service
*/ */
public async connectDevice( async connectDevice(
device: AdbServerDeviceSelector, device: AdbServerDeviceSelector,
service: string, service: string,
): Promise<AdbServerSocket> { ): Promise<AdbServerSocket> {
@ -386,7 +383,7 @@ export class AdbServerClient {
* @param options The options * @param options The options
* @returns A promise that resolves when the condition is met. * @returns A promise that resolves when the condition is met.
*/ */
public async waitFor( async waitFor(
device: AdbServerDeviceSelector, device: AdbServerDeviceSelector,
state: "device" | "disconnect", state: "device" | "disconnect",
options?: AdbServerConnectionOptions, options?: AdbServerConnectionOptions,
@ -418,7 +415,7 @@ export class AdbServerClient {
await this.connect(service, options); await this.connect(service, options);
} }
public async createTransport( async createTransport(
device: AdbServerDeviceSelector, device: AdbServerDeviceSelector,
): Promise<AdbServerTransport> { ): Promise<AdbServerTransport> {
const { transportId, features } = await this.getDeviceFeatures(device); const { transportId, features } = await this.getDeviceFeatures(device);

View file

@ -14,19 +14,19 @@ import type { AdbServerClient } from "./client.js";
export class AdbServerTransport implements AdbTransport { export class AdbServerTransport implements AdbTransport {
#client: AdbServerClient; #client: AdbServerClient;
public readonly serial: string; readonly serial: string;
public readonly transportId: bigint; readonly transportId: bigint;
public readonly maxPayloadSize: number = 1 * 1024 * 1024; readonly maxPayloadSize: number = 1 * 1024 * 1024;
public readonly banner: AdbBanner; readonly banner: AdbBanner;
#closed = new PromiseResolver<void>(); #closed = new PromiseResolver<void>();
#waitAbortController = new AbortController(); #waitAbortController = new AbortController();
public readonly disconnected: Promise<void>; readonly disconnected: Promise<void>;
public constructor( constructor(
client: AdbServerClient, client: AdbServerClient,
serial: string, serial: string,
banner: AdbBanner, banner: AdbBanner,
@ -46,7 +46,7 @@ export class AdbServerTransport implements AdbTransport {
]); ]);
} }
public async connect(service: string): Promise<AdbSocket> { async connect(service: string): Promise<AdbSocket> {
return await this.#client.connectDevice( return await this.#client.connectDevice(
{ {
transportId: this.transportId, transportId: this.transportId,
@ -55,18 +55,18 @@ export class AdbServerTransport implements AdbTransport {
); );
} }
public async addReverseTunnel( async addReverseTunnel(
handler: AdbIncomingSocketHandler, handler: AdbIncomingSocketHandler,
address?: string, address?: string,
): Promise<string> { ): Promise<string> {
return await this.#client.connection.addReverseTunnel(handler, address); return await this.#client.connection.addReverseTunnel(handler, address);
} }
public async removeReverseTunnel(address: string): Promise<void> { async removeReverseTunnel(address: string): Promise<void> {
await this.#client.connection.removeReverseTunnel(address); await this.#client.connection.removeReverseTunnel(address);
} }
public async clearReverseTunnels(): Promise<void> { async clearReverseTunnels(): Promise<void> {
await this.#client.connection.clearReverseTunnels(); await this.#client.connection.clearReverseTunnels();
} }

View file

@ -5,11 +5,11 @@ export class AutoResetEvent implements Disposable {
#set: boolean; #set: boolean;
readonly #queue: PromiseResolver<void>[] = []; readonly #queue: PromiseResolver<void>[] = [];
public constructor(initialSet = false) { constructor(initialSet = false) {
this.#set = initialSet; this.#set = initialSet;
} }
public wait(): Promise<void> { wait(): Promise<void> {
if (!this.#set) { if (!this.#set) {
this.#set = true; this.#set = true;
@ -23,7 +23,7 @@ export class AutoResetEvent implements Disposable {
return resolver.promise; return resolver.promise;
} }
public notifyOne() { notifyOne() {
if (this.#queue.length !== 0) { if (this.#queue.length !== 0) {
this.#queue.pop()!.resolve(); this.#queue.pop()!.resolve();
} else { } else {
@ -31,7 +31,7 @@ export class AutoResetEvent implements Disposable {
} }
} }
public dispose() { dispose() {
for (const item of this.#queue) { for (const item of this.#queue) {
item.reject(new Error("The AutoResetEvent has been disposed")); item.reject(new Error("The AutoResetEvent has been disposed"));
} }

View file

@ -10,7 +10,7 @@ export class ConditionalVariable implements Disposable {
#locked = false; #locked = false;
readonly #queue: WaitEntry[] = []; readonly #queue: WaitEntry[] = [];
public wait(condition: () => boolean): Promise<void> { wait(condition: () => boolean): Promise<void> {
if (!this.#locked) { if (!this.#locked) {
this.#locked = true; this.#locked = true;
if (this.#queue.length === 0 && condition()) { if (this.#queue.length === 0 && condition()) {
@ -23,7 +23,7 @@ export class ConditionalVariable implements Disposable {
return resolver.promise; return resolver.promise;
} }
public notifyOne() { notifyOne() {
const entry = this.#queue.shift(); const entry = this.#queue.shift();
if (entry) { if (entry) {
if (entry.condition()) { if (entry.condition()) {
@ -34,7 +34,7 @@ export class ConditionalVariable implements Disposable {
} }
} }
public dispose(): void { dispose(): void {
for (const item of this.#queue) { for (const item of this.#queue) {
item.resolver.reject( item.resolver.reject(
new Error("The ConditionalVariable has been disposed"), new Error("The ConditionalVariable has been disposed"),

View file

@ -21,7 +21,7 @@ export class AdbBackup extends AdbCommandBase {
/** /**
* User must confirm backup on device within 60 seconds. * User must confirm backup on device within 60 seconds.
*/ */
public async backup( async backup(
options: AdbBackupOptions, options: AdbBackupOptions,
): Promise<ReadableStream<Uint8Array>> { ): Promise<ReadableStream<Uint8Array>> {
const args = ["bu", "backup"]; const args = ["bu", "backup"];
@ -62,7 +62,7 @@ export class AdbBackup extends AdbCommandBase {
* User must enter the password (if any) and * User must enter the password (if any) and
* confirm restore on device within 60 seconds. * confirm restore on device within 60 seconds.
*/ */
public async restore(options: AdbRestoreOptions): Promise<void> { async restore(options: AdbRestoreOptions): Promise<void> {
const args = ["bu", "restore"]; const args = ["bu", "restore"];
if (options.user !== undefined) { if (options.user !== undefined) {
args.push("--user", options.user.toString()); args.push("--user", options.user.toString());

View file

@ -20,22 +20,22 @@ export interface BugReportZVersion {
} }
export class BugReportZ extends AdbCommandBase { export class BugReportZ extends AdbCommandBase {
public static VERSION_REGEX = /(\d+)\.(\d+)/; static VERSION_REGEX = /(\d+)\.(\d+)/;
public static BEGIN_REGEX = /BEGIN:(.*)/; static BEGIN_REGEX = /BEGIN:(.*)/;
public static PROGRESS_REGEX = /PROGRESS:(.*)\/(.*)/; static PROGRESS_REGEX = /PROGRESS:(.*)\/(.*)/;
public static OK_REGEX = /OK:(.*)/; static OK_REGEX = /OK:(.*)/;
public static FAIL_REGEX = /FAIL:(.*)/; static FAIL_REGEX = /FAIL:(.*)/;
/** /**
* Retrieve the version of bugreportz. * Retrieve the version of bugreportz.
* *
* @returns a `BugReportVersion` object, or `undefined` if `bugreportz` is not available. * @returns a `BugReportVersion` object, or `undefined` if `bugreportz` is not available.
*/ */
public async version(): Promise<BugReportZVersion | undefined> { async version(): Promise<BugReportZVersion | undefined> {
// bugreportz requires shell protocol // bugreportz requires shell protocol
if (!AdbSubprocessShellProtocol.isSupported(this.adb)) { if (!AdbSubprocessShellProtocol.isSupported(this.adb)) {
return undefined; return undefined;
@ -65,7 +65,7 @@ export class BugReportZ extends AdbCommandBase {
}; };
} }
public supportProgress(major: number, minor: number): boolean { supportProgress(major: number, minor: number): boolean {
return major > 1 || minor >= 1; return major > 1 || minor >= 1;
} }
@ -78,7 +78,7 @@ export class BugReportZ extends AdbCommandBase {
* @param onProgress Progress callback. Only specify this if `supportsProgress` is `true`. * @param onProgress Progress callback. Only specify this if `supportsProgress` is `true`.
* @returns The path of the bugreport file. * @returns The path of the bugreport file.
*/ */
public async generate( async generate(
onProgress?: (progress: string, total: string) => void, onProgress?: (progress: string, total: string) => void,
): Promise<string> { ): Promise<string> {
const process = await this.adb.subprocess.spawn([ const process = await this.adb.subprocess.spawn([
@ -133,11 +133,11 @@ export class BugReportZ extends AdbCommandBase {
return filename; return filename;
} }
public supportStream(major: number, minor: number): boolean { supportStream(major: number, minor: number): boolean {
return major > 1 || minor >= 2; return major > 1 || minor >= 2;
} }
public stream(): ReadableStream<Uint8Array> { stream(): ReadableStream<Uint8Array> {
return new PushReadableStream(async (controller) => { return new PushReadableStream(async (controller) => {
const process = await this.adb.subprocess.spawn([ const process = await this.adb.subprocess.spawn([
"bugreportz", "bugreportz",
@ -173,7 +173,7 @@ export class BugReportZ extends AdbCommandBase {
// https://cs.android.com/android/platform/superproject/+/master:frameworks/native/cmds/bugreport/bugreport.cpp;drc=9b73bf07d73dbab5b792632e1e233edbad77f5fd;bpv=0;bpt=0 // https://cs.android.com/android/platform/superproject/+/master:frameworks/native/cmds/bugreport/bugreport.cpp;drc=9b73bf07d73dbab5b792632e1e233edbad77f5fd;bpv=0;bpt=0
export class BugReport extends AdbCommandBase { export class BugReport extends AdbCommandBase {
public generate(): ReadableStream<Uint8Array> { generate(): ReadableStream<Uint8Array> {
return new WrapReadableStream(async () => { return new WrapReadableStream(async () => {
const process = await this.adb.subprocess.spawn(["bugreport"]); const process = await this.adb.subprocess.spawn(["bugreport"]);
return process.stdout; return process.stdout;

View file

@ -14,26 +14,26 @@ import { ConcatStringStream, DecodeUtf8Stream } from "@yume-chan/stream-extra";
export class Cmd extends AdbCommandBase { export class Cmd extends AdbCommandBase {
#supportsShellV2: boolean; #supportsShellV2: boolean;
public get supportsShellV2() { get supportsShellV2() {
return this.#supportsShellV2; return this.#supportsShellV2;
} }
#supportsCmd: boolean; #supportsCmd: boolean;
public get supportsCmd() { get supportsCmd() {
return this.#supportsCmd; return this.#supportsCmd;
} }
#supportsAbb: boolean; #supportsAbb: boolean;
public get supportsAbb() { get supportsAbb() {
return this.#supportsAbb; return this.#supportsAbb;
} }
#supportsAbbExec: boolean; #supportsAbbExec: boolean;
public get supportsAbbExec() { get supportsAbbExec() {
return this.#supportsAbbExec; return this.#supportsAbbExec;
} }
public constructor(adb: Adb) { constructor(adb: Adb) {
super(adb); super(adb);
this.#supportsShellV2 = adb.supportsFeature(AdbFeature.ShellV2); this.#supportsShellV2 = adb.supportsFeature(AdbFeature.ShellV2);
this.#supportsCmd = adb.supportsFeature(AdbFeature.Cmd); this.#supportsCmd = adb.supportsFeature(AdbFeature.Cmd);
@ -41,7 +41,7 @@ export class Cmd extends AdbCommandBase {
this.#supportsAbbExec = adb.supportsFeature(AdbFeature.AbbExec); this.#supportsAbbExec = adb.supportsFeature(AdbFeature.AbbExec);
} }
public async spawn( async spawn(
shellProtocol: boolean, shellProtocol: boolean,
command: string, command: string,
...args: string[] ...args: string[]
@ -76,7 +76,7 @@ export class Cmd extends AdbCommandBase {
throw new Error("Not supported"); throw new Error("Not supported");
} }
public async spawnAndWait( async spawnAndWait(
command: string, command: string,
...args: string[] ...args: string[]
): Promise<AdbSubprocessWaitResult> { ): Promise<AdbSubprocessWaitResult> {

View file

@ -52,55 +52,63 @@ export const DemoModeStatusBarModes = [
export type DemoModeStatusBarMode = (typeof DemoModeStatusBarModes)[number]; export type DemoModeStatusBarMode = (typeof DemoModeStatusBarModes)[number];
export class DemoMode extends AdbCommandBase { export class DemoMode extends AdbCommandBase {
private settings: Settings; #settings: Settings;
constructor(adb: Adb) { constructor(adb: Adb) {
super(adb); super(adb);
this.settings = new Settings(adb); this.#settings = new Settings(adb);
} }
public static readonly AllowedSettingKey = "sysui_demo_allowed"; static readonly ALLOWED_SETTING_KEY = "sysui_demo_allowed";
// Demo Mode actually doesn't have a setting indicates its enablement // Demo Mode actually doesn't have a setting indicates its enablement
// However Developer Mode menu uses this key // However Developer Mode menu uses this key
// So we can only try our best to guess if it's enabled // So we can only try our best to guess if it's enabled
public static readonly EnabledSettingKey = "sysui_tuner_demo_on"; static readonly ENABLED_SETTING_KEY = "sysui_tuner_demo_on";
public async getAllowed(): Promise<boolean> { async getAllowed(): Promise<boolean> {
const output = await this.settings.get( const output = await this.#settings.get(
"global", "global",
DemoMode.AllowedSettingKey, DemoMode.ALLOWED_SETTING_KEY,
); );
return output === "1"; return output === "1";
} }
public async setAllowed(value: boolean): Promise<void> { async setAllowed(value: boolean): Promise<void> {
if (value) { if (value) {
await this.settings.put("global", DemoMode.AllowedSettingKey, "1"); await this.#settings.put(
"global",
DemoMode.ALLOWED_SETTING_KEY,
"1",
);
} else { } else {
await this.setEnabled(false); await this.setEnabled(false);
await this.settings.delete("global", DemoMode.AllowedSettingKey); await this.#settings.delete("global", DemoMode.ALLOWED_SETTING_KEY);
} }
} }
public async getEnabled(): Promise<boolean> { async getEnabled(): Promise<boolean> {
const result = await this.settings.get( const result = await this.#settings.get(
"global", "global",
DemoMode.EnabledSettingKey, DemoMode.ENABLED_SETTING_KEY,
); );
return result === "1"; return result === "1";
} }
public async setEnabled(value: boolean): Promise<void> { async setEnabled(value: boolean): Promise<void> {
if (value) { if (value) {
await this.settings.put("global", DemoMode.EnabledSettingKey, "1"); await this.#settings.put(
"global",
DemoMode.ENABLED_SETTING_KEY,
"1",
);
} else { } else {
await this.settings.delete("global", DemoMode.EnabledSettingKey); await this.#settings.delete("global", DemoMode.ENABLED_SETTING_KEY);
await this.broadcast("exit"); await this.broadcast("exit");
} }
} }
public async broadcast( async broadcast(
command: string, command: string,
extra?: Record<string, string>, extra?: Record<string, string>,
): Promise<void> { ): Promise<void> {
@ -122,31 +130,27 @@ export class DemoMode extends AdbCommandBase {
]); ]);
} }
public async setBatteryLevel(level: number): Promise<void> { async setBatteryLevel(level: number): Promise<void> {
await this.broadcast("battery", { level: level.toString() }); await this.broadcast("battery", { level: level.toString() });
} }
public async setBatteryCharging(value: boolean): Promise<void> { async setBatteryCharging(value: boolean): Promise<void> {
await this.broadcast("battery", { plugged: value.toString() }); await this.broadcast("battery", { plugged: value.toString() });
} }
public async setPowerSaveMode(value: boolean): Promise<void> { async setPowerSaveMode(value: boolean): Promise<void> {
await this.broadcast("battery", { powersave: value.toString() }); await this.broadcast("battery", { powersave: value.toString() });
} }
public async setAirplaneMode(show: boolean): Promise<void> { async setAirplaneMode(show: boolean): Promise<void> {
await this.broadcast("network", { airplane: show ? "show" : "hide" }); await this.broadcast("network", { airplane: show ? "show" : "hide" });
} }
public async setWifiSignalStrength( async setWifiSignalStrength(value: DemoModeSignalStrength): Promise<void> {
value: DemoModeSignalStrength,
): Promise<void> {
await this.broadcast("network", { wifi: "show", level: value }); await this.broadcast("network", { wifi: "show", level: value });
} }
public async setMobileDataType( async setMobileDataType(value: DemoModeMobileDataType): Promise<void> {
value: DemoModeMobileDataType,
): Promise<void> {
for (let i = 0; i < 2; i += 1) { for (let i = 0; i < 2; i += 1) {
await this.broadcast("network", { await this.broadcast("network", {
mobile: "show", mobile: "show",
@ -164,60 +168,60 @@ export class DemoMode extends AdbCommandBase {
} }
} }
public async setMobileSignalStrength( async setMobileSignalStrength(
value: DemoModeSignalStrength, value: DemoModeSignalStrength,
): Promise<void> { ): Promise<void> {
await this.broadcast("network", { mobile: "show", level: value }); await this.broadcast("network", { mobile: "show", level: value });
} }
public async setNoSimCardIcon(show: boolean): Promise<void> { async setNoSimCardIcon(show: boolean): Promise<void> {
await this.broadcast("network", { nosim: show ? "show" : "hide" }); await this.broadcast("network", { nosim: show ? "show" : "hide" });
} }
public async setStatusBarMode(mode: DemoModeStatusBarMode): Promise<void> { async setStatusBarMode(mode: DemoModeStatusBarMode): Promise<void> {
await this.broadcast("bars", { mode }); await this.broadcast("bars", { mode });
} }
public async setVibrateModeEnabled(value: boolean): Promise<void> { async setVibrateModeEnabled(value: boolean): Promise<void> {
// https://cs.android.com/android/platform/superproject/+/master:frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/phone/DemoStatusIcons.java;l=103 // https://cs.android.com/android/platform/superproject/+/master:frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/phone/DemoStatusIcons.java;l=103
await this.broadcast("status", { volume: value ? "vibrate" : "hide" }); await this.broadcast("status", { volume: value ? "vibrate" : "hide" });
} }
public async setBluetoothConnected(value: boolean): Promise<void> { async setBluetoothConnected(value: boolean): Promise<void> {
// https://cs.android.com/android/platform/superproject/+/master:frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/phone/DemoStatusIcons.java;l=114 // https://cs.android.com/android/platform/superproject/+/master:frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/phone/DemoStatusIcons.java;l=114
await this.broadcast("status", { await this.broadcast("status", {
bluetooth: value ? "connected" : "hide", bluetooth: value ? "connected" : "hide",
}); });
} }
public async setLocatingIcon(show: boolean): Promise<void> { async setLocatingIcon(show: boolean): Promise<void> {
await this.broadcast("status", { location: show ? "show" : "hide" }); await this.broadcast("status", { location: show ? "show" : "hide" });
} }
public async setAlarmIcon(show: boolean): Promise<void> { async setAlarmIcon(show: boolean): Promise<void> {
await this.broadcast("status", { alarm: show ? "show" : "hide" }); await this.broadcast("status", { alarm: show ? "show" : "hide" });
} }
public async setSyncingIcon(show: boolean): Promise<void> { async setSyncingIcon(show: boolean): Promise<void> {
await this.broadcast("status", { sync: show ? "show" : "hide" }); await this.broadcast("status", { sync: show ? "show" : "hide" });
} }
public async setMuteIcon(show: boolean): Promise<void> { async setMuteIcon(show: boolean): Promise<void> {
await this.broadcast("status", { mute: show ? "show" : "hide" }); await this.broadcast("status", { mute: show ? "show" : "hide" });
} }
public async setSpeakerPhoneIcon(show: boolean): Promise<void> { async setSpeakerPhoneIcon(show: boolean): Promise<void> {
await this.broadcast("status", { await this.broadcast("status", {
speakerphone: show ? "show" : "hide", speakerphone: show ? "show" : "hide",
}); });
} }
public async setNotificationsVisibility(show: boolean): Promise<void> { async setNotificationsVisibility(show: boolean): Promise<void> {
// https://cs.android.com/android/platform/superproject/+/master:frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java;l=3131 // https://cs.android.com/android/platform/superproject/+/master:frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java;l=3131
await this.broadcast("notifications", { visible: show.toString() }); await this.broadcast("notifications", { visible: show.toString() });
} }
public async setTime(hour: number, minute: number): Promise<void> { async setTime(hour: number, minute: number): Promise<void> {
await this.broadcast("clock", { await this.broadcast("clock", {
// cspell: disable-next-line // cspell: disable-next-line
hhmm: hhmm:

View file

@ -1,7 +1,7 @@
import { AdbCommandBase } from "@yume-chan/adb"; import { AdbCommandBase } from "@yume-chan/adb";
export class DumpSys extends AdbCommandBase { export class DumpSys extends AdbCommandBase {
public async diskStats() { async diskStats() {
const output = await this.adb.subprocess.spawnAndWaitLegacy([ const output = await this.adb.subprocess.spawnAndWaitLegacy([
"dumpsys", "dumpsys",
"diskstats", "diskstats",
@ -34,7 +34,7 @@ export class DumpSys extends AdbCommandBase {
}; };
} }
public async battery() { async battery() {
const output = await this.adb.subprocess.spawnAndWaitLegacy([ const output = await this.adb.subprocess.spawnAndWaitLegacy([
"dumpsys", "dumpsys",
"battery", "battery",

View file

@ -382,35 +382,35 @@ export interface LogSize {
} }
export class Logcat extends AdbCommandBase { export class Logcat extends AdbCommandBase {
public static logIdToName(id: LogId): string { static logIdToName(id: LogId): string {
return LogId[id]!; return LogId[id]!;
} }
public static logNameToId(name: string): LogId { static logNameToId(name: string): LogId {
const key = name[0]!.toUpperCase() + name.substring(1); const key = name[0]!.toUpperCase() + name.substring(1);
return LogId[key as keyof typeof LogId]; return LogId[key as keyof typeof LogId];
} }
public static joinLogId(ids: LogId[]): string { static joinLogId(ids: LogId[]): string {
return ids.map((id) => Logcat.logIdToName(id)).join(","); return ids.map((id) => Logcat.logIdToName(id)).join(",");
} }
public static parseSize(value: number, multiplier: string): number { static parseSize(value: number, multiplier: string): number {
const MULTIPLIERS = ["", "Ki", "Mi", "Gi"]; const MULTIPLIERS = ["", "Ki", "Mi", "Gi"];
return value * 1024 ** (MULTIPLIERS.indexOf(multiplier) || 0); return value * 1024 ** (MULTIPLIERS.indexOf(multiplier) || 0);
} }
// TODO: logcat: Support output format before Android 10 // TODO: logcat: Support output format before Android 10
// ref https://android-review.googlesource.com/c/platform/system/core/+/748128 // ref https://android-review.googlesource.com/c/platform/system/core/+/748128
public static readonly LOG_SIZE_REGEX_10 = static readonly LOG_SIZE_REGEX_10 =
/(.*): ring buffer is (.*) (.*)B \((.*) (.*)B consumed\), max entry is (.*) B, max payload is (.*) B/; /(.*): ring buffer is (.*) (.*)B \((.*) (.*)B consumed\), max entry is (.*) B, max payload is (.*) B/;
// Android 11 added `readable` part // Android 11 added `readable` part
// ref https://android-review.googlesource.com/c/platform/system/core/+/1390940 // ref https://android-review.googlesource.com/c/platform/system/core/+/1390940
public static readonly LOG_SIZE_REGEX_11 = static readonly LOG_SIZE_REGEX_11 =
/(.*): ring buffer is (.*) (.*)B \((.*) (.*)B consumed, (.*) (.*)B readable\), max entry is (.*) B, max payload is (.*) B/; /(.*): ring buffer is (.*) (.*)B \((.*) (.*)B consumed, (.*) (.*)B readable\), max entry is (.*) B, max payload is (.*) B/;
public async getLogSize(ids?: LogId[]): Promise<LogSize[]> { async getLogSize(ids?: LogId[]): Promise<LogSize[]> {
const { stdout } = await this.adb.subprocess.spawn([ const { stdout } = await this.adb.subprocess.spawn([
"logcat", "logcat",
"-g", "-g",
@ -469,7 +469,7 @@ export class Logcat extends AdbCommandBase {
return result; return result;
} }
public async clear(ids?: LogId[]) { async clear(ids?: LogId[]) {
await this.adb.subprocess.spawnAndWait([ await this.adb.subprocess.spawnAndWait([
"logcat", "logcat",
"-c", "-c",
@ -477,7 +477,7 @@ export class Logcat extends AdbCommandBase {
]); ]);
} }
public binary(options?: LogcatOptions): ReadableStream<AndroidLogEntry> { binary(options?: LogcatOptions): ReadableStream<AndroidLogEntry> {
return new WrapReadableStream(async () => { return new WrapReadableStream(async () => {
// TODO: make `spawn` return synchronously with streams pending // TODO: make `spawn` return synchronously with streams pending
// so it's easier to chain them. // so it's easier to chain them.

View file

@ -3,12 +3,12 @@ import { describe, expect, it } from "@jest/globals";
import { OverlayDisplay } from "./overlay-display.js"; import { OverlayDisplay } from "./overlay-display.js";
describe("OverlayDisplay", () => { describe("OverlayDisplay", () => {
describe("OverlayDisplayDevicesFormat", () => { describe("SETTING_FORMAT", () => {
// values are from https://cs.android.com/android/platform/superproject/+/master:frameworks/base/packages/SettingsLib/res/values/arrays.xml;l=468;drc=60c1d392225bc6e1601693c7d5cfdf1d7f510015 // values are from https://cs.android.com/android/platform/superproject/+/master:frameworks/base/packages/SettingsLib/res/values/arrays.xml;l=468;drc=60c1d392225bc6e1601693c7d5cfdf1d7f510015
it("should parse 0 device", () => { it("should parse 0 device", () => {
expect( expect(
OverlayDisplay.OverlayDisplayDevicesFormat.parse({ OverlayDisplay.SETTING_FORMAT.parse({
value: "", value: "",
position: 0, position: 0,
}), }),
@ -17,7 +17,7 @@ describe("OverlayDisplay", () => {
it("should parse 1 mode", () => { it("should parse 1 mode", () => {
expect( expect(
OverlayDisplay.OverlayDisplayDevicesFormat.parse({ OverlayDisplay.SETTING_FORMAT.parse({
value: "720x480/142", value: "720x480/142",
position: 0, position: 0,
}), }),
@ -37,7 +37,7 @@ describe("OverlayDisplay", () => {
it("should parse 2 modes", () => { it("should parse 2 modes", () => {
expect( expect(
OverlayDisplay.OverlayDisplayDevicesFormat.parse({ OverlayDisplay.SETTING_FORMAT.parse({
value: "1920x1080/320|3840x2160/640", value: "1920x1080/320|3840x2160/640",
position: 0, position: 0,
}), }),
@ -62,7 +62,7 @@ describe("OverlayDisplay", () => {
it("should parse 2 device", () => { it("should parse 2 device", () => {
expect( expect(
OverlayDisplay.OverlayDisplayDevicesFormat.parse({ OverlayDisplay.SETTING_FORMAT.parse({
value: "1280x720/213;1920x1080/320", value: "1280x720/213;1920x1080/320",
position: 0, position: 0,
}), }),
@ -92,7 +92,7 @@ describe("OverlayDisplay", () => {
it("should parse flags", () => { it("should parse flags", () => {
expect( expect(
OverlayDisplay.OverlayDisplayDevicesFormat.parse({ OverlayDisplay.SETTING_FORMAT.parse({
value: "1920x1080/320|3840x2160/640,secure", value: "1920x1080/320|3840x2160/640,secure",
position: 0, position: 0,
}), }),

View file

@ -18,12 +18,11 @@ export interface OverlayDisplayDevice {
} }
export class OverlayDisplay extends AdbCommandBase { export class OverlayDisplay extends AdbCommandBase {
private settings: Settings; #settings: Settings;
public static readonly OVERLAY_DISPLAY_DEVICES_KEY = static readonly SETTING_KEY = "overlay_display_devices";
"overlay_display_devices";
public static readonly OverlayDisplayDevicesFormat = p.separated( static readonly SETTING_FORMAT = p.separated(
";", ";",
p.sequence( p.sequence(
{ {
@ -62,14 +61,14 @@ export class OverlayDisplay extends AdbCommandBase {
constructor(adb: Adb) { constructor(adb: Adb) {
super(adb); super(adb);
this.settings = new Settings(adb); this.#settings = new Settings(adb);
} }
public async get() { async get() {
return OverlayDisplay.OverlayDisplayDevicesFormat.parse({ return OverlayDisplay.SETTING_FORMAT.parse({
value: await this.settings.get( value: await this.#settings.get(
"global", "global",
OverlayDisplay.OVERLAY_DISPLAY_DEVICES_KEY, OverlayDisplay.SETTING_KEY,
), ),
position: 0, position: 0,
}).map((device) => ({ }).map((device) => ({
@ -82,11 +81,11 @@ export class OverlayDisplay extends AdbCommandBase {
})); }));
} }
public async set(devices: OverlayDisplayDevice[]) { async set(devices: OverlayDisplayDevice[]) {
await this.settings.put( await this.#settings.put(
"global", "global",
OverlayDisplay.OVERLAY_DISPLAY_DEVICES_KEY, OverlayDisplay.SETTING_KEY,
OverlayDisplay.OverlayDisplayDevicesFormat.stringify( OverlayDisplay.SETTING_FORMAT.stringify(
devices.map((device) => { devices.map((device) => {
const flags: ( const flags: (
| "secure" | "secure"

View file

@ -219,14 +219,14 @@ export interface PackageManagerListPackagesResult {
} }
export class PackageManager extends AdbCommandBase { export class PackageManager extends AdbCommandBase {
private _cmd: Cmd; #cmd: Cmd;
public constructor(adb: Adb) { constructor(adb: Adb) {
super(adb); super(adb);
this._cmd = new Cmd(adb); this.#cmd = new Cmd(adb);
} }
private buildArguments<T>( #buildArguments<T>(
commands: string[], commands: string[],
options: Partial<T> | undefined, options: Partial<T> | undefined,
map: Record<keyof T, string>, map: Record<keyof T, string>,
@ -253,27 +253,27 @@ export class PackageManager extends AdbCommandBase {
return args; return args;
} }
private buildInstallArguments( #buildInstallArguments(
options: Partial<PackageManagerInstallOptions> | undefined, options: Partial<PackageManagerInstallOptions> | undefined,
): string[] { ): string[] {
return this.buildArguments( return this.#buildArguments(
["install"], ["install"],
options, options,
PACKAGE_MANAGER_INSTALL_OPTIONS_MAP, PACKAGE_MANAGER_INSTALL_OPTIONS_MAP,
); );
} }
public async install( async install(
apks: string[], apks: string[],
options?: Partial<PackageManagerInstallOptions>, options?: Partial<PackageManagerInstallOptions>,
): Promise<string> { ): Promise<string> {
const args = this.buildInstallArguments(options); const args = this.#buildInstallArguments(options);
// WIP: old version of pm doesn't support multiple apks // WIP: old version of pm doesn't support multiple apks
args.push(...apks); args.push(...apks);
return await this.adb.subprocess.spawnAndWaitLegacy(args); return await this.adb.subprocess.spawnAndWaitLegacy(args);
} }
public async pushAndInstallStream( async pushAndInstallStream(
stream: ReadableStream<Consumable<Uint8Array>>, stream: ReadableStream<Consumable<Uint8Array>>,
options?: Partial<PackageManagerInstallOptions>, options?: Partial<PackageManagerInstallOptions>,
): Promise<ReadableStream<string>> { ): Promise<ReadableStream<string>> {
@ -295,7 +295,7 @@ export class PackageManager extends AdbCommandBase {
// and `cmd package` launches faster than `pm`. // and `cmd package` launches faster than `pm`.
// But `cmd package` can't read `/data/local/tmp` folder due to SELinux policy, // But `cmd package` can't read `/data/local/tmp` folder due to SELinux policy,
// so installing a file must use `pm`. // so installing a file must use `pm`.
const args = this.buildInstallArguments(options); const args = this.#buildInstallArguments(options);
args.push(filePath); args.push(filePath);
const process = await AdbSubprocessNoneProtocol.raw( const process = await AdbSubprocessNoneProtocol.raw(
this.adb, this.adb,
@ -309,7 +309,7 @@ export class PackageManager extends AdbCommandBase {
}); });
} }
public async installStream( async installStream(
size: number, size: number,
stream: ReadableStream<Consumable<Uint8Array>>, stream: ReadableStream<Consumable<Uint8Array>>,
options?: Partial<PackageManagerInstallOptions>, options?: Partial<PackageManagerInstallOptions>,
@ -317,20 +317,20 @@ export class PackageManager extends AdbCommandBase {
// Android 7 added both `cmd` command and streaming install support, // Android 7 added both `cmd` command and streaming install support,
// we can't detect whether `pm` supports streaming install, // we can't detect whether `pm` supports streaming install,
// so we detect `cmd` command support instead. // so we detect `cmd` command support instead.
if (!this._cmd.supportsCmd) { if (!this.#cmd.supportsCmd) {
return this.pushAndInstallStream(stream, options); return this.pushAndInstallStream(stream, options);
} }
const args = this.buildInstallArguments(options); const args = this.#buildInstallArguments(options);
// Remove `pm` from args, final command will starts with `cmd package install` // Remove `pm` from args, final command will starts with `cmd package install`
args.shift(); args.shift();
args.push("-S", size.toString()); args.push("-S", size.toString());
const process = await this._cmd.spawn(false, "package", ...args); const process = await this.#cmd.spawn(false, "package", ...args);
await stream.pipeTo(process.stdin); await stream.pipeTo(process.stdin);
return process.stdout.pipeThrough(new DecodeUtf8Stream()); return process.stdout.pipeThrough(new DecodeUtf8Stream());
} }
public static parsePackageListItem( static parsePackageListItem(
line: string, line: string,
): PackageManagerListPackagesResult { ): PackageManagerListPackagesResult {
line = line.substring("package:".length); line = line.substring("package:".length);
@ -381,10 +381,10 @@ export class PackageManager extends AdbCommandBase {
}; };
} }
public async listPackages( async listPackages(
options?: Partial<PackageManagerListPackagesOptions>, options?: Partial<PackageManagerListPackagesOptions>,
): Promise<PackageManagerListPackagesResult[]> { ): Promise<PackageManagerListPackagesResult[]> {
const args = this.buildArguments( const args = this.#buildArguments(
["list", "packages"], ["list", "packages"],
options, options,
PACKAGE_MANAGER_LIST_PACKAGES_OPTIONS_MAP, PACKAGE_MANAGER_LIST_PACKAGES_OPTIONS_MAP,

View file

@ -24,12 +24,12 @@ export interface SettingsPutOptions extends SettingsOptions {
export class Settings extends AdbCommandBase { export class Settings extends AdbCommandBase {
#cmd: Cmd; #cmd: Cmd;
public constructor(adb: Adb) { constructor(adb: Adb) {
super(adb); super(adb);
this.#cmd = new Cmd(adb); this.#cmd = new Cmd(adb);
} }
public async base( async base(
verb: string, verb: string,
namespace: SettingsNamespace, namespace: SettingsNamespace,
options: SettingsOptions | undefined, options: SettingsOptions | undefined,
@ -61,7 +61,7 @@ export class Settings extends AdbCommandBase {
return output.stdout; return output.stdout;
} }
public async get( async get(
namespace: SettingsNamespace, namespace: SettingsNamespace,
key: string, key: string,
options?: SettingsOptions, options?: SettingsOptions,
@ -71,7 +71,7 @@ export class Settings extends AdbCommandBase {
return output.substring(0, output.length - 1); return output.substring(0, output.length - 1);
} }
public async delete( async delete(
namespace: SettingsNamespace, namespace: SettingsNamespace,
key: string, key: string,
options?: SettingsOptions, options?: SettingsOptions,
@ -79,7 +79,7 @@ export class Settings extends AdbCommandBase {
await this.base("delete", namespace, options, key); await this.base("delete", namespace, options, key);
} }
public async put( async put(
namespace: SettingsNamespace, namespace: SettingsNamespace,
key: string, key: string,
value: string, value: string,
@ -95,18 +95,18 @@ export class Settings extends AdbCommandBase {
await this.base("put", namespace, options, ...args); await this.base("put", namespace, options, ...args);
} }
public reset( reset(
namespace: SettingsNamespace, namespace: SettingsNamespace,
mode: SettingsResetMode, mode: SettingsResetMode,
options?: SettingsOptions, options?: SettingsOptions,
): Promise<void>; ): Promise<void>;
public reset( reset(
namespace: SettingsNamespace, namespace: SettingsNamespace,
packageName: string, packageName: string,
tag?: string, tag?: string,
options?: SettingsOptions, options?: SettingsOptions,
): Promise<void>; ): Promise<void>;
public async reset( async reset(
namespace: SettingsNamespace, namespace: SettingsNamespace,
modeOrPackageName: string, modeOrPackageName: string,
tagOrOptions?: string | SettingsOptions, tagOrOptions?: string | SettingsOptions,

View file

@ -6,7 +6,7 @@ export class ParseError extends Error {
return this.#expected; return this.#expected;
} }
public constructor(expected: string[]) { constructor(expected: string[]) {
super(`Expected ${expected.join(", ")}`); super(`Expected ${expected.join(", ")}`);
this.#expected = expected; this.#expected = expected;
} }
@ -29,34 +29,20 @@ type UnionResult<T extends readonly Format<unknown>[]> = Exclude<
undefined undefined
>; >;
type UnionToIntersection<T> = (
T extends unknown ? (x: T) => void : never
) extends (x: infer R) => void
? R
: never;
type SequenceResult< type SequenceResult<
T extends readonly ( T extends readonly (
| Format<unknown> | Format<unknown>
| { name: string; format: Format<unknown> } | { name: string; format: Format<unknown> }
)[], )[],
> = UnionToIntersection< > = {
{ [K in keyof T as K extends `${number}`
[K in keyof T]: T[K] extends { ? T[K] extends { name: infer N extends string }
name: string; ? N
format: Format<unknown>;
}
? Record<
T[K]["name"],
T[K]["format"] extends Format<infer F>
? Exclude<F, undefined>
: never : never
> : never]: T[K] extends { format: Format<infer F> } ? F : never;
} extends infer R extends Record<string, unknown>
? R
: never; : never;
}[number]
>;
type Evaluate<T> = T extends infer U ? { [K in keyof U]: U[K] } : never;
export const p = { export const p = {
literal: <T extends string>(value: T): Format<T> => ({ literal: <T extends string>(value: T): Format<T> => ({
@ -174,7 +160,7 @@ export const p = {
)[], )[],
>( >(
...args: T ...args: T
): Format<Evaluate<SequenceResult<T>>> => ({ ): Format<SequenceResult<T>> => ({
parse(reader: Reader) { parse(reader: Reader) {
const result: Record<string, unknown> = {}; const result: Record<string, unknown> = {};
for (const part of args) { for (const part of args) {
@ -184,9 +170,9 @@ export const p = {
void part.parse(reader); void part.parse(reader);
} }
} }
return result as Evaluate<SequenceResult<T>>; return result as SequenceResult<T>;
}, },
stringify: (value: Evaluate<SequenceResult<T>>) => { stringify: (value: SequenceResult<T>) => {
let result = ""; let result = "";
for (const part of args) { for (const part of args) {
if ("name" in part) { if ("name" in part) {

View file

@ -77,7 +77,7 @@ export class AoaHidDevice {
* @param reportDescriptor The HID report descriptor. * @param reportDescriptor The HID report descriptor.
* @returns An instance of AoaHidDevice to send events. * @returns An instance of AoaHidDevice to send events.
*/ */
public static async register( static async register(
device: USBDevice, device: USBDevice,
accessoryId: number, accessoryId: number,
reportDescriptor: Uint8Array reportDescriptor: Uint8Array
@ -87,19 +87,19 @@ export class AoaHidDevice {
return new AoaHidDevice(device, accessoryId); return new AoaHidDevice(device, accessoryId);
} }
private _device: USBDevice; #device: USBDevice;
private _accessoryId: number; #accessoryId: number;
private constructor(device: USBDevice, accessoryId: number) { constructor(device: USBDevice, accessoryId: number) {
this._device = device; this.#device = device;
this._accessoryId = accessoryId; this.#accessoryId = accessoryId;
} }
public async sendInputReport(event: Uint8Array) { async sendInputReport(event: Uint8Array) {
await aoaHidSendInputReport(this._device, this._accessoryId, event); await aoaHidSendInputReport(this.#device, this.#accessoryId, event);
} }
public async unregister() { async unregister() {
await aoaHidUnregister(this._device, this._accessoryId); await aoaHidUnregister(this.#device, this.#accessoryId);
} }
} }

View file

@ -229,7 +229,7 @@ export class HidKeyboard {
* It's compatible with the legacy boot protocol. (1 byte modifier, 1 byte reserved, 6 bytes key codes). * It's compatible with the legacy boot protocol. (1 byte modifier, 1 byte reserved, 6 bytes key codes).
* Technically it doesn't need to be compatible with the legacy boot protocol, but it's the most common implementation. * Technically it doesn't need to be compatible with the legacy boot protocol, but it's the most common implementation.
*/ */
public static readonly DESCRIPTOR = new Uint8Array( static readonly DESCRIPTOR = new Uint8Array(
// prettier-ignore // prettier-ignore
[ [
0x05, 0x01, // Usage Page (Generic Desktop) 0x05, 0x01, // Usage Page (Generic Desktop)
@ -271,35 +271,35 @@ export class HidKeyboard {
] ]
); );
private _modifiers = 0; #modifiers = 0;
private _keys: Set<HidKeyCode> = new Set(); #keys: Set<HidKeyCode> = new Set();
public down(key: HidKeyCode) { down(key: HidKeyCode) {
if (key >= HidKeyCode.ControlLeft && key <= HidKeyCode.MetaRight) { if (key >= HidKeyCode.ControlLeft && key <= HidKeyCode.MetaRight) {
this._modifiers |= 1 << (key - HidKeyCode.ControlLeft); this.#modifiers |= 1 << (key - HidKeyCode.ControlLeft);
} else { } else {
this._keys.add(key); this.#keys.add(key);
} }
} }
public up(key: HidKeyCode) { up(key: HidKeyCode) {
if (key >= HidKeyCode.ControlLeft && key <= HidKeyCode.MetaRight) { if (key >= HidKeyCode.ControlLeft && key <= HidKeyCode.MetaRight) {
this._modifiers &= ~(1 << (key - HidKeyCode.ControlLeft)); this.#modifiers &= ~(1 << (key - HidKeyCode.ControlLeft));
} else { } else {
this._keys.delete(key); this.#keys.delete(key);
} }
} }
public reset() { reset() {
this._modifiers = 0; this.#modifiers = 0;
this._keys.clear(); this.#keys.clear();
} }
public serializeInputReport() { serializeInputReport() {
const buffer = new Uint8Array(8); const buffer = new Uint8Array(8);
buffer[0] = this._modifiers; buffer[0] = this.#modifiers;
let i = 2; let i = 2;
for (const key of this._keys) { for (const key of this.#keys) {
buffer[i] = key; buffer[i] = key;
i += 1; i += 1;
if (i >= 8) { if (i >= 8) {

View file

@ -1,5 +1,5 @@
export class HidMouse { export class HidMouse {
public static readonly descriptor = new Uint8Array( static readonly descriptor = new Uint8Array(
// prettier-ignore // prettier-ignore
[ [
0x05, 0x01, // Usage Page (Generic Desktop) 0x05, 0x01, // Usage Page (Generic Desktop)
@ -42,7 +42,7 @@ export class HidMouse {
] ]
); );
public static serializeInputReport( static serializeInputReport(
movementX: number, movementX: number,
movementY: number, movementY: number,
buttons: number, buttons: number,

View file

@ -81,40 +81,40 @@ interface Finger {
* A ten-point touch screen. * A ten-point touch screen.
*/ */
export class HidTouchScreen { export class HidTouchScreen {
public static readonly FINGER_DESCRIPTOR = FINGER_DESCRIPTOR; static readonly FINGER_DESCRIPTOR = FINGER_DESCRIPTOR;
public static readonly DESCRIPTOR = DESCRIPTOR; static readonly DESCRIPTOR = DESCRIPTOR;
private fingers: Map<number, Finger> = new Map(); #fingers: Map<number, Finger> = new Map();
public down(id: number, x: number, y: number) { down(id: number, x: number, y: number) {
if (this.fingers.size >= 10) { if (this.#fingers.size >= 10) {
return; return;
} }
this.fingers.set(id, { this.#fingers.set(id, {
x, x,
y, y,
}); });
} }
public move(id: number, x: number, y: number) { move(id: number, x: number, y: number) {
const finger = this.fingers.get(id); const finger = this.#fingers.get(id);
if (finger) { if (finger) {
finger.x = x; finger.x = x;
finger.y = y; finger.y = y;
} }
} }
public up(id: number) { up(id: number) {
this.fingers.delete(id); this.#fingers.delete(id);
} }
public serializeInputReport(): Uint8Array { serializeInputReport(): Uint8Array {
const report = new Uint8Array(1 + 6 * 10); const report = new Uint8Array(1 + 6 * 10);
report[0] = this.fingers.size; report[0] = this.#fingers.size;
let offset = 1; let offset = 1;
for (const [id, finger] of this.fingers) { for (const [id, finger] of this.#fingers) {
report[offset] = id; report[offset] = id;
offset += 1; offset += 1;

View file

@ -43,11 +43,11 @@ describe("BTree", () => {
function validateTree(tree: BTree) { function validateTree(tree: BTree) {
if (tree.size === 0) { if (tree.size === 0) {
expect(tree["_root"].keyCount).toBe(0); expect(tree.root.keyCount).toBe(0);
return; return;
} }
validateNode(tree["_root"], true); validateNode(tree.root, true);
} }
for (let order = 3; order < 10; order += 1) { for (let order = 3; order < 10; order += 1) {

View file

@ -32,7 +32,7 @@ export class BTreeNode {
height: number; height: number;
children: BTreeNode[]; children: BTreeNode[];
public constructor( constructor(
order: number, order: number,
keys: Int32Array, keys: Int32Array,
keyCount: number, keyCount: number,
@ -124,7 +124,7 @@ export class BTreeNode {
}; };
} }
public search(value: number): number { search(value: number): number {
let start = 0; let start = 0;
let end = this.keyCount - 1; let end = this.keyCount - 1;
while (start <= end) { while (start <= end) {
@ -140,7 +140,7 @@ export class BTreeNode {
return ~start; return ~start;
} }
public has(value: number): boolean { has(value: number): boolean {
let index = this.search(value); let index = this.search(value);
if (index >= 0) { if (index >= 0) {
@ -155,7 +155,7 @@ export class BTreeNode {
return false; return false;
} }
public add(value: number): BTreeInsertionResult | boolean { add(value: number): BTreeInsertionResult | boolean {
let index = this.search(value); let index = this.search(value);
if (index >= 0) { if (index >= 0) {
return false; return false;
@ -188,7 +188,7 @@ export class BTreeNode {
return true; return true;
} }
public delete(value: number): boolean { delete(value: number): boolean {
let index = this.search(value); let index = this.search(value);
if (index >= 0) { if (index >= 0) {
this.deleteAt(index); this.deleteAt(index);
@ -209,7 +209,7 @@ export class BTreeNode {
return deleted; return deleted;
} }
public max(): number { max(): number {
if (this.height === 0) { if (this.height === 0) {
return this.keys[this.keyCount - 1]!; return this.keys[this.keyCount - 1]!;
} }
@ -315,7 +315,7 @@ export class BTreeNode {
this.balance(index); this.balance(index);
} }
public *[Symbol.iterator](): Generator<number, void, void> { *[Symbol.iterator](): Generator<number, void, void> {
if (this.height > 0) { if (this.height > 0) {
for (let i = 0; i < this.keyCount; i += 1) { for (let i = 0; i < this.keyCount; i += 1) {
yield* this.children[i]!; yield* this.children[i]!;
@ -331,27 +331,31 @@ export class BTreeNode {
} }
export class BTree { export class BTree {
private _order: number; #order: number;
public get order() { get order() {
return this._order; return this.#order;
} }
private _root: BTreeNode; #root: BTreeNode;
/** @internal */
private _size = 0; get root() {
public get size() { return this.#root;
return this._size;
} }
public constructor(order: number) { #size = 0;
this._order = order; get size() {
return this.#size;
}
constructor(order: number) {
this.#order = order;
const keys = new Int32Array(order - 1); const keys = new Int32Array(order - 1);
const children = new Array<BTreeNode>(order); const children = new Array<BTreeNode>(order);
this._root = new BTreeNode(order, keys, 0, 0, children); this.#root = new BTreeNode(order, keys, 0, 0, children);
} }
public has(value: number) { has(value: number) {
let node = this._root; let node = this.#root;
while (true) { while (true) {
const index = node.search(value); const index = node.search(value);
if (index >= 0) { if (index >= 0) {
@ -365,50 +369,50 @@ export class BTree {
} }
} }
public add(value: number) { add(value: number) {
const split = this._root.add(value); const split = this.#root.add(value);
if (typeof split === "object") { if (typeof split === "object") {
const keys = new Int32Array(this._order - 1); const keys = new Int32Array(this.#order - 1);
keys[0] = split.key; keys[0] = split.key;
const children = new Array<BTreeNode>(this._order); const children = new Array<BTreeNode>(this.#order);
children[0] = this._root; children[0] = this.#root;
children[1] = split.child; children[1] = split.child;
this._root = new BTreeNode( this.#root = new BTreeNode(
this._order, this.#order,
keys, keys,
1, 1,
this._root.height + 1, this.#root.height + 1,
children, children,
); );
} }
if (split) { if (split) {
this._size += 1; this.#size += 1;
} }
return !!split; return !!split;
} }
public delete(value: number) { delete(value: number) {
const deleted = this._root.delete(value); const deleted = this.#root.delete(value);
if (deleted) { if (deleted) {
if (this._root.height > 0 && this._root.keyCount === 0) { if (this.#root.height > 0 && this.#root.keyCount === 0) {
this._root = this._root.children[0]!; this.#root = this.#root.children[0]!;
} }
this._size -= 1; this.#size -= 1;
} }
return deleted; return deleted;
} }
public clear() { clear() {
this._root.keyCount = 0; this.#root.keyCount = 0;
this._root.height = 0; this.#root.height = 0;
// immediately release all references // immediately release all references
this._root.children = new Array<BTreeNode>(this._order); this.#root.children = new Array<BTreeNode>(this.#order);
this._size = 0; this.#size = 0;
} }
public [Symbol.iterator]() { [Symbol.iterator]() {
return this._root[Symbol.iterator](); return this.#root[Symbol.iterator]();
} }
} }

View file

@ -3,28 +3,28 @@ export interface Disposable {
} }
export class AutoDisposable implements Disposable { export class AutoDisposable implements Disposable {
private disposables: Disposable[] = []; #disposables: Disposable[] = [];
public constructor() { constructor() {
this.dispose = this.dispose.bind(this); this.dispose = this.dispose.bind(this);
} }
protected addDisposable<T extends Disposable>(disposable: T): T { protected addDisposable<T extends Disposable>(disposable: T): T {
this.disposables.push(disposable); this.#disposables.push(disposable);
return disposable; return disposable;
} }
public dispose() { dispose() {
for (const disposable of this.disposables) { for (const disposable of this.#disposables) {
disposable.dispose(); disposable.dispose();
} }
this.disposables = []; this.#disposables = [];
} }
} }
export class DisposableList extends AutoDisposable { export class DisposableList extends AutoDisposable {
public add<T extends Disposable>(disposable: T): T { add<T extends Disposable>(disposable: T): T {
return this.addDisposable(disposable); return this.addDisposable(disposable);
} }
} }

View file

@ -23,7 +23,7 @@ export interface AddEventListener<TEvent, TResult = unknown> {
export class EventEmitter<TEvent, TResult = unknown> implements Disposable { export class EventEmitter<TEvent, TResult = unknown> implements Disposable {
protected readonly listeners: EventListenerInfo<TEvent, TResult>[] = []; protected readonly listeners: EventListenerInfo<TEvent, TResult>[] = [];
public constructor() { constructor() {
this.event = this.event.bind(this); this.event = this.event.bind(this);
} }
@ -42,10 +42,7 @@ export class EventEmitter<TEvent, TResult = unknown> implements Disposable {
return remove; return remove;
} }
public event: AddEventListener<TEvent, TResult> = < event: AddEventListener<TEvent, TResult> = <TThis, TArgs extends unknown[]>(
TThis,
TArgs extends unknown[],
>(
listener: EventListener<TEvent, TThis, TArgs, TResult>, listener: EventListener<TEvent, TThis, TArgs, TResult>,
thisArg?: TThis, thisArg?: TThis,
...args: TArgs ...args: TArgs
@ -63,13 +60,13 @@ export class EventEmitter<TEvent, TResult = unknown> implements Disposable {
return this.addEventListener(info); return this.addEventListener(info);
}; };
public fire(e: TEvent) { fire(e: TEvent) {
for (const info of this.listeners.slice()) { for (const info of this.listeners.slice()) {
info.listener.apply(info.thisArg, [e, ...info.args]); info.listener.apply(info.thisArg, [e, ...info.args]);
} }
} }
public dispose() { dispose() {
this.listeners.length = 0; this.listeners.length = 0;
} }
} }

View file

@ -1,12 +1,12 @@
export abstract class PcmPlayer<T> { export abstract class PcmPlayer<T> {
protected abstract sourceName: string; protected abstract sourceName: string;
private _context: AudioContext; #context: AudioContext;
private _worklet: AudioWorkletNode | undefined; #worklet: AudioWorkletNode | undefined;
private _buffer: T[] = []; #buffers: T[] = [];
constructor(sampleRate: number) { constructor(sampleRate: number) {
this._context = new AudioContext({ this.#context = new AudioContext({
latencyHint: "interactive", latencyHint: "interactive",
sampleRate, sampleRate,
}); });
@ -14,38 +14,38 @@ export abstract class PcmPlayer<T> {
protected abstract feedCore(worklet: AudioWorkletNode, source: T): void; protected abstract feedCore(worklet: AudioWorkletNode, source: T): void;
public feed(source: T) { feed(source: T) {
if (this._worklet === undefined) { if (this.#worklet === undefined) {
this._buffer.push(source); this.#buffers.push(source);
return; return;
} }
this.feedCore(this._worklet, source); this.feedCore(this.#worklet, source);
} }
public async start() { async start() {
await this._context.audioWorklet.addModule( await this.#context.audioWorklet.addModule(
new URL("./worker.js", import.meta.url), new URL("./worker.js", import.meta.url),
); );
this._worklet = new AudioWorkletNode(this._context, this.sourceName, { this.#worklet = new AudioWorkletNode(this.#context, this.sourceName, {
numberOfInputs: 0, numberOfInputs: 0,
numberOfOutputs: 1, numberOfOutputs: 1,
outputChannelCount: [2], outputChannelCount: [2],
}); });
this._worklet.connect(this._context.destination); this.#worklet.connect(this.#context.destination);
for (const source of this._buffer) { for (const source of this.#buffers) {
this.feedCore(this._worklet, source); this.feedCore(this.#worklet, source);
} }
this._buffer.length = 0; this.#buffers.length = 0;
} }
async stop() { async stop() {
this._worklet?.disconnect(); this.#worklet?.disconnect();
this._worklet = undefined; this.#worklet = undefined;
await this._context.close(); await this.#context.close();
} }
} }

View file

@ -2,16 +2,16 @@ abstract class SourceProcessor<T>
extends AudioWorkletProcessor extends AudioWorkletProcessor
implements AudioWorkletProcessorImpl implements AudioWorkletProcessorImpl
{ {
private _sources: T[] = []; #sources: T[] = [];
private _sourceSampleCount = 0; #sourceSampleCount = 0;
public constructor() { constructor() {
super(); super();
this.port.onmessage = (event) => { this.port.onmessage = (event) => {
const data = event.data as ArrayBuffer[]; const data = event.data as ArrayBuffer[];
const [source, length] = this.createSource(data); const [source, length] = this.createSource(data);
this._sources.push(source); this.#sources.push(source);
this._sourceSampleCount += length; this.#sourceSampleCount += length;
}; };
} }
@ -26,13 +26,13 @@ abstract class SourceProcessor<T>
// Resample source catch up with output // Resample source catch up with output
// TODO: should we limit the minimum and maximum speed? // TODO: should we limit the minimum and maximum speed?
// TODO: this simple resample method changes pitch // TODO: this simple resample method changes pitch
const sourceIndexStep = this._sourceSampleCount > 48000 ? 1.02 : 1; const sourceIndexStep = this.#sourceSampleCount > 48000 ? 1.02 : 1;
let sourceIndex = 0; let sourceIndex = 0;
while (this._sources.length > 0 && outputIndex < outputLength) { while (this.#sources.length > 0 && outputIndex < outputLength) {
const beginSourceIndex = sourceIndex | 0; const beginSourceIndex = sourceIndex | 0;
let source: T | undefined = this._sources[0]; let source: T | undefined = this.#sources[0];
[source, sourceIndex, outputIndex] = this.copyChunk( [source, sourceIndex, outputIndex] = this.copyChunk(
sourceIndex, sourceIndex,
sourceIndexStep, sourceIndexStep,
@ -44,16 +44,16 @@ abstract class SourceProcessor<T>
); );
const consumedSampleCount = (sourceIndex | 0) - beginSourceIndex; const consumedSampleCount = (sourceIndex | 0) - beginSourceIndex;
this._sourceSampleCount -= consumedSampleCount; this.#sourceSampleCount -= consumedSampleCount;
sourceIndex -= consumedSampleCount; sourceIndex -= consumedSampleCount;
if (source) { if (source) {
// Output full // Output full
this._sources[0] = source; this.#sources[0] = source;
return true; return true;
} }
this._sources.shift(); this.#sources.shift();
} }
if (outputIndex < outputLength) { if (outputIndex < outputLength) {
@ -98,7 +98,7 @@ class Int16SourceProcessor
): [ ): [
source: Int16Array | undefined, source: Int16Array | undefined,
sourceIndex: number, sourceIndex: number,
outputIndex: number outputIndex: number,
] { ] {
const sourceLength = source.length; const sourceLength = source.length;
let sourceSampleIndex = sourceIndex << 1; let sourceSampleIndex = sourceIndex << 1;
@ -145,7 +145,7 @@ class Float32SourceProcessor extends SourceProcessor<Float32Array> {
): [ ): [
source: Float32Array | undefined, source: Float32Array | undefined,
sourceIndex: number, sourceIndex: number,
outputIndex: number outputIndex: number,
] { ] {
const sourceLength = source.length; const sourceLength = source.length;
let sourceSampleIndex = sourceIndex << 1; let sourceSampleIndex = sourceIndex << 1;
@ -192,7 +192,7 @@ class Float32PlanerSourceProcessor extends SourceProcessor<Float32Array[]> {
): [ ): [
source: Float32Array[] | undefined, source: Float32Array[] | undefined,
sourceIndex: number, sourceIndex: number,
outputIndex: number outputIndex: number,
] { ] {
const sourceLeft = source[0]!; const sourceLeft = source[0]!;
const sourceRight = source[1]!; const sourceRight = source[1]!;

View file

@ -38,56 +38,54 @@ function initialize() {
} }
export class TinyH264Decoder implements ScrcpyVideoDecoder { export class TinyH264Decoder implements ScrcpyVideoDecoder {
public static readonly capabilities: Record< static readonly capabilities: Record<string, ScrcpyVideoDecoderCapability> =
string, {
ScrcpyVideoDecoderCapability
> = {
h264: { h264: {
maxProfile: AndroidAvcProfile.Baseline, maxProfile: AndroidAvcProfile.Baseline,
maxLevel: AndroidAvcLevel.Level4, maxLevel: AndroidAvcLevel.Level4,
}, },
}; };
private _renderer: HTMLCanvasElement; #renderer: HTMLCanvasElement;
public get renderer() { get renderer() {
return this._renderer; return this.#renderer;
} }
private _frameRendered = 0; #frameRendered = 0;
public get frameRendered() { get frameRendered() {
return this._frameRendered; return this.#frameRendered;
} }
private _frameSkipped = 0; #frameSkipped = 0;
public get frameSkipped() { get frameSkipped() {
return this._frameSkipped; return this.#frameSkipped;
} }
private _writable: WritableStream<ScrcpyMediaStreamPacket>; #writable: WritableStream<ScrcpyMediaStreamPacket>;
public get writable() { get writable() {
return this._writable; return this.#writable;
} }
private _yuvCanvas: YuvCanvas | undefined; #yuvCanvas: YuvCanvas | undefined;
private _initializer: PromiseResolver<TinyH264Wrapper> | undefined; #initializer: PromiseResolver<TinyH264Wrapper> | undefined;
public constructor() { constructor() {
void initialize(); void initialize();
this._renderer = document.createElement("canvas"); this.#renderer = document.createElement("canvas");
this._writable = new WritableStream<ScrcpyMediaStreamPacket>({ this.#writable = new WritableStream<ScrcpyMediaStreamPacket>({
write: async (packet) => { write: async (packet) => {
switch (packet.type) { switch (packet.type) {
case "configuration": case "configuration":
await this.configure(packet.data); await this.#configure(packet.data);
break; break;
case "data": { case "data": {
if (!this._initializer) { if (!this.#initializer) {
throw new Error("Decoder not configured"); throw new Error("Decoder not configured");
} }
const wrapper = await this._initializer.promise; const wrapper = await this.#initializer.promise;
wrapper.feed(packet.data.slice().buffer); wrapper.feed(packet.data.slice().buffer);
break; break;
} }
@ -96,14 +94,14 @@ export class TinyH264Decoder implements ScrcpyVideoDecoder {
}); });
} }
private async configure(data: Uint8Array) { async #configure(data: Uint8Array) {
this.dispose(); this.dispose();
this._initializer = new PromiseResolver<TinyH264Wrapper>(); this.#initializer = new PromiseResolver<TinyH264Wrapper>();
const { YuvBuffer, YuvCanvas } = await initialize(); const { YuvBuffer, YuvCanvas } = await initialize();
if (!this._yuvCanvas) { if (!this.#yuvCanvas) {
this._yuvCanvas = YuvCanvas.attach(this._renderer); this.#yuvCanvas = YuvCanvas.attach(this.#renderer);
} }
const { const {
@ -134,12 +132,12 @@ export class TinyH264Decoder implements ScrcpyVideoDecoder {
}); });
const wrapper = await createTinyH264Wrapper(); const wrapper = await createTinyH264Wrapper();
this._initializer.resolve(wrapper); this.#initializer.resolve(wrapper);
const uPlaneOffset = encodedWidth * encodedHeight; const uPlaneOffset = encodedWidth * encodedHeight;
const vPlaneOffset = uPlaneOffset + chromaWidth * chromaHeight; const vPlaneOffset = uPlaneOffset + chromaWidth * chromaHeight;
wrapper.onPictureReady(({ data }) => { wrapper.onPictureReady(({ data }) => {
this._frameRendered += 1; this.#frameRendered += 1;
const array = new Uint8Array(data); const array = new Uint8Array(data);
const frame = YuvBuffer.frame( const frame = YuvBuffer.frame(
format, format,
@ -147,17 +145,17 @@ export class TinyH264Decoder implements ScrcpyVideoDecoder {
YuvBuffer.chromaPlane(format, array, chromaWidth, uPlaneOffset), YuvBuffer.chromaPlane(format, array, chromaWidth, uPlaneOffset),
YuvBuffer.chromaPlane(format, array, chromaWidth, vPlaneOffset), YuvBuffer.chromaPlane(format, array, chromaWidth, vPlaneOffset),
); );
this._yuvCanvas!.drawFrame(frame); this.#yuvCanvas!.drawFrame(frame);
}); });
wrapper.feed(data.slice().buffer); wrapper.feed(data.slice().buffer);
} }
public dispose(): void { dispose(): void {
this._initializer?.promise this.#initializer?.promise
.then((wrapper) => wrapper.dispose()) .then((wrapper) => wrapper.dispose())
// NOOP: It's disposed so nobody cares about the error // NOOP: It's disposed so nobody cares about the error
.catch(NOOP); .catch(NOOP);
this._initializer = undefined; this.#initializer = undefined;
} }
} }

View file

@ -146,8 +146,8 @@ declare module "yuv-canvas" {
import type { YUVFrame } from "yuv-buffer"; import type { YUVFrame } from "yuv-buffer";
export default class YUVCanvas { export default class YUVCanvas {
public static attach(canvas: HTMLCanvasElement): YUVCanvas; static attach(canvas: HTMLCanvasElement): YUVCanvas;
public drawFrame(data: YUVFrame): void; drawFrame(data: YUVFrame): void;
} }
} }

View file

@ -1,6 +1,6 @@
import { PromiseResolver } from "@yume-chan/async"; import { PromiseResolver } from "@yume-chan/async";
import { AutoDisposable, EventEmitter } from "@yume-chan/event";
import type { Disposable } from "@yume-chan/event"; import type { Disposable } from "@yume-chan/event";
import { AutoDisposable, EventEmitter } from "@yume-chan/event";
let worker: Worker | undefined; let worker: Worker | undefined;
let workerReady = false; let workerReady = false;
@ -36,28 +36,27 @@ function subscribePictureReady(
} }
export class TinyH264Wrapper extends AutoDisposable { export class TinyH264Wrapper extends AutoDisposable {
public readonly streamId: number; readonly streamId: number;
private readonly pictureReadyEvent = readonly #pictureReadyEvent = new EventEmitter<PictureReadyEventArgs>();
new EventEmitter<PictureReadyEventArgs>(); get onPictureReady() {
public get onPictureReady() { return this.#pictureReadyEvent.event;
return this.pictureReadyEvent.event;
} }
public constructor(streamId: number) { constructor(streamId: number) {
super(); super();
this.streamId = streamId; this.streamId = streamId;
this.addDisposable( this.addDisposable(
subscribePictureReady(streamId, this.handlePictureReady), subscribePictureReady(streamId, this.#handlePictureReady),
); );
} }
private handlePictureReady = (e: PictureReadyEventArgs) => { #handlePictureReady = (e: PictureReadyEventArgs) => {
this.pictureReadyEvent.fire(e); this.#pictureReadyEvent.fire(e);
}; };
public feed(data: ArrayBuffer) { feed(data: ArrayBuffer) {
worker!.postMessage( worker!.postMessage(
{ {
type: "decode", type: "decode",
@ -70,7 +69,7 @@ export class TinyH264Wrapper extends AutoDisposable {
); );
} }
public override dispose() { override dispose() {
super.dispose(); super.dispose();
worker!.postMessage({ worker!.postMessage({
type: "release", type: "release",

View file

@ -27,63 +27,61 @@ function toUint32Le(data: Uint8Array, offset: number) {
} }
export class WebCodecsDecoder implements ScrcpyVideoDecoder { export class WebCodecsDecoder implements ScrcpyVideoDecoder {
public static isSupported() { static isSupported() {
return typeof globalThis.VideoDecoder !== "undefined"; return typeof globalThis.VideoDecoder !== "undefined";
} }
public static readonly capabilities: Record< static readonly capabilities: Record<string, ScrcpyVideoDecoderCapability> =
string, {
ScrcpyVideoDecoderCapability
> = {
h264: {}, h264: {},
h265: {}, h265: {},
}; };
private _codec: ScrcpyVideoCodecId; #codec: ScrcpyVideoCodecId;
public get codec() { get codec() {
return this._codec; return this.#codec;
} }
private _writable: WritableStream<ScrcpyMediaStreamPacket>; #writable: WritableStream<ScrcpyMediaStreamPacket>;
public get writable() { get writable() {
return this._writable; return this.#writable;
} }
private _renderer: HTMLCanvasElement; #renderer: HTMLCanvasElement;
public get renderer() { get renderer() {
return this._renderer; return this.#renderer;
} }
private _frameRendered = 0; #frameRendered = 0;
public get frameRendered() { get frameRendered() {
return this._frameRendered; return this.#frameRendered;
} }
private _frameSkipped = 0; #frameSkipped = 0;
public get frameSkipped() { get frameSkipped() {
return this._frameSkipped; return this.#frameSkipped;
} }
private context: CanvasRenderingContext2D; #context: CanvasRenderingContext2D;
private decoder: VideoDecoder; #decoder: VideoDecoder;
private _config: Uint8Array | undefined; #config: Uint8Array | undefined;
private currentFrameRendered = false; #currentFrameRendered = false;
private animationFrameId = 0; #animationFrameId = 0;
public constructor(codec: ScrcpyVideoCodecId) { constructor(codec: ScrcpyVideoCodecId) {
this._codec = codec; this.#codec = codec;
this._renderer = document.createElement("canvas"); this.#renderer = document.createElement("canvas");
this.context = this._renderer.getContext("2d")!; this.#context = this.#renderer.getContext("2d")!;
this.decoder = new VideoDecoder({ this.#decoder = new VideoDecoder({
output: (frame) => { output: (frame) => {
if (this.currentFrameRendered) { if (this.#currentFrameRendered) {
this._frameSkipped += 1; this.#frameSkipped += 1;
} else { } else {
this.currentFrameRendered = true; this.#currentFrameRendered = true;
this._frameRendered += 1; this.#frameRendered += 1;
} }
// PERF: H.264 renderer may draw multiple frames in one vertical sync interval to minimize latency. // PERF: H.264 renderer may draw multiple frames in one vertical sync interval to minimize latency.
@ -92,7 +90,7 @@ export class WebCodecsDecoder implements ScrcpyVideoDecoder {
// But this ensures users can always see the most up-to-date screen. // But this ensures users can always see the most up-to-date screen.
// This is also the behavior of official Scrcpy client. // This is also the behavior of official Scrcpy client.
// https://github.com/Genymobile/scrcpy/issues/3679 // https://github.com/Genymobile/scrcpy/issues/3679
this.context.drawImage(frame, 0, 0); this.#context.drawImage(frame, 0, 0);
frame.close(); frame.close();
}, },
error(e) { error(e) {
@ -100,29 +98,29 @@ export class WebCodecsDecoder implements ScrcpyVideoDecoder {
}, },
}); });
this._writable = new WritableStream<ScrcpyMediaStreamPacket>({ this.#writable = new WritableStream<ScrcpyMediaStreamPacket>({
write: (packet) => { write: (packet) => {
switch (packet.type) { switch (packet.type) {
case "configuration": case "configuration":
this.configure(packet.data); this.#configure(packet.data);
break; break;
case "data": case "data":
this.decode(packet); this.#decode(packet);
break; break;
} }
}, },
}); });
this.onFramePresented(); this.#onFramePresented();
} }
private onFramePresented = () => { #onFramePresented = () => {
this.currentFrameRendered = false; this.#currentFrameRendered = false;
this.animationFrameId = requestAnimationFrame(this.onFramePresented); this.#animationFrameId = requestAnimationFrame(this.#onFramePresented);
}; };
private configure(data: Uint8Array) { #configure(data: Uint8Array) {
switch (this._codec) { switch (this.#codec) {
case ScrcpyVideoCodecId.H264: { case ScrcpyVideoCodecId.H264: {
const { const {
profileIndex, profileIndex,
@ -132,15 +130,15 @@ export class WebCodecsDecoder implements ScrcpyVideoDecoder {
croppedHeight, croppedHeight,
} = h264ParseConfiguration(data); } = h264ParseConfiguration(data);
this._renderer.width = croppedWidth; this.#renderer.width = croppedWidth;
this._renderer.height = croppedHeight; this.#renderer.height = croppedHeight;
// https://www.rfc-editor.org/rfc/rfc6381#section-3.3 // https://www.rfc-editor.org/rfc/rfc6381#section-3.3
// ISO Base Media File Format Name Space // ISO Base Media File Format Name Space
const codec = `avc1.${[profileIndex, constraintSet, levelIndex] const codec = `avc1.${[profileIndex, constraintSet, levelIndex]
.map(toHex) .map(toHex)
.join("")}`; .join("")}`;
this.decoder.configure({ this.#decoder.configure({
codec: codec, codec: codec,
optimizeForLatency: true, optimizeForLatency: true,
}); });
@ -158,8 +156,8 @@ export class WebCodecsDecoder implements ScrcpyVideoDecoder {
croppedHeight, croppedHeight,
} = h265ParseConfiguration(data); } = h265ParseConfiguration(data);
this._renderer.width = croppedWidth; this.#renderer.width = croppedWidth;
this._renderer.height = croppedHeight; this.#renderer.height = croppedHeight;
const codec = [ const codec = [
"hev1", "hev1",
@ -175,36 +173,36 @@ export class WebCodecsDecoder implements ScrcpyVideoDecoder {
.toString(16) .toString(16)
.toUpperCase(), .toUpperCase(),
].join("."); ].join(".");
this.decoder.configure({ this.#decoder.configure({
codec, codec,
optimizeForLatency: true, optimizeForLatency: true,
}); });
break; break;
} }
} }
this._config = data; this.#config = data;
} }
private decode(packet: ScrcpyMediaStreamDataPacket) { #decode(packet: ScrcpyMediaStreamDataPacket) {
if (this.decoder.state !== "configured") { if (this.#decoder.state !== "configured") {
return; return;
} }
// WebCodecs requires configuration data to be with the first frame. // WebCodecs requires configuration data to be with the first frame.
// https://www.w3.org/TR/webcodecs-avc-codec-registration/#encodedvideochunk-type // https://www.w3.org/TR/webcodecs-avc-codec-registration/#encodedvideochunk-type
let data: Uint8Array; let data: Uint8Array;
if (this._config !== undefined) { if (this.#config !== undefined) {
data = new Uint8Array( data = new Uint8Array(
this._config.byteLength + packet.data.byteLength, this.#config.byteLength + packet.data.byteLength,
); );
data.set(this._config, 0); data.set(this.#config, 0);
data.set(packet.data, this._config.byteLength); data.set(packet.data, this.#config.byteLength);
this._config = undefined; this.#config = undefined;
} else { } else {
data = packet.data; data = packet.data;
} }
this.decoder.decode( this.#decoder.decode(
new EncodedVideoChunk({ new EncodedVideoChunk({
// Treat `undefined` as `key`, otherwise won't decode. // Treat `undefined` as `key`, otherwise won't decode.
type: packet.keyframe === false ? "delta" : "key", type: packet.keyframe === false ? "delta" : "key",
@ -214,10 +212,10 @@ export class WebCodecsDecoder implements ScrcpyVideoDecoder {
); );
} }
public dispose() { dispose() {
cancelAnimationFrame(this.animationFrameId); cancelAnimationFrame(this.#animationFrameId);
if (this.decoder.state !== "closed") { if (this.#decoder.state !== "closed") {
this.decoder.close(); this.#decoder.close();
} }
} }
} }

View file

@ -231,96 +231,93 @@ export function naluRemoveEmulation(buffer: Uint8Array) {
} }
export class NaluSodbBitReader { export class NaluSodbBitReader {
private readonly _nalu: Uint8Array; readonly #nalu: Uint8Array;
private readonly _byteLength: number; readonly #byteLength: number;
private readonly _stopBitIndex: number; readonly #stopBitIndex: number;
private _zeroCount = 0; #zeroCount = 0;
private _bytePosition = -1; #bytePosition = -1;
private _bitPosition = -1; #bitPosition = -1;
private _byte = 0; #byte = 0;
public get byteLength() { get byteLength() {
return this._byteLength; return this.#byteLength;
} }
public get stopBitIndex() { get stopBitIndex() {
return this._stopBitIndex; return this.#stopBitIndex;
} }
public get bytePosition() { get bytePosition() {
return this._bytePosition; return this.#bytePosition;
} }
public get bitPosition() { get bitPosition() {
return this._bitPosition; return this.#bitPosition;
} }
public get ended() { get ended() {
return ( return (
this._bytePosition === this._byteLength && this.#bytePosition === this.#byteLength &&
this._bitPosition === this._stopBitIndex this.#bitPosition === this.#stopBitIndex
); );
} }
public constructor(nalu: Uint8Array) { constructor(nalu: Uint8Array) {
this._nalu = nalu; this.#nalu = nalu;
for (let i = nalu.length - 1; i >= 0; i -= 1) { for (let i = nalu.length - 1; i >= 0; i -= 1) {
if (this._nalu[i] === 0) { if (this.#nalu[i] === 0) {
continue; continue;
} }
const byte = nalu[i]!; const byte = nalu[i]!;
for (let j = 0; j < 8; j += 1) { for (let j = 0; j < 8; j += 1) {
if (((byte >> j) & 1) === 1) { if (((byte >> j) & 1) === 1) {
this._byteLength = i; this.#byteLength = i;
this._stopBitIndex = j; this.#stopBitIndex = j;
this.readByte(); this.#readByte();
return; return;
} }
} }
} }
throw new Error("End bit not found"); throw new Error("Stop bit not found");
} }
private readByte() { #readByte() {
this._byte = this._nalu[this._bytePosition]!; this.#byte = this.#nalu[this.#bytePosition]!;
if (this._zeroCount === 2 && this._byte === 3) { if (this.#zeroCount === 2 && this.#byte === 3) {
this._zeroCount = 0; this.#zeroCount = 0;
this._bytePosition += 1; this.#bytePosition += 1;
this.readByte(); this.#readByte();
return; return;
} }
if (this._byte === 0) { if (this.#byte === 0) {
this._zeroCount += 1; this.#zeroCount += 1;
} else { } else {
this._zeroCount = 0; this.#zeroCount = 0;
} }
} }
public next() { next() {
if (this._bitPosition === -1) { if (this.#bitPosition === -1) {
this._bitPosition = 7; this.#bitPosition = 7;
this._bytePosition += 1; this.#bytePosition += 1;
this.readByte(); this.#readByte();
} }
if ( if (this.ended) {
this._bytePosition === this._byteLength &&
this._bitPosition === this._stopBitIndex
) {
throw new Error("Bit index out of bounds"); throw new Error("Bit index out of bounds");
} }
const value = (this._byte >> this._bitPosition) & 1; const value = (this.#byte >> this.#bitPosition) & 1;
this._bitPosition -= 1; this.#bitPosition -= 1;
return value; return value;
} }
public read(length: number): number { read(length: number): number {
if (length > 32) { if (length > 32) {
throw new Error("Read length too large"); throw new Error("Read length too large");
} }
@ -332,13 +329,13 @@ export class NaluSodbBitReader {
return result; return result;
} }
public skip(length: number) { skip(length: number) {
for (let i = 0; i < length; i += 1) { for (let i = 0; i < length; i += 1) {
this.next(); this.next();
} }
} }
public decodeExponentialGolombNumber(): number { decodeExponentialGolombNumber(): number {
let length = 0; let length = 0;
while (this.next() === 0) { while (this.next() === 0) {
length += 1; length += 1;
@ -349,14 +346,35 @@ export class NaluSodbBitReader {
return ((1 << length) | this.read(length)) - 1; return ((1 << length) | this.read(length)) - 1;
} }
public peek(length: number) { #save() {
const { _zeroCount, _bytePosition, _bitPosition, _byte } = this; return {
zeroCount: this.#zeroCount,
bytePosition: this.#bytePosition,
bitPosition: this.#bitPosition,
byte: this.#byte,
};
}
#restore(state: {
zeroCount: number;
bytePosition: number;
bitPosition: number;
byte: number;
}) {
this.#zeroCount = state.zeroCount;
this.#bytePosition = state.bytePosition;
this.#bitPosition = state.bitPosition;
this.#byte = state.byte;
}
peek(length: number) {
const state = this.#save();
const result = this.read(length); const result = this.read(length);
Object.assign(this, { _zeroCount, _bytePosition, _bitPosition, _byte }); this.#restore(state);
return result; return result;
} }
public readBytes(length: number): Uint8Array { readBytes(length: number): Uint8Array {
const result = new Uint8Array(length); const result = new Uint8Array(length);
for (let i = 0; i < length; i += 1) { for (let i = 0; i < length; i += 1) {
result[i] = this.read(8); result[i] = this.read(8);
@ -364,10 +382,10 @@ export class NaluSodbBitReader {
return result; return result;
} }
public peekBytes(length: number): Uint8Array { peekBytes(length: number): Uint8Array {
const { _zeroCount, _bytePosition, _bitPosition, _byte } = this; const state = this.#save();
const result = this.readBytes(length); const result = this.readBytes(length);
Object.assign(this, { _zeroCount, _bytePosition, _bitPosition, _byte }); this.#restore(state);
return result; return result;
} }
} }

View file

@ -15,12 +15,11 @@ import { ScrcpyControlMessageType } from "./type.js";
* so Scrcpy server can remove the previously hovering pointer. * so Scrcpy server can remove the previously hovering pointer.
*/ */
export class ScrcpyHoverHelper { export class ScrcpyHoverHelper {
// AFAIK, only mouse and pen can have hover state // There can be only one hovering pointer (either mouse or pen,
// and you can't have two mouses or pens. // touch can have multiple pointers but no hovering state).
// So remember the last hovering pointer is enough. #lastHoverMessage: ScrcpyInjectTouchControlMessage | undefined;
private lastHoverMessage: ScrcpyInjectTouchControlMessage | undefined;
public process( process(
message: Omit<ScrcpyInjectTouchControlMessage, "type">, message: Omit<ScrcpyInjectTouchControlMessage, "type">,
): ScrcpyInjectTouchControlMessage[] { ): ScrcpyInjectTouchControlMessage[] {
const result: ScrcpyInjectTouchControlMessage[] = []; const result: ScrcpyInjectTouchControlMessage[] = [];
@ -28,21 +27,21 @@ export class ScrcpyHoverHelper {
// A different pointer appeared, // A different pointer appeared,
// Cancel previously hovering pointer so Scrcpy server can free up the pointer ID. // Cancel previously hovering pointer so Scrcpy server can free up the pointer ID.
if ( if (
this.lastHoverMessage && this.#lastHoverMessage &&
this.lastHoverMessage.pointerId !== message.pointerId this.#lastHoverMessage.pointerId !== message.pointerId
) { ) {
// TODO: Inject MotionEvent.ACTION_HOVER_EXIT // TODO: Inject MotionEvent.ACTION_HOVER_EXIT
// From testing, it seems no App cares about this event. // From testing, it seems no App cares about this event.
result.push({ result.push({
...this.lastHoverMessage, ...this.#lastHoverMessage,
action: AndroidMotionEventAction.Up, action: AndroidMotionEventAction.Up,
}); });
this.lastHoverMessage = undefined; this.#lastHoverMessage = undefined;
} }
if (message.action === AndroidMotionEventAction.HoverMove) { if (message.action === AndroidMotionEventAction.HoverMove) {
// TODO: Inject MotionEvent.ACTION_HOVER_ENTER // TODO: Inject MotionEvent.ACTION_HOVER_ENTER
this.lastHoverMessage = message as ScrcpyInjectTouchControlMessage; this.#lastHoverMessage = message as ScrcpyInjectTouchControlMessage;
} }
(message as ScrcpyInjectTouchControlMessage).type = (message as ScrcpyInjectTouchControlMessage).type =

View file

@ -17,40 +17,38 @@ import {
} from "./type.js"; } from "./type.js";
export class ScrcpyControlMessageSerializer { export class ScrcpyControlMessageSerializer {
private _options: ScrcpyOptions<object>; #options: ScrcpyOptions<object>;
private _typeValues: ScrcpyControlMessageTypeValue; #typeValues: ScrcpyControlMessageTypeValue;
private _scrollController: ScrcpyScrollController; #scrollController: ScrcpyScrollController;
public constructor(options: ScrcpyOptions<object>) { constructor(options: ScrcpyOptions<object>) {
this._options = options; this.#options = options;
this._typeValues = new ScrcpyControlMessageTypeValue(options); this.#typeValues = new ScrcpyControlMessageTypeValue(options);
this._scrollController = options.createScrollController(); this.#scrollController = options.createScrollController();
} }
public injectKeyCode( injectKeyCode(message: Omit<ScrcpyInjectKeyCodeControlMessage, "type">) {
message: Omit<ScrcpyInjectKeyCodeControlMessage, "type">,
) {
return ScrcpyInjectKeyCodeControlMessage.serialize( return ScrcpyInjectKeyCodeControlMessage.serialize(
this._typeValues.fillMessageType( this.#typeValues.fillMessageType(
message, message,
ScrcpyControlMessageType.InjectKeyCode, ScrcpyControlMessageType.InjectKeyCode,
), ),
); );
} }
public injectText(text: string) { injectText(text: string) {
return ScrcpyInjectTextControlMessage.serialize({ return ScrcpyInjectTextControlMessage.serialize({
text, text,
type: this._typeValues.get(ScrcpyControlMessageType.InjectText), type: this.#typeValues.get(ScrcpyControlMessageType.InjectText),
}); });
} }
/** /**
* `pressure` is a float value between 0 and 1. * `pressure` is a float value between 0 and 1.
*/ */
public injectTouch(message: Omit<ScrcpyInjectTouchControlMessage, "type">) { injectTouch(message: Omit<ScrcpyInjectTouchControlMessage, "type">) {
return this._options.serializeInjectTouchControlMessage( return this.#options.serializeInjectTouchControlMessage(
this._typeValues.fillMessageType( this.#typeValues.fillMessageType(
message, message,
ScrcpyControlMessageType.InjectTouch, ScrcpyControlMessageType.InjectTouch,
), ),
@ -60,36 +58,34 @@ export class ScrcpyControlMessageSerializer {
/** /**
* `scrollX` and `scrollY` are float values between 0 and 1. * `scrollX` and `scrollY` are float values between 0 and 1.
*/ */
public injectScroll( injectScroll(message: Omit<ScrcpyInjectScrollControlMessage, "type">) {
message: Omit<ScrcpyInjectScrollControlMessage, "type">, return this.#scrollController.serializeScrollMessage(
) { this.#typeValues.fillMessageType(
return this._scrollController.serializeScrollMessage(
this._typeValues.fillMessageType(
message, message,
ScrcpyControlMessageType.InjectScroll, ScrcpyControlMessageType.InjectScroll,
), ),
); );
} }
public backOrScreenOn(action: AndroidKeyEventAction) { backOrScreenOn(action: AndroidKeyEventAction) {
return this._options.serializeBackOrScreenOnControlMessage({ return this.#options.serializeBackOrScreenOnControlMessage({
action, action,
type: this._typeValues.get(ScrcpyControlMessageType.BackOrScreenOn), type: this.#typeValues.get(ScrcpyControlMessageType.BackOrScreenOn),
}); });
} }
public setScreenPowerMode(mode: AndroidScreenPowerMode) { setScreenPowerMode(mode: AndroidScreenPowerMode) {
return ScrcpySetScreenPowerModeControlMessage.serialize({ return ScrcpySetScreenPowerModeControlMessage.serialize({
mode, mode,
type: this._typeValues.get( type: this.#typeValues.get(
ScrcpyControlMessageType.SetScreenPowerMode, ScrcpyControlMessageType.SetScreenPowerMode,
), ),
}); });
} }
public rotateDevice() { rotateDevice() {
return ScrcpyRotateDeviceControlMessage.serialize({ return ScrcpyRotateDeviceControlMessage.serialize({
type: this._typeValues.get(ScrcpyControlMessageType.RotateDevice), type: this.#typeValues.get(ScrcpyControlMessageType.RotateDevice),
}); });
} }
} }

View file

@ -25,21 +25,21 @@ export enum ScrcpyControlMessageType {
* This class provides a way to get the actual value for a given type. * This class provides a way to get the actual value for a given type.
*/ */
export class ScrcpyControlMessageTypeValue { export class ScrcpyControlMessageTypeValue {
private types: readonly ScrcpyControlMessageType[]; #types: readonly ScrcpyControlMessageType[];
public constructor(options: ScrcpyOptions<object>) { constructor(options: ScrcpyOptions<object>) {
this.types = options.controlMessageTypes; this.#types = options.controlMessageTypes;
} }
public get(type: ScrcpyControlMessageType): number { get(type: ScrcpyControlMessageType): number {
const value = this.types.indexOf(type); const value = this.#types.indexOf(type);
if (value === -1) { if (value === -1) {
throw new Error("Not supported"); throw new Error("Not supported");
} }
return value; return value;
} }
public fillMessageType<T extends { type: ScrcpyControlMessageType }>( fillMessageType<T extends { type: ScrcpyControlMessageType }>(
message: Omit<T, "type">, message: Omit<T, "type">,
type: T["type"], type: T["type"],
): T { ): T {

View file

@ -16,72 +16,70 @@ import { ScrcpyControlMessageSerializer } from "./serializer.js";
import type { AndroidScreenPowerMode } from "./set-screen-power-mode.js"; import type { AndroidScreenPowerMode } from "./set-screen-power-mode.js";
export class ScrcpyControlMessageWriter { export class ScrcpyControlMessageWriter {
private _writer: WritableStreamDefaultWriter<Consumable<Uint8Array>>; #writer: WritableStreamDefaultWriter<Consumable<Uint8Array>>;
private _serializer: ScrcpyControlMessageSerializer; #serializer: ScrcpyControlMessageSerializer;
public constructor( constructor(
writer: WritableStreamDefaultWriter<Consumable<Uint8Array>>, writer: WritableStreamDefaultWriter<Consumable<Uint8Array>>,
options: ScrcpyOptions<object>, options: ScrcpyOptions<object>,
) { ) {
this._writer = writer; this.#writer = writer;
this._serializer = new ScrcpyControlMessageSerializer(options); this.#serializer = new ScrcpyControlMessageSerializer(options);
} }
private async write(message: Uint8Array) { async #write(message: Uint8Array) {
await ConsumableWritableStream.write(this._writer, message); await ConsumableWritableStream.write(this.#writer, message);
} }
public async injectKeyCode( async injectKeyCode(
message: Omit<ScrcpyInjectKeyCodeControlMessage, "type">, message: Omit<ScrcpyInjectKeyCodeControlMessage, "type">,
) { ) {
await this.write(this._serializer.injectKeyCode(message)); await this.#write(this.#serializer.injectKeyCode(message));
} }
public async injectText(text: string) { async injectText(text: string) {
await this.write(this._serializer.injectText(text)); await this.#write(this.#serializer.injectText(text));
} }
/** /**
* `pressure` is a float value between 0 and 1. * `pressure` is a float value between 0 and 1.
*/ */
public async injectTouch( async injectTouch(message: Omit<ScrcpyInjectTouchControlMessage, "type">) {
message: Omit<ScrcpyInjectTouchControlMessage, "type">, await this.#write(this.#serializer.injectTouch(message));
) {
await this.write(this._serializer.injectTouch(message));
} }
/** /**
* `scrollX` and `scrollY` are float values between 0 and 1. * `scrollX` and `scrollY` are float values between 0 and 1.
*/ */
public async injectScroll( async injectScroll(
message: Omit<ScrcpyInjectScrollControlMessage, "type">, message: Omit<ScrcpyInjectScrollControlMessage, "type">,
) { ) {
const data = this._serializer.injectScroll(message); const data = this.#serializer.injectScroll(message);
if (data) { if (data) {
await this.write(data); await this.#write(data);
} }
} }
public async backOrScreenOn(action: AndroidKeyEventAction) { async backOrScreenOn(action: AndroidKeyEventAction) {
const data = this._serializer.backOrScreenOn(action); const data = this.#serializer.backOrScreenOn(action);
if (data) { if (data) {
await this.write(data); await this.#write(data);
} }
} }
public async setScreenPowerMode(mode: AndroidScreenPowerMode) { async setScreenPowerMode(mode: AndroidScreenPowerMode) {
await this.write(this._serializer.setScreenPowerMode(mode)); await this.#write(this.#serializer.setScreenPowerMode(mode));
} }
public async rotateDevice() { async rotateDevice() {
await this.write(this._serializer.rotateDevice()); await this.#write(this.#serializer.rotateDevice());
} }
public releaseLock() { releaseLock() {
this._writer.releaseLock(); this.#writer.releaseLock();
} }
public async close() { async close() {
await this._writer.close(); await this.#writer.close();
} }
} }

View file

@ -27,13 +27,13 @@ const CODEC_OPTION_TYPES: Partial<
}; };
export class CodecOptions implements ScrcpyOptionValue { export class CodecOptions implements ScrcpyOptionValue {
public value: Partial<CodecOptionsInit>; value: Partial<CodecOptionsInit>;
public constructor(value: Partial<CodecOptionsInit> = {}) { constructor(value: Partial<CodecOptionsInit> = {}) {
this.value = value; this.value = value;
} }
public toOptionValue(): string | undefined { toOptionValue(): string | undefined {
const entries = Object.entries(this.value).filter( const entries = Object.entries(this.value).filter(
([, value]) => value !== undefined, ([, value]) => value !== undefined,
); );

View file

@ -38,7 +38,7 @@ import type { ScrcpyScrollController } from "./scroll.js";
import { ScrcpyScrollController1_16 } from "./scroll.js"; import { ScrcpyScrollController1_16 } from "./scroll.js";
export class ScrcpyOptions1_16 implements ScrcpyOptions<ScrcpyOptionsInit1_16> { export class ScrcpyOptions1_16 implements ScrcpyOptions<ScrcpyOptionsInit1_16> {
public static readonly DEFAULTS = { static readonly DEFAULTS = {
logLevel: ScrcpyLogLevel1_16.Debug, logLevel: ScrcpyLogLevel1_16.Debug,
maxSize: 0, maxSize: 0,
bitRate: 8_000_000, bitRate: 8_000_000,
@ -54,7 +54,7 @@ export class ScrcpyOptions1_16 implements ScrcpyOptions<ScrcpyOptionsInit1_16> {
codecOptions: new CodecOptions(), codecOptions: new CodecOptions(),
} as const satisfies Required<ScrcpyOptionsInit1_16>; } as const satisfies Required<ScrcpyOptionsInit1_16>;
public static readonly SERIALIZE_ORDER = [ static readonly SERIALIZE_ORDER = [
"logLevel", "logLevel",
"maxSize", "maxSize",
"bitRate", "bitRate",
@ -70,11 +70,11 @@ export class ScrcpyOptions1_16 implements ScrcpyOptions<ScrcpyOptionsInit1_16> {
"codecOptions", "codecOptions",
] as const satisfies readonly (keyof ScrcpyOptionsInit1_16)[]; ] as const satisfies readonly (keyof ScrcpyOptionsInit1_16)[];
public static serialize<T>(options: T, order: readonly (keyof T)[]) { static serialize<T>(options: T, order: readonly (keyof T)[]) {
return order.map((key) => toScrcpyOptionValue(options[key], "-")); return order.map((key) => toScrcpyOptionValue(options[key], "-"));
} }
public static async parseCString( static async parseCString(
stream: AsyncExactReadable, stream: AsyncExactReadable,
maxLength: number, maxLength: number,
): Promise<string> { ): Promise<string> {
@ -83,55 +83,51 @@ export class ScrcpyOptions1_16 implements ScrcpyOptions<ScrcpyOptionsInit1_16> {
return result; return result;
} }
public static async parseUint16BE( static async parseUint16BE(stream: AsyncExactReadable): Promise<number> {
stream: AsyncExactReadable,
): Promise<number> {
const buffer = await stream.readExactly(NumberFieldType.Uint16.size); const buffer = await stream.readExactly(NumberFieldType.Uint16.size);
return NumberFieldType.Uint16.deserialize(buffer, false); return NumberFieldType.Uint16.deserialize(buffer, false);
} }
public static async parseUint32BE( static async parseUint32BE(stream: AsyncExactReadable): Promise<number> {
stream: AsyncExactReadable,
): Promise<number> {
const buffer = await stream.readExactly(NumberFieldType.Uint32.size); const buffer = await stream.readExactly(NumberFieldType.Uint32.size);
return NumberFieldType.Uint32.deserialize(buffer, false); return NumberFieldType.Uint32.deserialize(buffer, false);
} }
public value: Required<ScrcpyOptionsInit1_16>; value: Required<ScrcpyOptionsInit1_16>;
public readonly defaults: Required<ScrcpyOptionsInit1_16> = readonly defaults: Required<ScrcpyOptionsInit1_16> =
ScrcpyOptions1_16.DEFAULTS; ScrcpyOptions1_16.DEFAULTS;
public readonly controlMessageTypes: readonly ScrcpyControlMessageType[] = readonly controlMessageTypes: readonly ScrcpyControlMessageType[] =
SCRCPY_CONTROL_MESSAGE_TYPES_1_16; SCRCPY_CONTROL_MESSAGE_TYPES_1_16;
public constructor(init: ScrcpyOptionsInit1_16) { constructor(init: ScrcpyOptionsInit1_16) {
this.value = { ...ScrcpyOptions1_16.DEFAULTS, ...init }; this.value = { ...ScrcpyOptions1_16.DEFAULTS, ...init };
} }
public serialize(): string[] { serialize(): string[] {
return ScrcpyOptions1_16.serialize( return ScrcpyOptions1_16.serialize(
this.value, this.value,
ScrcpyOptions1_16.SERIALIZE_ORDER, ScrcpyOptions1_16.SERIALIZE_ORDER,
); );
} }
public setListEncoders(): void { setListEncoders(): void {
throw new Error("Not supported"); throw new Error("Not supported");
} }
public setListDisplays(): void { setListDisplays(): void {
// Set to an invalid value // Set to an invalid value
// Server will print valid values before crashing // Server will print valid values before crashing
// (server will crash before opening sockets) // (server will crash before opening sockets)
this.value.displayId = -1; this.value.displayId = -1;
} }
public parseEncoder(): ScrcpyEncoder | undefined { parseEncoder(): ScrcpyEncoder | undefined {
throw new Error("Not supported"); throw new Error("Not supported");
} }
public parseDisplay(line: string): ScrcpyDisplay | undefined { parseDisplay(line: string): ScrcpyDisplay | undefined {
const displayIdRegex = /\s+scrcpy --display (\d+)/; const displayIdRegex = /\s+scrcpy --display (\d+)/;
const match = line.match(displayIdRegex); const match = line.match(displayIdRegex);
if (match) { if (match) {
@ -142,7 +138,7 @@ export class ScrcpyOptions1_16 implements ScrcpyOptions<ScrcpyOptionsInit1_16> {
return undefined; return undefined;
} }
public parseVideoStreamMetadata( parseVideoStreamMetadata(
stream: ReadableStream<Uint8Array>, stream: ReadableStream<Uint8Array>,
): ValueOrPromise<ScrcpyVideoStream> { ): ValueOrPromise<ScrcpyVideoStream> {
return (async () => { return (async () => {
@ -160,11 +156,11 @@ export class ScrcpyOptions1_16 implements ScrcpyOptions<ScrcpyOptionsInit1_16> {
})(); })();
} }
public parseAudioStreamMetadata(): never { parseAudioStreamMetadata(): never {
throw new Error("Not supported"); throw new Error("Not supported");
} }
public createMediaStreamTransformer(): TransformStream< createMediaStreamTransformer(): TransformStream<
Uint8Array, Uint8Array,
ScrcpyMediaStreamPacket ScrcpyMediaStreamPacket
> { > {
@ -207,13 +203,13 @@ export class ScrcpyOptions1_16 implements ScrcpyOptions<ScrcpyOptionsInit1_16> {
}; };
} }
public serializeInjectTouchControlMessage( serializeInjectTouchControlMessage(
message: ScrcpyInjectTouchControlMessage, message: ScrcpyInjectTouchControlMessage,
): Uint8Array { ): Uint8Array {
return ScrcpyInjectTouchControlMessage1_16.serialize(message); return ScrcpyInjectTouchControlMessage1_16.serialize(message);
} }
public serializeBackOrScreenOnControlMessage( serializeBackOrScreenOnControlMessage(
message: ScrcpyBackOrScreenOnControlMessage, message: ScrcpyBackOrScreenOnControlMessage,
) { ) {
if (message.action === AndroidKeyEventAction.Down) { if (message.action === AndroidKeyEventAction.Down) {
@ -223,13 +219,13 @@ export class ScrcpyOptions1_16 implements ScrcpyOptions<ScrcpyOptionsInit1_16> {
return undefined; return undefined;
} }
public serializeSetClipboardControlMessage( serializeSetClipboardControlMessage(
message: ScrcpySetClipboardControlMessage, message: ScrcpySetClipboardControlMessage,
): Uint8Array { ): Uint8Array {
return ScrcpySetClipboardControlMessage1_15.serialize(message); return ScrcpySetClipboardControlMessage1_15.serialize(message);
} }
public createScrollController(): ScrcpyScrollController { createScrollController(): ScrcpyScrollController {
return new ScrcpyScrollController1_16(); return new ScrcpyScrollController1_16();
} }
} }

View file

@ -24,31 +24,31 @@ export const ScrcpyInjectScrollControlMessage1_16 = new Struct()
* reaches 1 or -1. * reaches 1 or -1.
*/ */
export class ScrcpyScrollController1_16 implements ScrcpyScrollController { export class ScrcpyScrollController1_16 implements ScrcpyScrollController {
private accumulatedX = 0; #accumulatedX = 0;
private accumulatedY = 0; #accumulatedY = 0;
protected processMessage( protected processMessage(
message: ScrcpyInjectScrollControlMessage, message: ScrcpyInjectScrollControlMessage,
): ScrcpyInjectScrollControlMessage | undefined { ): ScrcpyInjectScrollControlMessage | undefined {
this.accumulatedX += message.scrollX; this.#accumulatedX += message.scrollX;
this.accumulatedY += message.scrollY; this.#accumulatedY += message.scrollY;
let scrollX = 0; let scrollX = 0;
let scrollY = 0; let scrollY = 0;
if (this.accumulatedX >= 1) { if (this.#accumulatedX >= 1) {
scrollX = 1; scrollX = 1;
this.accumulatedX = 0; this.#accumulatedX = 0;
} else if (this.accumulatedX <= -1) { } else if (this.#accumulatedX <= -1) {
scrollX = -1; scrollX = -1;
this.accumulatedX = 0; this.#accumulatedX = 0;
} }
if (this.accumulatedY >= 1) { if (this.#accumulatedY >= 1) {
scrollY = 1; scrollY = 1;
this.accumulatedY = 0; this.#accumulatedY = 0;
} else if (this.accumulatedY <= -1) { } else if (this.#accumulatedY <= -1) {
scrollY = -1; scrollY = -1;
this.accumulatedY = 0; this.#accumulatedY = 0;
} }
if (scrollX === 0 && scrollY === 0) { if (scrollX === 0 && scrollY === 0) {
@ -60,7 +60,7 @@ export class ScrcpyScrollController1_16 implements ScrcpyScrollController {
return message; return message;
} }
public serializeScrollMessage( serializeScrollMessage(
message: ScrcpyInjectScrollControlMessage, message: ScrcpyInjectScrollControlMessage,
): Uint8Array | undefined { ): Uint8Array | undefined {
const processed = this.processMessage(message); const processed = this.processMessage(message);

View file

@ -11,17 +11,17 @@ export class ScrcpyOptions1_17 extends ScrcpyOptionsBase<
ScrcpyOptionsInit1_17, ScrcpyOptionsInit1_17,
ScrcpyOptions1_16 ScrcpyOptions1_16
> { > {
public static readonly DEFAULTS = { static readonly DEFAULTS = {
...ScrcpyOptions1_16.DEFAULTS, ...ScrcpyOptions1_16.DEFAULTS,
encoderName: undefined, encoderName: undefined,
} as const satisfies Required<ScrcpyOptionsInit1_17>; } as const satisfies Required<ScrcpyOptionsInit1_17>;
public static readonly SERIALIZE_ORDER = [ static readonly SERIALIZE_ORDER = [
...ScrcpyOptions1_16.SERIALIZE_ORDER, ...ScrcpyOptions1_16.SERIALIZE_ORDER,
"encoderName", "encoderName",
] as const satisfies readonly (keyof ScrcpyOptionsInit1_17)[]; ] as const satisfies readonly (keyof ScrcpyOptionsInit1_17)[];
public static parseEncoder( static parseEncoder(
line: string, line: string,
encoderNameRegex: RegExp, encoderNameRegex: RegExp,
): ScrcpyEncoder | undefined { ): ScrcpyEncoder | undefined {
@ -32,32 +32,32 @@ export class ScrcpyOptions1_17 extends ScrcpyOptionsBase<
return undefined; return undefined;
} }
public override get defaults(): Required<ScrcpyOptionsInit1_17> { override get defaults(): Required<ScrcpyOptionsInit1_17> {
return ScrcpyOptions1_17.DEFAULTS; return ScrcpyOptions1_17.DEFAULTS;
} }
public constructor(init: ScrcpyOptionsInit1_17) { constructor(init: ScrcpyOptionsInit1_17) {
super(new ScrcpyOptions1_16(init), { super(new ScrcpyOptions1_16(init), {
...ScrcpyOptions1_17.DEFAULTS, ...ScrcpyOptions1_17.DEFAULTS,
...init, ...init,
}); });
} }
public override serialize(): string[] { override serialize(): string[] {
return ScrcpyOptions1_16.serialize( return ScrcpyOptions1_16.serialize(
this.value, this.value,
ScrcpyOptions1_17.SERIALIZE_ORDER, ScrcpyOptions1_17.SERIALIZE_ORDER,
); );
} }
public override setListEncoders() { override setListEncoders() {
// Set to an invalid value // Set to an invalid value
// Server will print valid values before crashing // Server will print valid values before crashing
// (server will crash after opening video and control sockets) // (server will crash after opening video and control sockets)
this.value.encoderName = "_"; this.value.encoderName = "_";
} }
public override parseEncoder(line: string): ScrcpyEncoder | undefined { override parseEncoder(line: string): ScrcpyEncoder | undefined {
return ScrcpyOptions1_17.parseEncoder( return ScrcpyOptions1_17.parseEncoder(
line, line,
/\s+scrcpy --encoder-name '(.*?)'/, /\s+scrcpy --encoder-name '(.*?)'/,

View file

@ -82,23 +82,23 @@ export class ScrcpyOptions1_18 extends ScrcpyOptionsBase<
ScrcpyOptionsInit1_18, ScrcpyOptionsInit1_18,
ScrcpyOptions1_17 ScrcpyOptions1_17
> { > {
public static readonly DEFAULTS = { static readonly DEFAULTS = {
...ScrcpyOptions1_17.DEFAULTS, ...ScrcpyOptions1_17.DEFAULTS,
logLevel: ScrcpyLogLevel1_18.Debug, logLevel: ScrcpyLogLevel1_18.Debug,
lockVideoOrientation: ScrcpyVideoOrientation1_18.Unlocked, lockVideoOrientation: ScrcpyVideoOrientation1_18.Unlocked,
powerOffOnClose: false, powerOffOnClose: false,
} as const satisfies Required<ScrcpyOptionsInit1_18>; } as const satisfies Required<ScrcpyOptionsInit1_18>;
public static readonly SERIALIZE_ORDER = [ static readonly SERIALIZE_ORDER = [
...ScrcpyOptions1_17.SERIALIZE_ORDER, ...ScrcpyOptions1_17.SERIALIZE_ORDER,
"powerOffOnClose", "powerOffOnClose",
] as const satisfies readonly (keyof ScrcpyOptionsInit1_18)[]; ] as const satisfies readonly (keyof ScrcpyOptionsInit1_18)[];
public override get defaults(): Required<ScrcpyOptionsInit1_18> { override get defaults(): Required<ScrcpyOptionsInit1_18> {
return ScrcpyOptions1_18.DEFAULTS; return ScrcpyOptions1_18.DEFAULTS;
} }
public override get controlMessageTypes() { override get controlMessageTypes() {
return SCRCPY_CONTROL_MESSAGE_TYPES_1_18; return SCRCPY_CONTROL_MESSAGE_TYPES_1_18;
} }
@ -115,21 +115,21 @@ export class ScrcpyOptions1_18 extends ScrcpyOptionsBase<
); );
} }
public override serialize(): string[] { override serialize(): string[] {
return ScrcpyOptions1_16.serialize( return ScrcpyOptions1_16.serialize(
this.value, this.value,
ScrcpyOptions1_18.SERIALIZE_ORDER, ScrcpyOptions1_18.SERIALIZE_ORDER,
); );
} }
public override parseEncoder(line: string): ScrcpyEncoder | undefined { override parseEncoder(line: string): ScrcpyEncoder | undefined {
return ScrcpyOptions1_17.parseEncoder( return ScrcpyOptions1_17.parseEncoder(
line, line,
/\s+scrcpy --encoder '(.*?)'/, /\s+scrcpy --encoder '(.*?)'/,
); );
} }
public override serializeBackOrScreenOnControlMessage( override serializeBackOrScreenOnControlMessage(
message: ScrcpyBackOrScreenOnControlMessage, message: ScrcpyBackOrScreenOnControlMessage,
) { ) {
return ScrcpyBackOrScreenOnControlMessage1_18.serialize(message); return ScrcpyBackOrScreenOnControlMessage1_18.serialize(message);

View file

@ -30,12 +30,12 @@ export class ScrcpyOptions1_21 extends ScrcpyOptionsBase<
ScrcpyOptionsInit1_21, ScrcpyOptionsInit1_21,
ScrcpyOptions1_18 ScrcpyOptions1_18
> { > {
public static readonly DEFAULTS = { static readonly DEFAULTS = {
...ScrcpyOptions1_18.DEFAULTS, ...ScrcpyOptions1_18.DEFAULTS,
clipboardAutosync: true, clipboardAutosync: true,
} as const satisfies Required<ScrcpyOptionsInit1_21>; } as const satisfies Required<ScrcpyOptionsInit1_21>;
public static serialize<T extends object>( static serialize<T extends object>(
options: T, options: T,
defaults: Required<T>, defaults: Required<T>,
): string[] { ): string[] {
@ -60,22 +60,22 @@ export class ScrcpyOptions1_21 extends ScrcpyOptionsBase<
return result; return result;
} }
public override get defaults(): Required<ScrcpyOptionsInit1_21> { override get defaults(): Required<ScrcpyOptionsInit1_21> {
return ScrcpyOptions1_21.DEFAULTS; return ScrcpyOptions1_21.DEFAULTS;
} }
public constructor(init: ScrcpyOptionsInit1_21) { constructor(init: ScrcpyOptionsInit1_21) {
super(new ScrcpyOptions1_18(init), { super(new ScrcpyOptions1_18(init), {
...ScrcpyOptions1_21.DEFAULTS, ...ScrcpyOptions1_21.DEFAULTS,
...init, ...init,
}); });
} }
public override serialize(): string[] { override serialize(): string[] {
return ScrcpyOptions1_21.serialize(this.value, this.defaults); return ScrcpyOptions1_21.serialize(this.value, this.defaults);
} }
public override serializeSetClipboardControlMessage( override serializeSetClipboardControlMessage(
message: ScrcpySetClipboardControlMessage, message: ScrcpySetClipboardControlMessage,
): Uint8Array { ): Uint8Array {
return ScrcpySetClipboardControlMessage1_21.serialize(message); return ScrcpySetClipboardControlMessage1_21.serialize(message);

View file

@ -14,25 +14,25 @@ export class ScrcpyOptions1_22 extends ScrcpyOptionsBase<
ScrcpyOptionsInit1_22, ScrcpyOptionsInit1_22,
ScrcpyOptions1_21 ScrcpyOptions1_21
> { > {
public static readonly DEFAULTS = { static readonly DEFAULTS = {
...ScrcpyOptions1_21.DEFAULTS, ...ScrcpyOptions1_21.DEFAULTS,
downsizeOnError: true, downsizeOnError: true,
sendDeviceMeta: true, sendDeviceMeta: true,
sendDummyByte: true, sendDummyByte: true,
} as const satisfies Required<ScrcpyOptionsInit1_22>; } as const satisfies Required<ScrcpyOptionsInit1_22>;
public override get defaults(): Required<ScrcpyOptionsInit1_22> { override get defaults(): Required<ScrcpyOptionsInit1_22> {
return ScrcpyOptions1_22.DEFAULTS; return ScrcpyOptions1_22.DEFAULTS;
} }
public constructor(init: ScrcpyOptionsInit1_22) { constructor(init: ScrcpyOptionsInit1_22) {
super(new ScrcpyOptions1_21(init), { super(new ScrcpyOptions1_21(init), {
...ScrcpyOptions1_22.DEFAULTS, ...ScrcpyOptions1_22.DEFAULTS,
...init, ...init,
}); });
} }
public override parseVideoStreamMetadata( override parseVideoStreamMetadata(
stream: ReadableStream<Uint8Array>, stream: ReadableStream<Uint8Array>,
): ValueOrPromise<ScrcpyVideoStream> { ): ValueOrPromise<ScrcpyVideoStream> {
if (!this.value.sendDeviceMeta) { if (!this.value.sendDeviceMeta) {
@ -42,11 +42,11 @@ export class ScrcpyOptions1_22 extends ScrcpyOptionsBase<
} }
} }
public override serialize(): string[] { override serialize(): string[] {
return ScrcpyOptions1_21.serialize(this.value, this.defaults); return ScrcpyOptions1_21.serialize(this.value, this.defaults);
} }
public override createScrollController(): ScrcpyScrollController { override createScrollController(): ScrcpyScrollController {
return new ScrcpyScrollController1_22(); return new ScrcpyScrollController1_22();
} }
} }

View file

@ -13,7 +13,7 @@ export type ScrcpyInjectScrollControlMessage1_22 =
(typeof ScrcpyInjectScrollControlMessage1_22)["TInit"]; (typeof ScrcpyInjectScrollControlMessage1_22)["TInit"];
export class ScrcpyScrollController1_22 extends ScrcpyScrollController1_16 { export class ScrcpyScrollController1_22 extends ScrcpyScrollController1_16 {
public override serializeScrollMessage( override serializeScrollMessage(
message: ScrcpyInjectScrollControlMessage1_22, message: ScrcpyInjectScrollControlMessage1_22,
): Uint8Array | undefined { ): Uint8Array | undefined {
const processed = this.processMessage(message); const processed = this.processMessage(message);

View file

@ -16,27 +16,27 @@ export class ScrcpyOptions1_23 extends ScrcpyOptionsBase<
ScrcpyOptionsInit1_23, ScrcpyOptionsInit1_23,
ScrcpyOptions1_22 ScrcpyOptions1_22
> { > {
public static readonly DEFAULTS = { static readonly DEFAULTS = {
...ScrcpyOptions1_22.DEFAULTS, ...ScrcpyOptions1_22.DEFAULTS,
cleanup: true, cleanup: true,
} as const satisfies Required<ScrcpyOptionsInit1_23>; } as const satisfies Required<ScrcpyOptionsInit1_23>;
public override get defaults(): Required<ScrcpyOptionsInit1_23> { override get defaults(): Required<ScrcpyOptionsInit1_23> {
return ScrcpyOptions1_23.DEFAULTS; return ScrcpyOptions1_23.DEFAULTS;
} }
public constructor(init: ScrcpyOptionsInit1_22) { constructor(init: ScrcpyOptionsInit1_22) {
super(new ScrcpyOptions1_22(init), { super(new ScrcpyOptions1_22(init), {
...ScrcpyOptions1_23.DEFAULTS, ...ScrcpyOptions1_23.DEFAULTS,
...init, ...init,
}); });
} }
public override serialize(): string[] { override serialize(): string[] {
return ScrcpyOptions1_21.serialize(this.value, this.defaults); return ScrcpyOptions1_21.serialize(this.value, this.defaults);
} }
public override createMediaStreamTransformer(): TransformStream< override createMediaStreamTransformer(): TransformStream<
Uint8Array, Uint8Array,
ScrcpyMediaStreamPacket ScrcpyMediaStreamPacket
> { > {

View file

@ -11,23 +11,23 @@ export class ScrcpyOptions1_24 extends ScrcpyOptionsBase<
ScrcpyOptionsInit1_24, ScrcpyOptionsInit1_24,
ScrcpyOptions1_23 ScrcpyOptions1_23
> { > {
public static readonly DEFAULTS = { static readonly DEFAULTS = {
...ScrcpyOptions1_23.DEFAULTS, ...ScrcpyOptions1_23.DEFAULTS,
powerOn: true, powerOn: true,
} as const satisfies Required<ScrcpyOptionsInit1_24>; } as const satisfies Required<ScrcpyOptionsInit1_24>;
public override get defaults(): Required<ScrcpyOptionsInit1_24> { override get defaults(): Required<ScrcpyOptionsInit1_24> {
return ScrcpyOptions1_24.DEFAULTS; return ScrcpyOptions1_24.DEFAULTS;
} }
public constructor(init: ScrcpyOptionsInit1_24) { constructor(init: ScrcpyOptionsInit1_24) {
super(new ScrcpyOptions1_23(init), { super(new ScrcpyOptions1_23(init), {
...ScrcpyOptions1_24.DEFAULTS, ...ScrcpyOptions1_24.DEFAULTS,
...init, ...init,
}); });
} }
public override serialize(): string[] { override serialize(): string[] {
return ScrcpyOptions1_21.serialize(this.value, this.defaults); return ScrcpyOptions1_21.serialize(this.value, this.defaults);
} }
} }

View file

@ -10,22 +10,22 @@ export class ScrcpyOptions1_25 extends ScrcpyOptionsBase<
ScrcpyOptionsInit1_24, ScrcpyOptionsInit1_24,
ScrcpyOptions1_24 ScrcpyOptions1_24
> { > {
public override get defaults(): Required<ScrcpyOptionsInit1_24> { override get defaults(): Required<ScrcpyOptionsInit1_24> {
return ScrcpyOptions1_24.DEFAULTS; return ScrcpyOptions1_24.DEFAULTS;
} }
public constructor(init: ScrcpyOptionsInit1_24) { constructor(init: ScrcpyOptionsInit1_24) {
super(new ScrcpyOptions1_24(init), { super(new ScrcpyOptions1_24(init), {
...ScrcpyOptions1_24.DEFAULTS, ...ScrcpyOptions1_24.DEFAULTS,
...init, ...init,
}); });
} }
public override serialize(): string[] { override serialize(): string[] {
return ScrcpyOptions1_21.serialize(this.value, this.defaults); return ScrcpyOptions1_21.serialize(this.value, this.defaults);
} }
public override createScrollController(): ScrcpyScrollController { override createScrollController(): ScrcpyScrollController {
return new ScrcpyScrollController1_25(); return new ScrcpyScrollController1_25();
} }
} }

View file

@ -45,20 +45,20 @@ export type ScrcpyInjectTouchControlMessage2_0 =
(typeof ScrcpyInjectTouchControlMessage2_0)["TInit"]; (typeof ScrcpyInjectTouchControlMessage2_0)["TInit"];
export class ScrcpyInstanceId implements ScrcpyOptionValue { export class ScrcpyInstanceId implements ScrcpyOptionValue {
public static readonly NONE = new ScrcpyInstanceId(-1); static readonly NONE = new ScrcpyInstanceId(-1);
public static random(): ScrcpyInstanceId { static random(): ScrcpyInstanceId {
// A random 31-bit unsigned integer // A random 31-bit unsigned integer
return new ScrcpyInstanceId((Math.random() * 0x80000000) | 0); return new ScrcpyInstanceId((Math.random() * 0x80000000) | 0);
} }
public value: number; value: number;
public constructor(value: number) { constructor(value: number) {
this.value = value; this.value = value;
} }
public toOptionValue(): string | undefined { toOptionValue(): string | undefined {
if (this.value < 0) { if (this.value < 0) {
return undefined; return undefined;
} }
@ -106,7 +106,7 @@ export class ScrcpyOptions2_0 extends ScrcpyOptionsBase<
ScrcpyOptionsInit2_0, ScrcpyOptionsInit2_0,
ScrcpyOptions1_25 ScrcpyOptions1_25
> { > {
public static readonly DEFAULTS = { static readonly DEFAULTS = {
...omit(ScrcpyOptions1_24.DEFAULTS, [ ...omit(ScrcpyOptions1_24.DEFAULTS, [
"bitRate", "bitRate",
"codecOptions", "codecOptions",
@ -130,30 +130,30 @@ export class ScrcpyOptions2_0 extends ScrcpyOptionsBase<
sendCodecMeta: true, sendCodecMeta: true,
} as const satisfies Required<ScrcpyOptionsInit2_0>; } as const satisfies Required<ScrcpyOptionsInit2_0>;
public override get defaults(): Required<ScrcpyOptionsInit2_0> { override get defaults(): Required<ScrcpyOptionsInit2_0> {
return ScrcpyOptions2_0.DEFAULTS; return ScrcpyOptions2_0.DEFAULTS;
} }
public constructor(init: ScrcpyOptionsInit2_0) { constructor(init: ScrcpyOptionsInit2_0) {
super(new ScrcpyOptions1_25(init), { super(new ScrcpyOptions1_25(init), {
...ScrcpyOptions2_0.DEFAULTS, ...ScrcpyOptions2_0.DEFAULTS,
...init, ...init,
}); });
} }
public override serialize(): string[] { override serialize(): string[] {
return ScrcpyOptions1_21.serialize(this.value, this.defaults); return ScrcpyOptions1_21.serialize(this.value, this.defaults);
} }
public override setListEncoders(): void { override setListEncoders(): void {
this.value.listEncoders = true; this.value.listEncoders = true;
} }
public override setListDisplays(): void { override setListDisplays(): void {
this.value.listDisplays = true; this.value.listDisplays = true;
} }
public override parseEncoder(line: string): ScrcpyEncoder | undefined { override parseEncoder(line: string): ScrcpyEncoder | undefined {
let match = line.match( let match = line.match(
/\s+--video-codec=(.*)\s+--video-encoder='(.*)'/, /\s+--video-codec=(.*)\s+--video-encoder='(.*)'/,
); );
@ -177,7 +177,7 @@ export class ScrcpyOptions2_0 extends ScrcpyOptionsBase<
return undefined; return undefined;
} }
public override parseDisplay(line: string): ScrcpyDisplay | undefined { override parseDisplay(line: string): ScrcpyDisplay | undefined {
const match = line.match(/\s+--display=(\d+)\s+\((.*?)\)/); const match = line.match(/\s+--display=(\d+)\s+\((.*?)\)/);
if (match) { if (match) {
const display: ScrcpyDisplay = { const display: ScrcpyDisplay = {
@ -191,7 +191,7 @@ export class ScrcpyOptions2_0 extends ScrcpyOptionsBase<
return undefined; return undefined;
} }
public override parseVideoStreamMetadata( override parseVideoStreamMetadata(
stream: ReadableStream<Uint8Array>, stream: ReadableStream<Uint8Array>,
): ValueOrPromise<ScrcpyVideoStream> { ): ValueOrPromise<ScrcpyVideoStream> {
const { sendDeviceMeta, sendCodecMeta } = this.value; const { sendDeviceMeta, sendCodecMeta } = this.value;
@ -249,7 +249,7 @@ export class ScrcpyOptions2_0 extends ScrcpyOptionsBase<
})(); })();
} }
public override parseAudioStreamMetadata( override parseAudioStreamMetadata(
stream: ReadableStream<Uint8Array>, stream: ReadableStream<Uint8Array>,
): ValueOrPromise<ScrcpyAudioStreamMetadata> { ): ValueOrPromise<ScrcpyAudioStreamMetadata> {
return (async (): Promise<ScrcpyAudioStreamMetadata> => { return (async (): Promise<ScrcpyAudioStreamMetadata> => {
@ -334,7 +334,7 @@ export class ScrcpyOptions2_0 extends ScrcpyOptionsBase<
})(); })();
} }
public override serializeInjectTouchControlMessage( override serializeInjectTouchControlMessage(
message: ScrcpyInjectTouchControlMessage, message: ScrcpyInjectTouchControlMessage,
): Uint8Array { ): Uint8Array {
return ScrcpyInjectTouchControlMessage2_0.serialize(message); return ScrcpyInjectTouchControlMessage2_0.serialize(message);

View file

@ -12,24 +12,24 @@ export class ScrcpyOptions2_1 extends ScrcpyOptionsBase<
ScrcpyOptionsInit2_1, ScrcpyOptionsInit2_1,
ScrcpyOptions2_0 ScrcpyOptions2_0
> { > {
public static readonly DEFAULTS = { static readonly DEFAULTS = {
...ScrcpyOptions2_0.DEFAULTS, ...ScrcpyOptions2_0.DEFAULTS,
video: true, video: true,
audioSource: "output", audioSource: "output",
} as const satisfies Required<ScrcpyOptionsInit2_1>; } as const satisfies Required<ScrcpyOptionsInit2_1>;
public override get defaults(): Required<ScrcpyOptionsInit2_1> { override get defaults(): Required<ScrcpyOptionsInit2_1> {
return ScrcpyOptions2_1.DEFAULTS; return ScrcpyOptions2_1.DEFAULTS;
} }
public constructor(init: ScrcpyOptionsInit2_1) { constructor(init: ScrcpyOptionsInit2_1) {
super(new ScrcpyOptions2_0(init), { super(new ScrcpyOptions2_0(init), {
...ScrcpyOptions2_1.DEFAULTS, ...ScrcpyOptions2_1.DEFAULTS,
...init, ...init,
}); });
} }
public override serialize(): string[] { override serialize(): string[] {
return ScrcpyOptions1_21.serialize(this.value, this.defaults); return ScrcpyOptions1_21.serialize(this.value, this.defaults);
} }
} }

View file

@ -21,31 +21,31 @@ export interface ScrcpyVideoStream {
} }
export class ScrcpyAudioCodec implements ScrcpyOptionValue { export class ScrcpyAudioCodec implements ScrcpyOptionValue {
public static readonly OPUS = new ScrcpyAudioCodec( static readonly OPUS = new ScrcpyAudioCodec(
"opus", "opus",
0x6f_70_75_73, 0x6f_70_75_73,
"audio/opus", "audio/opus",
"opus", "opus",
); );
public static readonly AAC = new ScrcpyAudioCodec( static readonly AAC = new ScrcpyAudioCodec(
"aac", "aac",
0x00_61_61_63, 0x00_61_61_63,
"audio/aac", "audio/aac",
"mp4a.66", "mp4a.66",
); );
public static readonly RAW = new ScrcpyAudioCodec( static readonly RAW = new ScrcpyAudioCodec(
"raw", "raw",
0x00_72_61_77, 0x00_72_61_77,
"audio/raw", "audio/raw",
"1", "1",
); );
public readonly optionValue: string; readonly optionValue: string;
public readonly metadataValue: number; readonly metadataValue: number;
public readonly mimeType: string; readonly mimeType: string;
public readonly webCodecId: string; readonly webCodecId: string;
public constructor( constructor(
optionValue: string, optionValue: string,
metadataValue: number, metadataValue: number,
mimeType: string, mimeType: string,
@ -57,7 +57,7 @@ export class ScrcpyAudioCodec implements ScrcpyOptionValue {
this.webCodecId = webCodecId; this.webCodecId = webCodecId;
} }
public toOptionValue(): string { toOptionValue(): string {
return this.optionValue; return this.optionValue;
} }
} }

View file

@ -130,34 +130,34 @@ export abstract class ScrcpyOptionsBase<
{ {
protected _base: B; protected _base: B;
public abstract get defaults(): Required<T>; abstract get defaults(): Required<T>;
public get controlMessageTypes(): readonly ScrcpyControlMessageType[] { get controlMessageTypes(): readonly ScrcpyControlMessageType[] {
return this._base.controlMessageTypes; return this._base.controlMessageTypes;
} }
public readonly value: Required<T>; readonly value: Required<T>;
public constructor(base: B, value: Required<T>) { constructor(base: B, value: Required<T>) {
this._base = base; this._base = base;
this.value = value; this.value = value;
} }
public abstract serialize(): string[]; abstract serialize(): string[];
public setListEncoders(): void { setListEncoders(): void {
this._base.setListEncoders(); this._base.setListEncoders();
} }
public setListDisplays(): void { setListDisplays(): void {
this._base.setListDisplays(); this._base.setListDisplays();
} }
public parseEncoder(line: string): ScrcpyEncoder | undefined { parseEncoder(line: string): ScrcpyEncoder | undefined {
return this._base.parseEncoder(line); return this._base.parseEncoder(line);
} }
public parseDisplay(line: string): ScrcpyDisplay | undefined { parseDisplay(line: string): ScrcpyDisplay | undefined {
return this._base.parseDisplay(line); return this._base.parseDisplay(line);
} }
@ -169,44 +169,44 @@ export abstract class ScrcpyOptionsBase<
* *
* The returned video stream may be different from the input stream, and should be used for further processing. * The returned video stream may be different from the input stream, and should be used for further processing.
*/ */
public parseVideoStreamMetadata( parseVideoStreamMetadata(
stream: ReadableStream<Uint8Array>, stream: ReadableStream<Uint8Array>,
): ValueOrPromise<ScrcpyVideoStream> { ): ValueOrPromise<ScrcpyVideoStream> {
return this._base.parseVideoStreamMetadata(stream); return this._base.parseVideoStreamMetadata(stream);
} }
public parseAudioStreamMetadata( parseAudioStreamMetadata(
stream: ReadableStream<Uint8Array>, stream: ReadableStream<Uint8Array>,
): ValueOrPromise<ScrcpyAudioStreamMetadata> { ): ValueOrPromise<ScrcpyAudioStreamMetadata> {
return this._base.parseAudioStreamMetadata(stream); return this._base.parseAudioStreamMetadata(stream);
} }
public createMediaStreamTransformer(): TransformStream< createMediaStreamTransformer(): TransformStream<
Uint8Array, Uint8Array,
ScrcpyMediaStreamPacket ScrcpyMediaStreamPacket
> { > {
return this._base.createMediaStreamTransformer(); return this._base.createMediaStreamTransformer();
} }
public serializeInjectTouchControlMessage( serializeInjectTouchControlMessage(
message: ScrcpyInjectTouchControlMessage, message: ScrcpyInjectTouchControlMessage,
): Uint8Array { ): Uint8Array {
return this._base.serializeInjectTouchControlMessage(message); return this._base.serializeInjectTouchControlMessage(message);
} }
public serializeBackOrScreenOnControlMessage( serializeBackOrScreenOnControlMessage(
message: ScrcpyBackOrScreenOnControlMessage, message: ScrcpyBackOrScreenOnControlMessage,
): Uint8Array | undefined { ): Uint8Array | undefined {
return this._base.serializeBackOrScreenOnControlMessage(message); return this._base.serializeBackOrScreenOnControlMessage(message);
} }
public serializeSetClipboardControlMessage( serializeSetClipboardControlMessage(
message: ScrcpySetClipboardControlMessage, message: ScrcpySetClipboardControlMessage,
): Uint8Array { ): Uint8Array {
return this._base.serializeSetClipboardControlMessage(message); return this._base.serializeSetClipboardControlMessage(message);
} }
public createScrollController(): ScrcpyScrollController { createScrollController(): ScrcpyScrollController {
return this._base.createScrollController(); return this._base.createScrollController();
} }
} }

View file

@ -12,12 +12,12 @@ export class BufferedTransformStream<T>
implements ReadableWritablePair<T, Uint8Array> implements ReadableWritablePair<T, Uint8Array>
{ {
#readable: ReadableStream<T>; #readable: ReadableStream<T>;
public get readable() { get readable() {
return this.#readable; return this.#readable;
} }
#writable: WritableStream<Uint8Array>; #writable: WritableStream<Uint8Array>;
public get writable() { get writable() {
return this.#writable; return this.#writable;
} }

View file

@ -14,19 +14,19 @@ export class BufferedReadableStream implements AsyncExactReadable {
#bufferedLength = 0; #bufferedLength = 0;
#position = 0; #position = 0;
public get position() { get position() {
return this.#position; return this.#position;
} }
protected readonly stream: ReadableStream<Uint8Array>; protected readonly stream: ReadableStream<Uint8Array>;
protected readonly reader: ReadableStreamDefaultReader<Uint8Array>; protected readonly reader: ReadableStreamDefaultReader<Uint8Array>;
public constructor(stream: ReadableStream<Uint8Array>) { constructor(stream: ReadableStream<Uint8Array>) {
this.stream = stream; this.stream = stream;
this.reader = stream.getReader(); this.reader = stream.getReader();
} }
private async readSource() { async #readSource() {
const { done, value } = await this.reader.read(); const { done, value } = await this.reader.read();
if (done) { if (done) {
throw new ExactReadableEndedError(); throw new ExactReadableEndedError();
@ -35,7 +35,7 @@ export class BufferedReadableStream implements AsyncExactReadable {
return value; return value;
} }
private async readAsync(length: number, initial?: Uint8Array) { async #readAsync(length: number, initial?: Uint8Array) {
let result: Uint8Array; let result: Uint8Array;
let index: number; let index: number;
@ -45,7 +45,7 @@ export class BufferedReadableStream implements AsyncExactReadable {
index = initial.byteLength; index = initial.byteLength;
length -= initial.byteLength; length -= initial.byteLength;
} else { } else {
const array = await this.readSource(); const array = await this.#readSource();
if (array.byteLength === length) { if (array.byteLength === length) {
return array; return array;
} }
@ -64,7 +64,7 @@ export class BufferedReadableStream implements AsyncExactReadable {
} }
while (length > 0) { while (length > 0) {
const array = await this.readSource(); const array = await this.#readSource();
if (array.byteLength === length) { if (array.byteLength === length) {
result.set(array, index); result.set(array, index);
return result; return result;
@ -91,7 +91,7 @@ export class BufferedReadableStream implements AsyncExactReadable {
* @param length * @param length
* @returns * @returns
*/ */
public readExactly(length: number): Uint8Array | Promise<Uint8Array> { readExactly(length: number): Uint8Array | Promise<Uint8Array> {
// PERF: Add a synchronous path for reading from internal buffer // PERF: Add a synchronous path for reading from internal buffer
if (this.#buffered) { if (this.#buffered) {
const array = this.#buffered; const array = this.#buffered;
@ -107,10 +107,10 @@ export class BufferedReadableStream implements AsyncExactReadable {
this.#buffered = undefined; this.#buffered = undefined;
this.#bufferedLength = 0; this.#bufferedLength = 0;
this.#bufferedOffset = 0; this.#bufferedOffset = 0;
return this.readAsync(length, array.subarray(offset)); return this.#readAsync(length, array.subarray(offset));
} }
return this.readAsync(length); return this.#readAsync(length);
} }
/** /**
@ -118,7 +118,7 @@ export class BufferedReadableStream implements AsyncExactReadable {
* all data from the wrapped stream. * all data from the wrapped stream.
* @returns A `ReadableStream` * @returns A `ReadableStream`
*/ */
public release(): ReadableStream<Uint8Array> { release(): ReadableStream<Uint8Array> {
if (this.#bufferedLength > 0) { if (this.#bufferedLength > 0) {
return new PushReadableStream<Uint8Array>(async (controller) => { return new PushReadableStream<Uint8Array>(async (controller) => {
// Put the remaining data back to the stream // Put the remaining data back to the stream
@ -147,7 +147,7 @@ export class BufferedReadableStream implements AsyncExactReadable {
} }
} }
public cancel(reason?: unknown) { cancel(reason?: unknown) {
return this.reader.cancel(reason); return this.reader.cancel(reason);
} }
} }

View file

@ -41,7 +41,7 @@ export class ConcatStringStream {
this.#readableController.error(reason); this.#readableController.error(reason);
}, },
}); });
public get writable(): WritableStream<string> { get writable(): WritableStream<string> {
return this.#writable; return this.#writable;
} }
@ -51,11 +51,11 @@ export class ConcatStringStream {
this.#readableController = controller; this.#readableController = controller;
}, },
}) as ConcatStringReadableStream; }) as ConcatStringReadableStream;
public get readable(): ConcatStringReadableStream { get readable(): ConcatStringReadableStream {
return this.#readable; return this.#readable;
} }
public constructor() { constructor() {
void Object.defineProperties(this.#readable, { void Object.defineProperties(this.#readable, {
then: { then: {
get: () => get: () =>
@ -127,7 +127,7 @@ export class ConcatBufferStream {
this.#readableController.error(reason); this.#readableController.error(reason);
}, },
}); });
public get writable(): WritableStream<Uint8Array> { get writable(): WritableStream<Uint8Array> {
return this.#writable; return this.#writable;
} }
@ -137,11 +137,11 @@ export class ConcatBufferStream {
this.#readableController = controller; this.#readableController = controller;
}, },
}) as ConcatBufferReadableStream; }) as ConcatBufferReadableStream;
public get readable(): ConcatBufferReadableStream { get readable(): ConcatBufferReadableStream {
return this.#readable; return this.#readable;
} }
public constructor() { constructor() {
void Object.defineProperties(this.#readable, { void Object.defineProperties(this.#readable, {
then: { then: {
get: () => get: () =>

View file

@ -29,25 +29,25 @@ export class Consumable<T> {
readonly #task: Task; readonly #task: Task;
readonly #resolver: PromiseResolver<void>; readonly #resolver: PromiseResolver<void>;
public readonly value: T; readonly value: T;
public readonly consumed: Promise<void>; readonly consumed: Promise<void>;
public constructor(value: T) { constructor(value: T) {
this.#task = createTask("Consumable"); this.#task = createTask("Consumable");
this.value = value; this.value = value;
this.#resolver = new PromiseResolver<void>(); this.#resolver = new PromiseResolver<void>();
this.consumed = this.#resolver.promise; this.consumed = this.#resolver.promise;
} }
public consume() { consume() {
this.#resolver.resolve(); this.#resolver.resolve();
} }
public error(error: any) { error(error: any) {
this.#resolver.reject(error); this.#resolver.reject(error);
} }
public async tryConsume<U>(callback: (value: T) => U) { async tryConsume<U>(callback: (value: T) => U) {
try { try {
// eslint-disable-next-line @typescript-eslint/await-thenable // eslint-disable-next-line @typescript-eslint/await-thenable
const result = await this.#task.run(() => callback(this.value)); const result = await this.#task.run(() => callback(this.value));
@ -70,7 +70,7 @@ async function enqueue<T>(
} }
export class WrapConsumableStream<T> extends TransformStream<T, Consumable<T>> { export class WrapConsumableStream<T> extends TransformStream<T, Consumable<T>> {
public constructor() { constructor() {
super({ super({
async transform(chunk, controller) { async transform(chunk, controller) {
await enqueue(controller, chunk); await enqueue(controller, chunk);
@ -83,7 +83,7 @@ export class UnwrapConsumableStream<T> extends TransformStream<
Consumable<T>, Consumable<T>,
T T
> { > {
public constructor() { constructor() {
super({ super({
transform(chunk, controller) { transform(chunk, controller) {
controller.enqueue(chunk.value); controller.enqueue(chunk.value);
@ -110,7 +110,7 @@ export interface ConsumableReadableStreamSource<T> {
} }
export class ConsumableReadableStream<T> extends ReadableStream<Consumable<T>> { export class ConsumableReadableStream<T> extends ReadableStream<Consumable<T>> {
public constructor( constructor(
source: ConsumableReadableStreamSource<T>, source: ConsumableReadableStreamSource<T>,
strategy?: QueuingStrategy<T>, strategy?: QueuingStrategy<T>,
) { ) {
@ -168,7 +168,7 @@ export interface ConsumableWritableStreamSink<T> {
} }
export class ConsumableWritableStream<T> extends WritableStream<Consumable<T>> { export class ConsumableWritableStream<T> extends WritableStream<Consumable<T>> {
public static async write<T>( static async write<T>(
writer: WritableStreamDefaultWriter<Consumable<T>>, writer: WritableStreamDefaultWriter<Consumable<T>>,
value: T, value: T,
) { ) {
@ -177,7 +177,7 @@ export class ConsumableWritableStream<T> extends WritableStream<Consumable<T>> {
await consumable.consumed; await consumable.consumed;
} }
public constructor( constructor(
sink: ConsumableWritableStreamSink<T>, sink: ConsumableWritableStreamSink<T>,
strategy?: QueuingStrategy<T>, strategy?: QueuingStrategy<T>,
) { ) {
@ -232,7 +232,7 @@ export class ConsumableTransformStream<I, O> extends TransformStream<
Consumable<I>, Consumable<I>,
Consumable<O> Consumable<O>
> { > {
public constructor(transformer: ConsumableTransformer<I, O>) { constructor(transformer: ConsumableTransformer<I, O>) {
let wrappedController: let wrappedController:
| ConsumableReadableStreamController<O> | ConsumableReadableStreamController<O>
| undefined; | undefined;
@ -271,7 +271,7 @@ export class ConsumableInspectStream<T> extends TransformStream<
Consumable<T>, Consumable<T>,
Consumable<T> Consumable<T>
> { > {
public constructor(callback: (value: T) => void) { constructor(callback: (value: T) => void) {
super({ super({
transform(chunk, controller) { transform(chunk, controller) {
callback(chunk.value); callback(chunk.value);

View file

@ -3,7 +3,7 @@ import { decodeUtf8 } from "@yume-chan/struct";
import { TransformStream } from "./stream.js"; import { TransformStream } from "./stream.js";
export class DecodeUtf8Stream extends TransformStream<Uint8Array, string> { export class DecodeUtf8Stream extends TransformStream<Uint8Array, string> {
public constructor() { constructor() {
super({ super({
transform(chunk, controller) { transform(chunk, controller) {
controller.enqueue(decodeUtf8(chunk)); controller.enqueue(decodeUtf8(chunk));

View file

@ -9,7 +9,7 @@ export class BufferCombiner {
#offset: number; #offset: number;
#available: number; #available: number;
public constructor(size: number) { constructor(size: number) {
this.#capacity = size; this.#capacity = size;
this.#buffer = new Uint8Array(size); this.#buffer = new Uint8Array(size);
this.#offset = 0; this.#offset = 0;
@ -23,7 +23,7 @@ export class BufferCombiner {
* A generator that yields buffers of specified size. * A generator that yields buffers of specified size.
* It may yield the same buffer multiple times, consume the data before calling `next`. * It may yield the same buffer multiple times, consume the data before calling `next`.
*/ */
public *push(data: Uint8Array): Generator<Uint8Array, void, void> { *push(data: Uint8Array): Generator<Uint8Array, void, void> {
let offset = 0; let offset = 0;
let available = data.byteLength; let available = data.byteLength;
@ -65,7 +65,7 @@ export class BufferCombiner {
} }
} }
public flush(): Uint8Array | undefined { flush(): Uint8Array | undefined {
if (this.#offset === 0) { if (this.#offset === 0) {
return undefined; return undefined;
} }
@ -81,7 +81,7 @@ export class DistributionStream extends ConsumableTransformStream<
Uint8Array, Uint8Array,
Uint8Array Uint8Array
> { > {
public constructor(size: number, combine = false) { constructor(size: number, combine = false) {
const combiner = combine ? new BufferCombiner(size) : undefined; const combiner = combine ? new BufferCombiner(size) : undefined;
super({ super({
async transform(chunk, controller) { async transform(chunk, controller) {

View file

@ -50,22 +50,22 @@ export class DuplexStreamFactory<R, W> {
#writers: WritableStreamDefaultWriter<W>[] = []; #writers: WritableStreamDefaultWriter<W>[] = [];
#writableClosed = false; #writableClosed = false;
public get writableClosed() { get writableClosed() {
return this.#writableClosed; return this.#writableClosed;
} }
#closed = new PromiseResolver<void>(); #closed = new PromiseResolver<void>();
public get closed() { get closed() {
return this.#closed.promise; return this.#closed.promise;
} }
readonly #options: DuplexStreamFactoryOptions; readonly #options: DuplexStreamFactoryOptions;
public constructor(options?: DuplexStreamFactoryOptions) { constructor(options?: DuplexStreamFactoryOptions) {
this.#options = options ?? {}; this.#options = options ?? {};
} }
public wrapReadable(readable: ReadableStream<R>): WrapReadableStream<R> { wrapReadable(readable: ReadableStream<R>): WrapReadableStream<R> {
return new WrapReadableStream<R>({ return new WrapReadableStream<R>({
start: (controller) => { start: (controller) => {
this.#readableControllers.push(controller); this.#readableControllers.push(controller);
@ -82,7 +82,7 @@ export class DuplexStreamFactory<R, W> {
}); });
} }
public createWritable(stream: WritableStream<W>): WritableStream<W> { createWritable(stream: WritableStream<W>): WritableStream<W> {
const writer = stream.getWriter(); const writer = stream.getWriter();
this.#writers.push(writer); this.#writers.push(writer);
@ -104,7 +104,7 @@ export class DuplexStreamFactory<R, W> {
}); });
} }
public async close() { async close() {
if (this.#writableClosed) { if (this.#writableClosed) {
return; return;
} }
@ -122,7 +122,7 @@ export class DuplexStreamFactory<R, W> {
} }
} }
public async dispose() { async dispose() {
this.#writableClosed = true; this.#writableClosed = true;
this.#closed.resolve(); this.#closed.resolve();

View file

@ -25,7 +25,7 @@ export class PushReadableStream<T> extends ReadableStream<T> {
* when the `Promise` is resolved, and be errored when the `Promise` is rejected. * when the `Promise` is resolved, and be errored when the `Promise` is rejected.
* @param strategy * @param strategy
*/ */
public constructor( constructor(
source: PushReadableStreamSource<T>, source: PushReadableStreamSource<T>,
strategy?: QueuingStrategy<T>, strategy?: QueuingStrategy<T>,
) { ) {

View file

@ -20,7 +20,7 @@ function* split(
} }
export class SplitStringStream extends TransformStream<string, string> { export class SplitStringStream extends TransformStream<string, string> {
public constructor(separator: string) { constructor(separator: string) {
super({ super({
transform(chunk, controller) { transform(chunk, controller) {
for (const part of split(chunk, separator)) { for (const part of split(chunk, separator)) {

View file

@ -6,7 +6,7 @@ import { BufferedTransformStream } from "./buffered-transform.js";
export class StructDeserializeStream< export class StructDeserializeStream<
T extends Struct<any, any, any, any>, T extends Struct<any, any, any, any>,
> extends BufferedTransformStream<StructValueType<T>> { > extends BufferedTransformStream<StructValueType<T>> {
public constructor(struct: T) { constructor(struct: T) {
super((stream) => { super((stream) => {
return struct.deserialize(stream); return struct.deserialize(stream);
}); });

View file

@ -42,11 +42,11 @@ function getWrappedReadableStream<T>(
* 3. Convert native `ReadableStream`s to polyfilled ones so they can `pipe` between. * 3. Convert native `ReadableStream`s to polyfilled ones so they can `pipe` between.
*/ */
export class WrapReadableStream<T> extends ReadableStream<T> { export class WrapReadableStream<T> extends ReadableStream<T> {
public readable!: ReadableStream<T>; readable!: ReadableStream<T>;
#reader!: ReadableStreamDefaultReader<T>; #reader!: ReadableStreamDefaultReader<T>;
public constructor( constructor(
wrapper: wrapper:
| ReadableStream<T> | ReadableStream<T>
| WrapReadableStreamStart<T> | WrapReadableStreamStart<T>

View file

@ -30,11 +30,11 @@ async function getWrappedWritableStream<T>(
} }
export class WrapWritableStream<T> extends WritableStream<T> { export class WrapWritableStream<T> extends WritableStream<T> {
public writable!: WritableStream<T>; writable!: WritableStream<T>;
#writer!: WritableStreamDefaultWriter<T>; #writer!: WritableStreamDefaultWriter<T>;
public constructor( constructor(
wrapper: wrapper:
| WritableStream<T> | WritableStream<T>
| WrapWritableStreamStart<T> | WrapWritableStreamStart<T>
@ -73,7 +73,7 @@ export class WrapWritableStream<T> extends WritableStream<T> {
}); });
} }
public bePipedThroughFrom<U>(transformer: TransformStream<U, T>) { bePipedThroughFrom<U>(transformer: TransformStream<U, T>) {
let promise: Promise<void>; let promise: Promise<void>;
return new WrapWritableStream<U>({ return new WrapWritableStream<U>({
start: () => { start: () => {

View file

@ -12,13 +12,13 @@ describe("StructFieldDefinition", () => {
describe(".constructor", () => { describe(".constructor", () => {
it("should save the `options` parameter", () => { it("should save the `options` parameter", () => {
class MockFieldDefinition extends StructFieldDefinition<number> { class MockFieldDefinition extends StructFieldDefinition<number> {
public constructor(options: number) { constructor(options: number) {
super(options); super(options);
} }
public getSize(): number { getSize(): number {
throw new Error("Method not implemented."); throw new Error("Method not implemented.");
} }
public create( create(
options: Readonly<StructOptions>, options: Readonly<StructOptions>,
struct: StructValue, struct: StructValue,
value: unknown, value: unknown,
@ -28,17 +28,17 @@ describe("StructFieldDefinition", () => {
void value; void value;
throw new Error("Method not implemented."); throw new Error("Method not implemented.");
} }
public override deserialize( override deserialize(
options: Readonly<StructOptions>, options: Readonly<StructOptions>,
stream: ExactReadable, stream: ExactReadable,
struct: StructValue, struct: StructValue,
): StructFieldValue<this>; ): StructFieldValue<this>;
public override deserialize( override deserialize(
options: Readonly<StructOptions>, options: Readonly<StructOptions>,
stream: AsyncExactReadable, stream: AsyncExactReadable,
struct: StructValue, struct: StructValue,
): Promise<StructFieldValue<this>>; ): Promise<StructFieldValue<this>>;
public deserialize( deserialize(
options: Readonly<StructOptions>, options: Readonly<StructOptions>,
stream: ExactReadable | AsyncExactReadable, stream: ExactReadable | AsyncExactReadable,
struct: StructValue, struct: StructValue,

View file

@ -19,17 +19,17 @@ export abstract class StructFieldDefinition<
* When `T` is a type initiated `StructFieldDefinition`, * When `T` is a type initiated `StructFieldDefinition`,
* use `T['TValue']` to retrieve its `TValue` type parameter. * use `T['TValue']` to retrieve its `TValue` type parameter.
*/ */
public readonly TValue!: TValue; readonly TValue!: TValue;
/** /**
* When `T` is a type initiated `StructFieldDefinition`, * When `T` is a type initiated `StructFieldDefinition`,
* use `T['TOmitInitKey']` to retrieve its `TOmitInitKey` type parameter. * use `T['TOmitInitKey']` to retrieve its `TOmitInitKey` type parameter.
*/ */
public readonly TOmitInitKey!: TOmitInitKey; readonly TOmitInitKey!: TOmitInitKey;
public readonly options: TOptions; readonly options: TOptions;
public constructor(options: TOptions) { constructor(options: TOptions) {
this.options = options; this.options = options;
} }
@ -38,12 +38,12 @@ export abstract class StructFieldDefinition<
* *
* Actual size can be retrieved from `StructFieldValue#getSize` * Actual size can be retrieved from `StructFieldValue#getSize`
*/ */
public abstract getSize(): number; abstract getSize(): number;
/** /**
* When implemented in derived classes, creates a `StructFieldValue` from a given `value`. * When implemented in derived classes, creates a `StructFieldValue` from a given `value`.
*/ */
public abstract create( abstract create(
options: Readonly<StructOptions>, options: Readonly<StructOptions>,
structValue: StructValue, structValue: StructValue,
value: TValue, value: TValue,
@ -55,12 +55,12 @@ export abstract class StructFieldDefinition<
* *
* `SyncPromise` can be used to simplify implementation. * `SyncPromise` can be used to simplify implementation.
*/ */
public abstract deserialize( abstract deserialize(
options: Readonly<StructOptions>, options: Readonly<StructOptions>,
stream: ExactReadable, stream: ExactReadable,
structValue: StructValue, structValue: StructValue,
): StructFieldValue<this>; ): StructFieldValue<this>;
public abstract deserialize( abstract deserialize(
options: Readonly<StructOptions>, options: Readonly<StructOptions>,
stream: AsyncExactReadable, stream: AsyncExactReadable,
struct: StructValue, struct: StructValue,

View file

@ -12,7 +12,7 @@ describe("StructFieldValue", () => {
describe(".constructor", () => { describe(".constructor", () => {
it("should save parameters", () => { it("should save parameters", () => {
class MockStructFieldValue extends StructFieldValue { class MockStructFieldValue extends StructFieldValue {
public serialize(dataView: DataView, offset: number): void { serialize(dataView: DataView, offset: number): void {
void dataView; void dataView;
void offset; void offset;
throw new Error("Method not implemented."); throw new Error("Method not implemented.");
@ -40,10 +40,10 @@ describe("StructFieldValue", () => {
describe("#getSize", () => { describe("#getSize", () => {
it("should return same value as definition's", () => { it("should return same value as definition's", () => {
class MockFieldDefinition extends StructFieldDefinition { class MockFieldDefinition extends StructFieldDefinition {
public getSize(): number { getSize(): number {
return 42; return 42;
} }
public create( create(
options: Readonly<StructOptions>, options: Readonly<StructOptions>,
struct: StructValue, struct: StructValue,
value: unknown, value: unknown,
@ -54,17 +54,17 @@ describe("StructFieldValue", () => {
throw new Error("Method not implemented."); throw new Error("Method not implemented.");
} }
public override deserialize( override deserialize(
options: Readonly<StructOptions>, options: Readonly<StructOptions>,
stream: ExactReadable, stream: ExactReadable,
struct: StructValue, struct: StructValue,
): StructFieldValue<this>; ): StructFieldValue<this>;
public override deserialize( override deserialize(
options: Readonly<StructOptions>, options: Readonly<StructOptions>,
stream: AsyncExactReadable, stream: AsyncExactReadable,
struct: StructValue, struct: StructValue,
): Promise<StructFieldValue<this>>; ): Promise<StructFieldValue<this>>;
public override deserialize( override deserialize(
options: Readonly<StructOptions>, options: Readonly<StructOptions>,
stream: ExactReadable | AsyncExactReadable, stream: ExactReadable | AsyncExactReadable,
struct: StructValue, struct: StructValue,
@ -77,7 +77,7 @@ describe("StructFieldValue", () => {
} }
class MockStructFieldValue extends StructFieldValue { class MockStructFieldValue extends StructFieldValue {
public serialize(dataView: DataView, offset: number): void { serialize(dataView: DataView, offset: number): void {
void dataView; void dataView;
void offset; void offset;
throw new Error("Method not implemented."); throw new Error("Method not implemented.");
@ -98,7 +98,7 @@ describe("StructFieldValue", () => {
describe("#set", () => { describe("#set", () => {
it("should update its internal value", () => { it("should update its internal value", () => {
class MockStructFieldValue extends StructFieldValue { class MockStructFieldValue extends StructFieldValue {
public serialize(dataView: DataView, offset: number): void { serialize(dataView: DataView, offset: number): void {
void dataView; void dataView;
void offset; void offset;
throw new Error("Method not implemented."); throw new Error("Method not implemented.");

View file

@ -16,15 +16,15 @@ export abstract class StructFieldValue<
> = StructFieldDefinition<any, any, any>, > = StructFieldDefinition<any, any, any>,
> { > {
/** Gets the definition associated with this runtime value */ /** Gets the definition associated with this runtime value */
public readonly definition: TDefinition; readonly definition: TDefinition;
/** Gets the options of the associated `Struct` */ /** Gets the options of the associated `Struct` */
public readonly options: Readonly<StructOptions>; readonly options: Readonly<StructOptions>;
/** Gets the associated `Struct` instance */ /** Gets the associated `Struct` instance */
public readonly struct: StructValue; readonly struct: StructValue;
public get hasCustomAccessors(): boolean { get hasCustomAccessors(): boolean {
return ( return (
this.get !== StructFieldValue.prototype.get || this.get !== StructFieldValue.prototype.get ||
this.set !== StructFieldValue.prototype.set this.set !== StructFieldValue.prototype.set
@ -33,7 +33,7 @@ export abstract class StructFieldValue<
protected value: TDefinition["TValue"]; protected value: TDefinition["TValue"];
public constructor( constructor(
definition: TDefinition, definition: TDefinition,
options: Readonly<StructOptions>, options: Readonly<StructOptions>,
struct: StructValue, struct: StructValue,
@ -50,26 +50,26 @@ export abstract class StructFieldValue<
* *
* When overridden in derived classes, can have custom logic to calculate the actual size. * When overridden in derived classes, can have custom logic to calculate the actual size.
*/ */
public getSize(): number { getSize(): number {
return this.definition.getSize(); return this.definition.getSize();
} }
/** /**
* When implemented in derived classes, reads current field's value. * When implemented in derived classes, reads current field's value.
*/ */
public get(): TDefinition["TValue"] { get(): TDefinition["TValue"] {
return this.value; return this.value;
} }
/** /**
* When implemented in derived classes, updates current field's value. * When implemented in derived classes, updates current field's value.
*/ */
public set(value: TDefinition["TValue"]): void { set(value: TDefinition["TValue"]): void {
this.value = value; this.value = value;
} }
/** /**
* When implemented in derived classes, serializes this field into `dataView` at `offset` * When implemented in derived classes, serializes this field into `dataView` at `offset`
*/ */
public abstract serialize(dataView: DataView, offset: number): void; abstract serialize(dataView: DataView, offset: number): void;
} }

View file

@ -3,7 +3,7 @@ import type { ValueOrPromise } from "../utils.js";
// TODO: allow over reading (returning a `Uint8Array`, an `offset` and a `length`) to avoid copying // TODO: allow over reading (returning a `Uint8Array`, an `offset` and a `length`) to avoid copying
export class ExactReadableEndedError extends Error { export class ExactReadableEndedError extends Error {
public constructor() { constructor() {
super("ExactReadable ended"); super("ExactReadable ended");
Object.setPrototypeOf(this, new.target.prototype); Object.setPrototypeOf(this, new.target.prototype);
} }

View file

@ -14,9 +14,9 @@ export class StructValue {
/** /**
* Gets the result struct value object * Gets the result struct value object
*/ */
public readonly value: Record<PropertyKey, unknown>; readonly value: Record<PropertyKey, unknown>;
public constructor(prototype: object) { constructor(prototype: object) {
// PERF: `Object.create(extra)` is 50% faster // PERF: `Object.create(extra)` is 50% faster
// than `Object.defineProperties(this.value, extra)` // than `Object.defineProperties(this.value, extra)`
this.value = Object.create(prototype) as Record<PropertyKey, unknown>; this.value = Object.create(prototype) as Record<PropertyKey, unknown>;
@ -35,7 +35,7 @@ export class StructValue {
* @param name The field name * @param name The field name
* @param fieldValue The associated `StructFieldValue` * @param fieldValue The associated `StructFieldValue`
*/ */
public set(name: PropertyKey, fieldValue: StructFieldValue): void { set(name: PropertyKey, fieldValue: StructFieldValue): void {
this.fieldValues[name] = fieldValue; this.fieldValues[name] = fieldValue;
// PERF: `Object.defineProperty` is slow // PERF: `Object.defineProperty` is slow
@ -61,7 +61,7 @@ export class StructValue {
* *
* @param name The field name * @param name The field name
*/ */
public get(name: PropertyKey): StructFieldValue { get(name: PropertyKey): StructFieldValue {
return this.fieldValues[name]!; return this.fieldValues[name]!;
} }
} }

View file

@ -22,11 +22,11 @@ import {
} from "./index.js"; } from "./index.js";
class MockDeserializationStream implements ExactReadable { class MockDeserializationStream implements ExactReadable {
public buffer = new Uint8Array(0); buffer = new Uint8Array(0);
public position = 0; position = 0;
public readExactly = jest.fn(() => this.buffer); readExactly = jest.fn(() => this.buffer);
} }
describe("Struct", () => { describe("Struct", () => {
@ -40,15 +40,15 @@ describe("Struct", () => {
describe("#field", () => { describe("#field", () => {
class MockFieldDefinition extends StructFieldDefinition<number> { class MockFieldDefinition extends StructFieldDefinition<number> {
public constructor(size: number) { constructor(size: number) {
super(size); super(size);
} }
public getSize = jest.fn(() => { getSize = jest.fn(() => {
return this.options; return this.options;
}); });
public create( create(
options: Readonly<StructOptions>, options: Readonly<StructOptions>,
struct: StructValue, struct: StructValue,
value: unknown, value: unknown,
@ -58,17 +58,17 @@ describe("Struct", () => {
void value; void value;
throw new Error("Method not implemented."); throw new Error("Method not implemented.");
} }
public override deserialize( override deserialize(
options: Readonly<StructOptions>, options: Readonly<StructOptions>,
stream: ExactReadable, stream: ExactReadable,
struct: StructValue, struct: StructValue,
): StructFieldValue<this>; ): StructFieldValue<this>;
public override deserialize( override deserialize(
options: Readonly<StructOptions>, options: Readonly<StructOptions>,
stream: AsyncExactReadable, stream: AsyncExactReadable,
struct: StructValue, struct: StructValue,
): Promise<StructFieldValue<this>>; ): Promise<StructFieldValue<this>>;
public override deserialize( override deserialize(
options: Readonly<StructOptions>, options: Readonly<StructOptions>,
stream: ExactReadable | AsyncExactReadable, stream: ExactReadable | AsyncExactReadable,
struct: StructValue, struct: StructValue,

View file

@ -192,14 +192,14 @@ export type StructDeserializedResult<
: TPostDeserialized; : TPostDeserialized;
export class StructDeserializeError extends Error { export class StructDeserializeError extends Error {
public constructor(message: string) { constructor(message: string) {
super(message); super(message);
Object.setPrototypeOf(this, new.target.prototype); Object.setPrototypeOf(this, new.target.prototype);
} }
} }
export class StructNotEnoughDataError extends StructDeserializeError { export class StructNotEnoughDataError extends StructDeserializeError {
public constructor() { constructor() {
super( super(
"The underlying readable was ended before the struct was fully deserialized", "The underlying readable was ended before the struct was fully deserialized",
); );
@ -207,7 +207,7 @@ export class StructNotEnoughDataError extends StructDeserializeError {
} }
export class StructEmptyError extends StructDeserializeError { export class StructEmptyError extends StructDeserializeError {
public constructor() { constructor() {
super("The underlying readable doesn't contain any more struct"); super("The underlying readable doesn't contain any more struct");
} }
} }
@ -222,27 +222,27 @@ export class Struct<
StructDeserializedResult<TFields, TExtra, TPostDeserialized> StructDeserializedResult<TFields, TExtra, TPostDeserialized>
> >
{ {
public readonly TFields!: TFields; readonly TFields!: TFields;
public readonly TOmitInitKey!: TOmitInitKey; readonly TOmitInitKey!: TOmitInitKey;
public readonly TExtra!: TExtra; readonly TExtra!: TExtra;
public readonly TInit!: Evaluate<Omit<TFields, TOmitInitKey>>; readonly TInit!: Evaluate<Omit<TFields, TOmitInitKey>>;
public readonly TDeserializeResult!: StructDeserializedResult< readonly TDeserializeResult!: StructDeserializedResult<
TFields, TFields,
TExtra, TExtra,
TPostDeserialized TPostDeserialized
>; >;
public readonly options: Readonly<StructOptions>; readonly options: Readonly<StructOptions>;
#size = 0; #size = 0;
/** /**
* Gets the static size (exclude fields that can change size at runtime) * Gets the static size (exclude fields that can change size at runtime)
*/ */
public get size() { get size() {
return this.#size; return this.#size;
} }
@ -250,7 +250,7 @@ export class Struct<
name: PropertyKey, name: PropertyKey,
definition: StructFieldDefinition<any, any, any>, definition: StructFieldDefinition<any, any, any>,
][] = []; ][] = [];
public get fields(): readonly [ get fields(): readonly [
name: PropertyKey, name: PropertyKey,
definition: StructFieldDefinition<any, any, any>, definition: StructFieldDefinition<any, any, any>,
][] { ][] {
@ -261,14 +261,14 @@ export class Struct<
#postDeserialized?: StructPostDeserialized<any, any> | undefined; #postDeserialized?: StructPostDeserialized<any, any> | undefined;
public constructor(options?: Partial<Readonly<StructOptions>>) { constructor(options?: Partial<Readonly<StructOptions>>) {
this.options = { ...StructDefaultOptions, ...options }; this.options = { ...StructDefaultOptions, ...options };
} }
/** /**
* Appends a `StructFieldDefinition` to the `Struct * Appends a `StructFieldDefinition` to the `Struct
*/ */
public field< field<
TName extends PropertyKey, TName extends PropertyKey,
TDefinition extends StructFieldDefinition<any, any, any>, TDefinition extends StructFieldDefinition<any, any, any>,
>( >(
@ -304,7 +304,7 @@ export class Struct<
/** /**
* Merges (flats) another `Struct`'s fields and extra fields into this one. * Merges (flats) another `Struct`'s fields and extra fields into this one.
*/ */
public concat<TOther extends Struct<any, any, any, any>>( concat<TOther extends Struct<any, any, any, any>>(
other: TOther, other: TOther,
): Struct< ): Struct<
TFields & TOther["TFields"], TFields & TOther["TFields"],
@ -323,7 +323,7 @@ export class Struct<
return this as any; return this as any;
} }
private number< #number<
TName extends PropertyKey, TName extends PropertyKey,
TType extends NumberFieldType = NumberFieldType, TType extends NumberFieldType = NumberFieldType,
TTypeScriptType = number, TTypeScriptType = number,
@ -337,64 +337,64 @@ export class Struct<
/** /**
* Appends an `int8` field to the `Struct` * Appends an `int8` field to the `Struct`
*/ */
public int8<TName extends PropertyKey, TTypeScriptType = number>( int8<TName extends PropertyKey, TTypeScriptType = number>(
name: TName, name: TName,
typeScriptType?: TTypeScriptType, typeScriptType?: TTypeScriptType,
) { ) {
return this.number(name, NumberFieldType.Int8, typeScriptType); return this.#number(name, NumberFieldType.Int8, typeScriptType);
} }
/** /**
* Appends an `uint8` field to the `Struct` * Appends an `uint8` field to the `Struct`
*/ */
public uint8<TName extends PropertyKey, TTypeScriptType = number>( uint8<TName extends PropertyKey, TTypeScriptType = number>(
name: TName, name: TName,
typeScriptType?: TTypeScriptType, typeScriptType?: TTypeScriptType,
) { ) {
return this.number(name, NumberFieldType.Uint8, typeScriptType); return this.#number(name, NumberFieldType.Uint8, typeScriptType);
} }
/** /**
* Appends an `int16` field to the `Struct` * Appends an `int16` field to the `Struct`
*/ */
public int16<TName extends PropertyKey, TTypeScriptType = number>( int16<TName extends PropertyKey, TTypeScriptType = number>(
name: TName, name: TName,
typeScriptType?: TTypeScriptType, typeScriptType?: TTypeScriptType,
) { ) {
return this.number(name, NumberFieldType.Int16, typeScriptType); return this.#number(name, NumberFieldType.Int16, typeScriptType);
} }
/** /**
* Appends an `uint16` field to the `Struct` * Appends an `uint16` field to the `Struct`
*/ */
public uint16<TName extends PropertyKey, TTypeScriptType = number>( uint16<TName extends PropertyKey, TTypeScriptType = number>(
name: TName, name: TName,
typeScriptType?: TTypeScriptType, typeScriptType?: TTypeScriptType,
) { ) {
return this.number(name, NumberFieldType.Uint16, typeScriptType); return this.#number(name, NumberFieldType.Uint16, typeScriptType);
} }
/** /**
* Appends an `int32` field to the `Struct` * Appends an `int32` field to the `Struct`
*/ */
public int32<TName extends PropertyKey, TTypeScriptType = number>( int32<TName extends PropertyKey, TTypeScriptType = number>(
name: TName, name: TName,
typeScriptType?: TTypeScriptType, typeScriptType?: TTypeScriptType,
) { ) {
return this.number(name, NumberFieldType.Int32, typeScriptType); return this.#number(name, NumberFieldType.Int32, typeScriptType);
} }
/** /**
* Appends an `uint32` field to the `Struct` * Appends an `uint32` field to the `Struct`
*/ */
public uint32<TName extends PropertyKey, TTypeScriptType = number>( uint32<TName extends PropertyKey, TTypeScriptType = number>(
name: TName, name: TName,
typeScriptType?: TTypeScriptType, typeScriptType?: TTypeScriptType,
) { ) {
return this.number(name, NumberFieldType.Uint32, typeScriptType); return this.#number(name, NumberFieldType.Uint32, typeScriptType);
} }
private bigint< #bigint<
TName extends PropertyKey, TName extends PropertyKey,
TType extends BigIntFieldType = BigIntFieldType, TType extends BigIntFieldType = BigIntFieldType,
TTypeScriptType = TType["TTypeScriptType"], TTypeScriptType = TType["TTypeScriptType"],
@ -410,11 +410,11 @@ export class Struct<
* *
* Requires native `BigInt` support * Requires native `BigInt` support
*/ */
public int64< int64<
TName extends PropertyKey, TName extends PropertyKey,
TTypeScriptType = BigIntFieldType["TTypeScriptType"], TTypeScriptType = BigIntFieldType["TTypeScriptType"],
>(name: TName, typeScriptType?: TTypeScriptType) { >(name: TName, typeScriptType?: TTypeScriptType) {
return this.bigint(name, BigIntFieldType.Int64, typeScriptType); return this.#bigint(name, BigIntFieldType.Int64, typeScriptType);
} }
/** /**
@ -422,14 +422,14 @@ export class Struct<
* *
* Requires native `BigInt` support * Requires native `BigInt` support
*/ */
public uint64< uint64<
TName extends PropertyKey, TName extends PropertyKey,
TTypeScriptType = BigIntFieldType["TTypeScriptType"], TTypeScriptType = BigIntFieldType["TTypeScriptType"],
>(name: TName, typeScriptType?: TTypeScriptType) { >(name: TName, typeScriptType?: TTypeScriptType) {
return this.bigint(name, BigIntFieldType.Uint64, typeScriptType); return this.#bigint(name, BigIntFieldType.Uint64, typeScriptType);
} }
private arrayBufferLike: ArrayBufferLikeFieldCreator< #arrayBufferLike: ArrayBufferLikeFieldCreator<
TFields, TFields,
TOmitInitKey, TOmitInitKey,
TExtra, TExtra,
@ -454,14 +454,14 @@ export class Struct<
} }
}; };
public uint8Array: BoundArrayBufferLikeFieldDefinitionCreator< uint8Array: BoundArrayBufferLikeFieldDefinitionCreator<
TFields, TFields,
TOmitInitKey, TOmitInitKey,
TExtra, TExtra,
TPostDeserialized, TPostDeserialized,
Uint8ArrayBufferFieldSubType Uint8ArrayBufferFieldSubType
> = (name: PropertyKey, options: any, typeScriptType: any): any => { > = (name: PropertyKey, options: any, typeScriptType: any): any => {
return this.arrayBufferLike( return this.#arrayBufferLike(
name, name,
Uint8ArrayBufferFieldSubType.Instance, Uint8ArrayBufferFieldSubType.Instance,
options, options,
@ -469,14 +469,14 @@ export class Struct<
); );
}; };
public string: BoundArrayBufferLikeFieldDefinitionCreator< string: BoundArrayBufferLikeFieldDefinitionCreator<
TFields, TFields,
TOmitInitKey, TOmitInitKey,
TExtra, TExtra,
TPostDeserialized, TPostDeserialized,
StringBufferFieldSubType StringBufferFieldSubType
> = (name: PropertyKey, options: any, typeScriptType: any): any => { > = (name: PropertyKey, options: any, typeScriptType: any): any => {
return this.arrayBufferLike( return this.#arrayBufferLike(
name, name,
StringBufferFieldSubType.Instance, StringBufferFieldSubType.Instance,
options, options,
@ -494,7 +494,7 @@ export class Struct<
* @param value * @param value
* An object containing properties to be added to the result value. Accessors and methods are also allowed. * An object containing properties to be added to the result value. Accessors and methods are also allowed.
*/ */
public extra< extra<
T extends Record< T extends Record<
// This trick disallows any keys that are already in `TValue` // This trick disallows any keys that are already in `TValue`
Exclude<keyof T, Exclude<keyof T, keyof TFields>>, Exclude<keyof T, Exclude<keyof T, keyof TFields>>,
@ -516,7 +516,7 @@ export class Struct<
* A callback returning `never` (always throw an error) * A callback returning `never` (always throw an error)
* will also change the return type of `deserialize` to `never`. * will also change the return type of `deserialize` to `never`.
*/ */
public postDeserialize( postDeserialize(
callback: StructPostDeserialized<TFields, never>, callback: StructPostDeserialized<TFields, never>,
): Struct<TFields, TOmitInitKey, TExtra, never>; ): Struct<TFields, TOmitInitKey, TExtra, never>;
/** /**
@ -525,7 +525,7 @@ export class Struct<
* A callback returning `void` means it modify the result object in-place * A callback returning `void` means it modify the result object in-place
* (or doesn't modify it at all), so `deserialize` will still return the result object. * (or doesn't modify it at all), so `deserialize` will still return the result object.
*/ */
public postDeserialize( postDeserialize(
callback?: StructPostDeserialized<TFields, void>, callback?: StructPostDeserialized<TFields, void>,
): Struct<TFields, TOmitInitKey, TExtra, undefined>; ): Struct<TFields, TOmitInitKey, TExtra, undefined>;
/** /**
@ -534,10 +534,10 @@ export class Struct<
* A callback returning anything other than `undefined` * A callback returning anything other than `undefined`
* will `deserialize` to return that object instead. * will `deserialize` to return that object instead.
*/ */
public postDeserialize<TPostSerialize>( postDeserialize<TPostSerialize>(
callback?: StructPostDeserialized<TFields, TPostSerialize>, callback?: StructPostDeserialized<TFields, TPostSerialize>,
): Struct<TFields, TOmitInitKey, TExtra, TPostSerialize>; ): Struct<TFields, TOmitInitKey, TExtra, TPostSerialize>;
public postDeserialize(callback?: StructPostDeserialized<TFields, any>) { postDeserialize(callback?: StructPostDeserialized<TFields, any>) {
this.#postDeserialized = callback; this.#postDeserialized = callback;
return this as any; return this as any;
} }
@ -545,13 +545,13 @@ export class Struct<
/** /**
* Deserialize a struct value from `stream`. * Deserialize a struct value from `stream`.
*/ */
public deserialize( deserialize(
stream: ExactReadable, stream: ExactReadable,
): StructDeserializedResult<TFields, TExtra, TPostDeserialized>; ): StructDeserializedResult<TFields, TExtra, TPostDeserialized>;
public deserialize( deserialize(
stream: AsyncExactReadable, stream: AsyncExactReadable,
): Promise<StructDeserializedResult<TFields, TExtra, TPostDeserialized>>; ): Promise<StructDeserializedResult<TFields, TExtra, TPostDeserialized>>;
public deserialize( deserialize(
stream: ExactReadable | AsyncExactReadable, stream: ExactReadable | AsyncExactReadable,
): ValueOrPromise< ): ValueOrPromise<
StructDeserializedResult<TFields, TExtra, TPostDeserialized> StructDeserializedResult<TFields, TExtra, TPostDeserialized>
@ -603,12 +603,12 @@ export class Struct<
.valueOrPromise(); .valueOrPromise();
} }
public serialize(init: Evaluate<Omit<TFields, TOmitInitKey>>): Uint8Array; serialize(init: Evaluate<Omit<TFields, TOmitInitKey>>): Uint8Array;
public serialize( serialize(
init: Evaluate<Omit<TFields, TOmitInitKey>>, init: Evaluate<Omit<TFields, TOmitInitKey>>,
output: Uint8Array, output: Uint8Array,
): number; ): number;
public serialize( serialize(
init: Evaluate<Omit<TFields, TOmitInitKey>>, init: Evaluate<Omit<TFields, TOmitInitKey>>,
output?: Uint8Array, output?: Uint8Array,
): Uint8Array | number { ): Uint8Array | number {

View file

@ -57,11 +57,11 @@ export const SyncPromise: SyncPromiseStatic = {
class PendingSyncPromise<T> implements SyncPromise<T> { class PendingSyncPromise<T> implements SyncPromise<T> {
#promise: PromiseLike<T>; #promise: PromiseLike<T>;
public constructor(promise: PromiseLike<T>) { constructor(promise: PromiseLike<T>) {
this.#promise = promise; this.#promise = promise;
} }
public then<TResult1 = T, TResult2 = never>( then<TResult1 = T, TResult2 = never>(
onfulfilled?: onfulfilled?:
| ((value: T) => TResult1 | PromiseLike<TResult1>) | ((value: T) => TResult1 | PromiseLike<TResult1>)
| null | null
@ -76,7 +76,7 @@ class PendingSyncPromise<T> implements SyncPromise<T> {
); );
} }
public valueOrPromise(): T | PromiseLike<T> { valueOrPromise(): T | PromiseLike<T> {
return this.#promise; return this.#promise;
} }
} }
@ -84,11 +84,11 @@ class PendingSyncPromise<T> implements SyncPromise<T> {
class ResolvedSyncPromise<T> implements SyncPromise<T> { class ResolvedSyncPromise<T> implements SyncPromise<T> {
#value: T; #value: T;
public constructor(value: T) { constructor(value: T) {
this.#value = value; this.#value = value;
} }
public then<TResult1 = T>( then<TResult1 = T>(
onfulfilled?: onfulfilled?:
| ((value: T) => TResult1 | PromiseLike<TResult1>) | ((value: T) => TResult1 | PromiseLike<TResult1>)
| null | null
@ -100,7 +100,7 @@ class ResolvedSyncPromise<T> implements SyncPromise<T> {
return SyncPromise.try(() => onfulfilled(this.#value)); return SyncPromise.try(() => onfulfilled(this.#value));
} }
public valueOrPromise(): T | PromiseLike<T> { valueOrPromise(): T | PromiseLike<T> {
return this.#value; return this.#value;
} }
} }
@ -108,11 +108,11 @@ class ResolvedSyncPromise<T> implements SyncPromise<T> {
class RejectedSyncPromise<T> implements SyncPromise<T> { class RejectedSyncPromise<T> implements SyncPromise<T> {
#reason: any; #reason: any;
public constructor(reason: any) { constructor(reason: any) {
this.#reason = reason; this.#reason = reason;
} }
public then<TResult1 = T, TResult2 = never>( then<TResult1 = T, TResult2 = never>(
onfulfilled?: onfulfilled?:
| ((value: T) => TResult1 | PromiseLike<TResult1>) | ((value: T) => TResult1 | PromiseLike<TResult1>)
| null | null
@ -128,7 +128,7 @@ class RejectedSyncPromise<T> implements SyncPromise<T> {
return SyncPromise.try(() => onrejected(this.#reason)); return SyncPromise.try(() => onrejected(this.#reason));
} }
public valueOrPromise(): T | PromiseLike<T> { valueOrPromise(): T | PromiseLike<T> {
throw this.#reason; throw this.#reason;
} }
} }

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