mirror of
https://github.com/yume-chan/ya-webadb.git
synced 2025-10-04 18:29:23 +02:00
feat: migrate scrcpy to streams
This commit is contained in:
parent
8b78c9c331
commit
b7725567a6
21 changed files with 308 additions and 460 deletions
11
common/config/rush/pnpm-lock.yaml
generated
11
common/config/rush/pnpm-lock.yaml
generated
|
@ -13074,13 +13074,20 @@ packages:
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
file:projects/adb-backend-ws.tgz:
|
file:projects/adb-backend-ws.tgz:
|
||||||
resolution: {integrity: sha512-FCm/eOTkUVJ+hsthYXzX2GrLsrapfStzJ/aXIjv/gRGG1sE9bxL1YOGyJl28K7PldM0oiLXW8zedvB7Ry57w9g==, tarball: file:projects/adb-backend-ws.tgz}
|
resolution: {integrity: sha512-68/Hp311ik7D+hvQyWS3p3SJah0w7ypo1ZFcPMtX+6W443UF2J+0qr6BHMwD/3kXvIvUI3nIeBvfuQFFpXwP0w==, tarball: file:projects/adb-backend-ws.tgz}
|
||||||
name: '@rush-temp/adb-backend-ws'
|
name: '@rush-temp/adb-backend-ws'
|
||||||
version: 0.0.0
|
version: 0.0.0
|
||||||
dependencies:
|
dependencies:
|
||||||
'@yume-chan/async': 2.1.4
|
'@yume-chan/async': 2.1.4
|
||||||
|
jest: 26.6.3
|
||||||
tslib: 2.3.1
|
tslib: 2.3.1
|
||||||
typescript: 4.5.5
|
typescript: 4.5.5
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- bufferutil
|
||||||
|
- canvas
|
||||||
|
- supports-color
|
||||||
|
- ts-node
|
||||||
|
- utf-8-validate
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
file:projects/adb-credential-web.tgz:
|
file:projects/adb-credential-web.tgz:
|
||||||
|
@ -13094,7 +13101,7 @@ packages:
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
file:projects/adb.tgz:
|
file:projects/adb.tgz:
|
||||||
resolution: {integrity: sha512-g2UseeMAhatJMFHZFEzJLFPricrQdJeHKXH5KE9XBDIoNla+2aB5UlZRLC+a/WKIa+WBQBD6bWT/3Ydwm5iexA==, tarball: file:projects/adb.tgz}
|
resolution: {integrity: sha512-ERJiC9hMtOFRZhGBqQ76HXFVsjsGHajyWrPoHqOrAW4LqTdHf8jJueqhzPt6e9asmTuxcOynqz4MzbYnahcTtA==, tarball: file:projects/adb.tgz}
|
||||||
name: '@rush-temp/adb'
|
name: '@rush-temp/adb'
|
||||||
version: 0.0.0
|
version: 0.0.0
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { AdbBackend, ReadableStream, TransformStream, WritableStream } from '@yume-chan/adb';
|
import { AdbBackend, ReadableStream, ReadableWritablePair, TransformStream, WritableStream } from '@yume-chan/adb';
|
||||||
import { EventEmitter } from '@yume-chan/event';
|
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface TCPSocket {
|
interface TCPSocket {
|
||||||
|
@ -30,20 +29,8 @@ declare global {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class AdbDirectSocketsBackend implements AdbBackend {
|
export class AdbDirectSocketsBackendStreams implements ReadableWritablePair<ArrayBuffer, ArrayBuffer>{
|
||||||
public static isSupported(): boolean {
|
private socket: TCPSocket;
|
||||||
return typeof window !== 'undefined' && !!window.navigator?.openTCPSocket;
|
|
||||||
}
|
|
||||||
|
|
||||||
public readonly serial: string;
|
|
||||||
|
|
||||||
public readonly address: string;
|
|
||||||
|
|
||||||
public readonly port: number;
|
|
||||||
|
|
||||||
public name: string | undefined;
|
|
||||||
|
|
||||||
private socket: TCPSocket | undefined;
|
|
||||||
|
|
||||||
private _readableTransformStream = new TransformStream<Uint8Array, ArrayBuffer>({
|
private _readableTransformStream = new TransformStream<Uint8Array, ArrayBuffer>({
|
||||||
transform(chunk, controller) {
|
transform(chunk, controller) {
|
||||||
|
@ -56,44 +43,43 @@ export default class AdbDirectSocketsBackend implements AdbBackend {
|
||||||
return this._readableTransformStream.readable;
|
return this._readableTransformStream.readable;
|
||||||
}
|
}
|
||||||
|
|
||||||
public get writable(): WritableStream<ArrayBuffer> | undefined {
|
public get writable(): WritableStream<ArrayBuffer> {
|
||||||
return this.socket?.writable;
|
return this.socket.writable;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _connected = false;
|
constructor(socket: TCPSocket) {
|
||||||
public get connected() { return this._connected; }
|
this.socket = socket;
|
||||||
|
this.socket.readable.pipeTo(this._readableTransformStream.writable);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private readonly disconnectEvent = new EventEmitter<void>();
|
export default class AdbDirectSocketsBackend implements AdbBackend {
|
||||||
public readonly onDisconnected = this.disconnectEvent.event;
|
public static isSupported(): boolean {
|
||||||
|
return typeof window !== 'undefined' && !!window.navigator?.openTCPSocket;
|
||||||
|
}
|
||||||
|
|
||||||
public constructor(address: string, port: number = 5555, name?: string) {
|
public readonly serial: string;
|
||||||
this.address = address;
|
|
||||||
|
public readonly host: string;
|
||||||
|
|
||||||
|
public readonly port: number;
|
||||||
|
|
||||||
|
public name: string | undefined;
|
||||||
|
|
||||||
|
public constructor(host: string, port: number = 5555, name?: string) {
|
||||||
|
this.host = host;
|
||||||
this.port = port;
|
this.port = port;
|
||||||
this.serial = `${address}:${port}`;
|
this.serial = `${host}:${port}`;
|
||||||
this.name = name;
|
this.name = name;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async connect() {
|
public async connect() {
|
||||||
const socket = await navigator.openTCPSocket({
|
const socket = await navigator.openTCPSocket({
|
||||||
remoteAddress: this.address,
|
remoteAddress: this.host,
|
||||||
remotePort: this.port,
|
remotePort: this.port,
|
||||||
noDelay: true,
|
noDelay: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.socket = socket;
|
return new AdbDirectSocketsBackendStreams(socket);
|
||||||
this.socket.readable
|
|
||||||
.pipeThrough(new TransformStream<Uint8Array, Uint8Array>({
|
|
||||||
flush: () => {
|
|
||||||
this.disconnectEvent.fire();
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
.pipeTo(this._readableTransformStream.writable);
|
|
||||||
|
|
||||||
this._connected = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public dispose(): void | Promise<void> {
|
|
||||||
this.socket?.close();
|
|
||||||
this._connected = false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { AdbBackend, ReadableStream } from '@yume-chan/adb';
|
import { AdbBackend, ReadableStream, ReadableWritablePair } from '@yume-chan/adb';
|
||||||
import { EventEmitter } from '@yume-chan/event';
|
|
||||||
|
|
||||||
export const WebUsbDeviceFilter: USBDeviceFilter = {
|
export const WebUsbDeviceFilter: USBDeviceFilter = {
|
||||||
classCode: 0xFF,
|
classCode: 0xFF,
|
||||||
|
@ -7,6 +6,49 @@ export const WebUsbDeviceFilter: USBDeviceFilter = {
|
||||||
protocolCode: 1,
|
protocolCode: 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export class AdbWebUsbBackendStream implements ReadableWritablePair<ArrayBuffer, ArrayBuffer>{
|
||||||
|
private _readable: ReadableStream<ArrayBuffer>;
|
||||||
|
public get readable() { return this._readable; }
|
||||||
|
|
||||||
|
private _writable: WritableStream<ArrayBuffer>;
|
||||||
|
public get writable() { return this._writable; }
|
||||||
|
|
||||||
|
public constructor(device: USBDevice, inEndpoint: USBEndpoint, outEndpoint: USBEndpoint) {
|
||||||
|
this._readable = new ReadableStream({
|
||||||
|
pull: async (controller) => {
|
||||||
|
let result = await device.transferIn(inEndpoint.endpointNumber, inEndpoint.packetSize);
|
||||||
|
|
||||||
|
if (result.status === 'stall') {
|
||||||
|
// https://android.googlesource.com/platform/packages/modules/adb/+/79010dc6d5ca7490c493df800d4421730f5466ca/client/usb_osx.cpp#543
|
||||||
|
await device.clearHalt('in', inEndpoint.endpointNumber);
|
||||||
|
result = await device.transferIn(inEndpoint.endpointNumber, inEndpoint.packetSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { buffer } = result.data!;
|
||||||
|
controller.enqueue(buffer);
|
||||||
|
},
|
||||||
|
cancel: async () => {
|
||||||
|
await device.close();
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
highWaterMark: 16 * 1024,
|
||||||
|
size(chunk) { return chunk.byteLength; },
|
||||||
|
});
|
||||||
|
|
||||||
|
this._writable = new WritableStream({
|
||||||
|
write: async (chunk) => {
|
||||||
|
await device.transferOut(outEndpoint.endpointNumber, chunk);
|
||||||
|
},
|
||||||
|
close: async () => {
|
||||||
|
await device.close();
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
highWaterMark: 16 * 1024,
|
||||||
|
size(chunk) { return chunk.byteLength; },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class AdbWebUsbBackend implements AdbBackend {
|
export class AdbWebUsbBackend implements AdbBackend {
|
||||||
public static isSupported(): boolean {
|
public static isSupported(): boolean {
|
||||||
return !!window.navigator?.usb;
|
return !!window.navigator?.usb;
|
||||||
|
@ -37,31 +79,11 @@ export class AdbWebUsbBackend implements AdbBackend {
|
||||||
|
|
||||||
public get name(): string { return this._device.productName!; }
|
public get name(): string { return this._device.productName!; }
|
||||||
|
|
||||||
private _connected = false;
|
|
||||||
public get connected() { return this._connected; }
|
|
||||||
|
|
||||||
private readonly disconnectEvent = new EventEmitter<void>();
|
|
||||||
public readonly onDisconnected = this.disconnectEvent.event;
|
|
||||||
|
|
||||||
private _readable: ReadableStream<ArrayBuffer> | undefined;
|
|
||||||
public get readable() { return this._readable; }
|
|
||||||
|
|
||||||
private _writable: WritableStream<ArrayBuffer> | undefined;
|
|
||||||
public get writable() { return this._writable; }
|
|
||||||
|
|
||||||
public constructor(device: USBDevice) {
|
public constructor(device: USBDevice) {
|
||||||
this._device = device;
|
this._device = device;
|
||||||
window.navigator.usb.addEventListener('disconnect', this.handleDisconnect);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleDisconnect = (e: USBConnectionEvent) => {
|
public async connect() {
|
||||||
if (e.device === this._device) {
|
|
||||||
this._connected = false;
|
|
||||||
this.disconnectEvent.fire();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
public async connect(): Promise<void> {
|
|
||||||
if (!this._device.opened) {
|
if (!this._device.opened) {
|
||||||
await this._device.open();
|
await this._device.open();
|
||||||
}
|
}
|
||||||
|
@ -84,49 +106,21 @@ export class AdbWebUsbBackend implements AdbBackend {
|
||||||
await this._device.selectAlternateInterface(interface_.interfaceNumber, alternate.alternateSetting);
|
await this._device.selectAlternateInterface(interface_.interfaceNumber, alternate.alternateSetting);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let inEndpoint: USBEndpoint | undefined;
|
||||||
|
let outEndpoint: USBEndpoint | undefined;
|
||||||
|
|
||||||
for (const endpoint of alternate.endpoints) {
|
for (const endpoint of alternate.endpoints) {
|
||||||
switch (endpoint.direction) {
|
switch (endpoint.direction) {
|
||||||
case 'in':
|
case 'in':
|
||||||
this._readable = new ReadableStream({
|
inEndpoint = endpoint;
|
||||||
pull: async (controller) => {
|
if (outEndpoint) {
|
||||||
let result = await this._device.transferIn(endpoint.endpointNumber, endpoint.packetSize);
|
return new AdbWebUsbBackendStream(this._device, inEndpoint, outEndpoint);
|
||||||
|
|
||||||
if (result.status === 'stall') {
|
|
||||||
// https://android.googlesource.com/platform/packages/modules/adb/+/79010dc6d5ca7490c493df800d4421730f5466ca/client/usb_osx.cpp#543
|
|
||||||
await this._device.clearHalt('in', endpoint.endpointNumber);
|
|
||||||
result = await this._device.transferIn(endpoint.endpointNumber, endpoint.packetSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { buffer } = result.data!;
|
|
||||||
controller.enqueue(buffer);
|
|
||||||
},
|
|
||||||
cancel: () => {
|
|
||||||
this.dispose();
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
highWaterMark: 16 * 1024,
|
|
||||||
size(chunk) { return chunk.byteLength; },
|
|
||||||
});
|
|
||||||
if (this._writable !== undefined) {
|
|
||||||
this._connected = true;
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'out':
|
case 'out':
|
||||||
this._writable = new WritableStream({
|
outEndpoint = endpoint;
|
||||||
write: async (chunk) => {
|
if (inEndpoint) {
|
||||||
await this._device.transferOut(endpoint.endpointNumber, chunk);
|
return new AdbWebUsbBackendStream(this._device, inEndpoint, outEndpoint);
|
||||||
},
|
|
||||||
close: () => {
|
|
||||||
this.dispose();
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
highWaterMark: 16 * 1024,
|
|
||||||
size(chunk) { return chunk.byteLength; },
|
|
||||||
});
|
|
||||||
if (this.readable !== undefined) {
|
|
||||||
this._connected = true;
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -138,11 +132,4 @@ export class AdbWebUsbBackend implements AdbBackend {
|
||||||
|
|
||||||
throw new Error('Unknown error');
|
throw new Error('Unknown error');
|
||||||
}
|
}
|
||||||
|
|
||||||
public async dispose() {
|
|
||||||
this._connected = false;
|
|
||||||
window.navigator.usb.removeEventListener('disconnect', this.handleDisconnect);
|
|
||||||
this.disconnectEvent.dispose();
|
|
||||||
await this._device.close();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
{
|
{
|
||||||
"extends": "./node_modules/@yume-chan/ts-package-builder/tsconfig.base.json",
|
"extends": "./node_modules/@yume-chan/ts-package-builder/tsconfig.base.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ES2016",
|
|
||||||
"lib": [
|
"lib": [
|
||||||
"ESNext",
|
"ESNext",
|
||||||
"DOM"
|
"DOM"
|
||||||
|
|
|
@ -30,6 +30,7 @@
|
||||||
"build:watch": "build-ts-package --incremental"
|
"build:watch": "build-ts-package --incremental"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"jest": "^26.6.3",
|
||||||
"typescript": "^4.5.5",
|
"typescript": "^4.5.5",
|
||||||
"@yume-chan/ts-package-builder": "^1.0.0"
|
"@yume-chan/ts-package-builder": "^1.0.0"
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,37 +1,11 @@
|
||||||
import { AdbBackend, ReadableStream, WritableStream } from '@yume-chan/adb';
|
import { AdbBackend, ReadableStream, WritableStream } from '@yume-chan/adb';
|
||||||
import { PromiseResolver } from '@yume-chan/async';
|
import { PromiseResolver } from '@yume-chan/async';
|
||||||
import { EventEmitter } from '@yume-chan/event';
|
|
||||||
|
|
||||||
const Utf8Encoder = new TextEncoder();
|
|
||||||
const Utf8Decoder = new TextDecoder();
|
|
||||||
|
|
||||||
export function encodeUtf8(input: string): ArrayBuffer {
|
|
||||||
return Utf8Encoder.encode(input).buffer;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function decodeUtf8(buffer: ArrayBuffer): string {
|
|
||||||
return Utf8Decoder.decode(buffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class AdbWsBackend implements AdbBackend {
|
export default class AdbWsBackend implements AdbBackend {
|
||||||
public readonly serial: string;
|
public readonly serial: string;
|
||||||
|
|
||||||
public name: string | undefined;
|
public name: string | undefined;
|
||||||
|
|
||||||
private socket: WebSocket | undefined;
|
|
||||||
|
|
||||||
private _readable: ReadableStream<ArrayBuffer> | undefined;
|
|
||||||
public get readable() { return this._readable; }
|
|
||||||
|
|
||||||
private _writable: WritableStream<ArrayBuffer> | undefined;
|
|
||||||
public get writable() { return this._writable; }
|
|
||||||
|
|
||||||
private _connected = false;
|
|
||||||
public get connected() { return this._connected; }
|
|
||||||
|
|
||||||
private readonly disconnectEvent = new EventEmitter<void>();
|
|
||||||
public readonly onDisconnected = this.disconnectEvent.event;
|
|
||||||
|
|
||||||
public constructor(url: string, name?: string) {
|
public constructor(url: string, name?: string) {
|
||||||
this.serial = url;
|
this.serial = url;
|
||||||
this.name = name;
|
this.name = name;
|
||||||
|
@ -48,22 +22,21 @@ export default class AdbWsBackend implements AdbBackend {
|
||||||
};
|
};
|
||||||
await resolver.promise;
|
await resolver.promise;
|
||||||
|
|
||||||
this._readable = new ReadableStream({
|
const readable = new ReadableStream({
|
||||||
start: (controller) => {
|
start: (controller) => {
|
||||||
socket.onmessage = ({ data }: { data: ArrayBuffer; }) => {
|
socket.onmessage = ({ data }: { data: ArrayBuffer; }) => {
|
||||||
controller.enqueue(data);
|
controller.enqueue(data);
|
||||||
};
|
};
|
||||||
socket.onclose = () => {
|
socket.onclose = () => {
|
||||||
controller.close();
|
controller.close();
|
||||||
this._connected = false;
|
|
||||||
this.disconnectEvent.fire();
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
highWaterMark: 16 * 1024,
|
highWaterMark: 16 * 1024,
|
||||||
size(chunk) { return chunk.byteLength; },
|
size(chunk) { return chunk.byteLength; },
|
||||||
});
|
});
|
||||||
this._writable = new WritableStream({
|
|
||||||
|
const writable = new WritableStream({
|
||||||
write: (chunk) => {
|
write: (chunk) => {
|
||||||
socket.send(chunk);
|
socket.send(chunk);
|
||||||
},
|
},
|
||||||
|
@ -72,23 +45,6 @@ export default class AdbWsBackend implements AdbBackend {
|
||||||
size(chunk) { return chunk.byteLength; },
|
size(chunk) { return chunk.byteLength; },
|
||||||
});
|
});
|
||||||
|
|
||||||
this.socket = socket;
|
return { readable, writable };
|
||||||
this._connected = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public encodeUtf8(input: string): ArrayBuffer {
|
|
||||||
return encodeUtf8(input);
|
|
||||||
}
|
|
||||||
|
|
||||||
public decodeUtf8(buffer: ArrayBuffer): string {
|
|
||||||
return decodeUtf8(buffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
public write(buffer: ArrayBuffer): void | Promise<void> {
|
|
||||||
this.socket?.send(buffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
public dispose(): void | Promise<void> {
|
|
||||||
this.socket?.close();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,11 +2,11 @@ import { PromiseResolver } from '@yume-chan/async';
|
||||||
import { DisposableList } from '@yume-chan/event';
|
import { DisposableList } from '@yume-chan/event';
|
||||||
import { AdbAuthenticationHandler, AdbCredentialStore, AdbDefaultAuthenticators } from './auth';
|
import { AdbAuthenticationHandler, AdbCredentialStore, AdbDefaultAuthenticators } from './auth';
|
||||||
import { AdbBackend } from './backend';
|
import { AdbBackend } from './backend';
|
||||||
import { AdbSubprocess, AdbFrameBuffer, AdbPower, AdbReverseCommand, AdbSync, AdbTcpIpCommand, escapeArg, framebuffer, install } from './commands';
|
import { AdbFrameBuffer, AdbPower, AdbReverseCommand, AdbSubprocess, AdbSync, AdbTcpIpCommand, escapeArg, framebuffer, install } from './commands';
|
||||||
import { AdbFeatures } from './features';
|
import { AdbFeatures } from './features';
|
||||||
import { AdbCommand } from './packet';
|
import { AdbCommand } from './packet';
|
||||||
import { AdbLogger, AdbPacketDispatcher, AdbSocket } from './socket';
|
import { AdbLogger, AdbPacketDispatcher, AdbSocket } from './socket';
|
||||||
import { decodeUtf8, ReadableStream } from "./utils";
|
import { decodeUtf8, ReadableStream, WritableStream } from "./utils";
|
||||||
|
|
||||||
export enum AdbPropKey {
|
export enum AdbPropKey {
|
||||||
Product = 'ro.product.name',
|
Product = 'ro.product.name',
|
||||||
|
@ -16,17 +16,17 @@ export enum AdbPropKey {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Adb {
|
export class Adb {
|
||||||
|
public static async connect(backend: AdbBackend, logger?: AdbLogger) {
|
||||||
|
const { readable, writable } = await backend.connect();
|
||||||
|
return new Adb(backend, readable, writable, logger);
|
||||||
|
}
|
||||||
|
|
||||||
private readonly _backend: AdbBackend;
|
private readonly _backend: AdbBackend;
|
||||||
|
|
||||||
public get backend(): AdbBackend { return this._backend; }
|
public get backend(): AdbBackend { return this._backend; }
|
||||||
|
|
||||||
private readonly packetDispatcher: AdbPacketDispatcher;
|
private readonly packetDispatcher: AdbPacketDispatcher;
|
||||||
|
|
||||||
public get onDisconnected() { return this.backend.onDisconnected; }
|
|
||||||
|
|
||||||
private _connected = false;
|
|
||||||
public get connected() { return this._connected; }
|
|
||||||
|
|
||||||
public get name() { return this.backend.name; }
|
public get name() { return this.backend.name; }
|
||||||
|
|
||||||
private _protocolVersion: number | undefined;
|
private _protocolVersion: number | undefined;
|
||||||
|
@ -49,28 +49,28 @@ export class Adb {
|
||||||
public readonly reverse: AdbReverseCommand;
|
public readonly reverse: AdbReverseCommand;
|
||||||
public readonly tcpip: AdbTcpIpCommand;
|
public readonly tcpip: AdbTcpIpCommand;
|
||||||
|
|
||||||
public constructor(backend: AdbBackend, logger?: AdbLogger) {
|
public constructor(
|
||||||
|
backend: AdbBackend,
|
||||||
|
readable: ReadableStream<ArrayBuffer>,
|
||||||
|
writable: WritableStream<ArrayBuffer>,
|
||||||
|
logger?: AdbLogger
|
||||||
|
) {
|
||||||
this._backend = backend;
|
this._backend = backend;
|
||||||
this.packetDispatcher = new AdbPacketDispatcher(backend, logger);
|
this.packetDispatcher = new AdbPacketDispatcher(readable, writable, logger);
|
||||||
|
|
||||||
this.subprocess = new AdbSubprocess(this);
|
this.subprocess = new AdbSubprocess(this);
|
||||||
this.power = new AdbPower(this);
|
this.power = new AdbPower(this);
|
||||||
this.reverse = new AdbReverseCommand(this.packetDispatcher);
|
this.reverse = new AdbReverseCommand(this.packetDispatcher);
|
||||||
this.tcpip = new AdbTcpIpCommand(this);
|
this.tcpip = new AdbTcpIpCommand(this);
|
||||||
|
|
||||||
backend.onDisconnected(this.dispose, this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async connect(
|
public async authenticate(
|
||||||
credentialStore: AdbCredentialStore,
|
credentialStore: AdbCredentialStore,
|
||||||
authenticators = AdbDefaultAuthenticators
|
authenticators = AdbDefaultAuthenticators
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await this.backend.connect?.();
|
|
||||||
this.packetDispatcher.maxPayloadSize = 0x1000;
|
this.packetDispatcher.maxPayloadSize = 0x1000;
|
||||||
// TODO: Adb: properly set `calculateChecksum`
|
this.packetDispatcher.calculateChecksum = true;
|
||||||
// this.packetDispatcher.calculateChecksum = true;
|
|
||||||
this.packetDispatcher.appendNullToServiceString = true;
|
this.packetDispatcher.appendNullToServiceString = true;
|
||||||
this.packetDispatcher.start();
|
|
||||||
|
|
||||||
const version = 0x01000001;
|
const version = 0x01000001;
|
||||||
const versionNoChecksum = 0x01000001;
|
const versionNoChecksum = 0x01000001;
|
||||||
|
@ -140,20 +140,17 @@ export class Adb {
|
||||||
resolver.reject(e);
|
resolver.reject(e);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Android prior 9.0.0 requires the null character
|
|
||||||
// Newer versions can also handle the null character
|
|
||||||
// The terminating `;` is required in formal definition
|
|
||||||
// But ADB daemon can also work without it
|
|
||||||
await this.packetDispatcher.sendPacket(
|
await this.packetDispatcher.sendPacket(
|
||||||
AdbCommand.Connect,
|
AdbCommand.Connect,
|
||||||
version,
|
version,
|
||||||
maxPayloadSize,
|
maxPayloadSize,
|
||||||
`host::features=${features};\0`
|
// The terminating `;` is required in formal definition
|
||||||
|
// But ADB daemon can also work without it
|
||||||
|
`host::features=${features};`
|
||||||
);
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await resolver.promise;
|
await resolver.promise;
|
||||||
this._connected = true;
|
|
||||||
} finally {
|
} finally {
|
||||||
disposableList.dispose();
|
disposableList.dispose();
|
||||||
}
|
}
|
||||||
|
@ -236,6 +233,5 @@ export class Adb {
|
||||||
|
|
||||||
public async dispose(): Promise<void> {
|
public async dispose(): Promise<void> {
|
||||||
this.packetDispatcher.dispose();
|
this.packetDispatcher.dispose();
|
||||||
await this.backend.dispose();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,21 +1,10 @@
|
||||||
import type { Event } from '@yume-chan/event';
|
|
||||||
import type { ValueOrPromise } from '@yume-chan/struct';
|
import type { ValueOrPromise } from '@yume-chan/struct';
|
||||||
import type { ReadableStream, WritableStream } from "./utils";
|
import type { ReadableWritablePair } from "./utils";
|
||||||
|
|
||||||
export interface AdbBackend {
|
export interface AdbBackend {
|
||||||
readonly serial: string;
|
readonly serial: string;
|
||||||
|
|
||||||
readonly name: string | undefined;
|
readonly name: string | undefined;
|
||||||
|
|
||||||
readonly connected: boolean;
|
connect(): ValueOrPromise<ReadableWritablePair<ArrayBuffer, ArrayBuffer>>;
|
||||||
|
|
||||||
readonly onDisconnected: Event<void>;
|
|
||||||
|
|
||||||
readonly readable: ReadableStream<ArrayBuffer> | undefined;
|
|
||||||
|
|
||||||
readonly writable: WritableStream<ArrayBuffer> | undefined;
|
|
||||||
|
|
||||||
connect?(): ValueOrPromise<void>;
|
|
||||||
|
|
||||||
dispose(): ValueOrPromise<void>;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,10 @@
|
||||||
import { AutoDisposable } from '@yume-chan/event';
|
import { AutoDisposable } from '@yume-chan/event';
|
||||||
import { AdbBackend } from '../backend';
|
|
||||||
import { AdbCommand } from '../packet';
|
import { AdbCommand } from '../packet';
|
||||||
import { AutoResetEvent, chunkArrayLike, TransformStream, WritableStream, WritableStreamDefaultWriter } from '../utils';
|
import { AutoResetEvent, chunkArrayLike, TransformStream, WritableStream, WritableStreamDefaultWriter } from '../utils';
|
||||||
import { AdbPacketDispatcher } from './dispatcher';
|
import { AdbPacketDispatcher } from './dispatcher';
|
||||||
|
|
||||||
export interface AdbSocketInfo {
|
export interface AdbSocketInfo {
|
||||||
backend: AdbBackend;
|
|
||||||
|
|
||||||
localId: number;
|
localId: number;
|
||||||
|
|
||||||
remoteId: number;
|
remoteId: number;
|
||||||
|
|
||||||
localCreated: boolean;
|
localCreated: boolean;
|
||||||
|
@ -35,7 +31,6 @@ export class AdbSocketController extends AutoDisposable implements AdbSocketInfo
|
||||||
private readonly writeLock = this.addDisposable(new AutoResetEvent());
|
private readonly writeLock = this.addDisposable(new AutoResetEvent());
|
||||||
|
|
||||||
private readonly dispatcher!: AdbPacketDispatcher;
|
private readonly dispatcher!: AdbPacketDispatcher;
|
||||||
public get backend() { return this.dispatcher.backend; }
|
|
||||||
|
|
||||||
public readonly localId!: number;
|
public readonly localId!: number;
|
||||||
public readonly remoteId!: number;
|
public readonly remoteId!: number;
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
import { AsyncOperationManager } from '@yume-chan/async';
|
import { AsyncOperationManager } from '@yume-chan/async';
|
||||||
import { AutoDisposable, EventEmitter } from '@yume-chan/event';
|
import { AutoDisposable, EventEmitter } from '@yume-chan/event';
|
||||||
import { AdbBackend } from '../backend';
|
|
||||||
import { AdbCommand, AdbPacket, AdbPacketInit, AdbPacketSerializeStream } from '../packet';
|
import { AdbCommand, AdbPacket, AdbPacketInit, AdbPacketSerializeStream } from '../packet';
|
||||||
import { AbortController, decodeUtf8, encodeUtf8, StructDeserializeStream, WritableStream, WritableStreamDefaultWriter } from '../utils';
|
import { AbortController, decodeUtf8, encodeUtf8, ReadableStream, StructDeserializeStream, TransformStream, WritableStream, WritableStreamDefaultWriter } from '../utils';
|
||||||
import { AdbSocketController } from './controller';
|
import { AdbSocketController } from './controller';
|
||||||
import { AdbLogger } from './logger';
|
import { AdbLogger } from './logger';
|
||||||
import { AdbSocket } from './socket';
|
import { AdbSocket } from './socket';
|
||||||
|
@ -32,7 +31,6 @@ export class AdbPacketDispatcher extends AutoDisposable {
|
||||||
private readonly sockets = new Map<number, AdbSocketController>();
|
private readonly sockets = new Map<number, AdbSocketController>();
|
||||||
private readonly logger: AdbLogger | undefined;
|
private readonly logger: AdbLogger | undefined;
|
||||||
|
|
||||||
public readonly backend: AdbBackend;
|
|
||||||
private _packetSerializeStream!: AdbPacketSerializeStream;
|
private _packetSerializeStream!: AdbPacketSerializeStream;
|
||||||
private _packetSerializeStreamWriter!: WritableStreamDefaultWriter<AdbPacketInit>;
|
private _packetSerializeStreamWriter!: WritableStreamDefaultWriter<AdbPacketInit>;
|
||||||
|
|
||||||
|
@ -50,15 +48,62 @@ export class AdbPacketDispatcher extends AutoDisposable {
|
||||||
private readonly errorEvent = this.addDisposable(new EventEmitter<Error>());
|
private readonly errorEvent = this.addDisposable(new EventEmitter<Error>());
|
||||||
public get onError() { return this.errorEvent.event; }
|
public get onError() { return this.errorEvent.event; }
|
||||||
|
|
||||||
private _running = false;
|
private _abortController = new AbortController();
|
||||||
public get running() { return this._running; }
|
|
||||||
private _runningAbortController!: AbortController;
|
|
||||||
|
|
||||||
public constructor(backend: AdbBackend, logger?: AdbLogger) {
|
public constructor(readable: ReadableStream<ArrayBuffer>, writable: WritableStream<ArrayBuffer>, logger?: AdbLogger) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this.backend = backend;
|
|
||||||
this.logger = logger;
|
this.logger = logger;
|
||||||
|
|
||||||
|
readable
|
||||||
|
.pipeThrough(new TransformStream(), { signal: this._abortController.signal })
|
||||||
|
.pipeThrough(new StructDeserializeStream(AdbPacket))
|
||||||
|
.pipeTo(new WritableStream({
|
||||||
|
write: async (packet) => {
|
||||||
|
try {
|
||||||
|
this.logger?.onIncomingPacket?.(packet);
|
||||||
|
|
||||||
|
switch (packet.command) {
|
||||||
|
case AdbCommand.OK:
|
||||||
|
this.handleOk(packet);
|
||||||
|
return;
|
||||||
|
case AdbCommand.Close:
|
||||||
|
await this.handleClose(packet);
|
||||||
|
return;
|
||||||
|
case AdbCommand.Write:
|
||||||
|
if (this.sockets.has(packet.arg1)) {
|
||||||
|
await this.sockets.get(packet.arg1)!.enqueue(packet.payload!);
|
||||||
|
await this.sendPacket(AdbCommand.OK, packet.arg1, packet.arg0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Maybe the device is responding to a packet of last connection
|
||||||
|
// Just ignore it
|
||||||
|
return;
|
||||||
|
case AdbCommand.Open:
|
||||||
|
await this.handleOpen(packet);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const args: AdbPacketReceivedEventArgs = {
|
||||||
|
handled: false,
|
||||||
|
packet,
|
||||||
|
};
|
||||||
|
this.packetEvent.fire(args);
|
||||||
|
if (!args.handled) {
|
||||||
|
this.dispose();
|
||||||
|
throw new Error(`Unhandled packet with command '${packet.command}'`);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
readable.cancel(e);
|
||||||
|
writable.abort(e);
|
||||||
|
this.errorEvent.fire(e as Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
this._packetSerializeStream = new AdbPacketSerializeStream();
|
||||||
|
this._packetSerializeStream.readable.pipeTo(writable, { signal: this._abortController.signal });
|
||||||
|
this._packetSerializeStreamWriter = this._packetSerializeStream.writable.getWriter();
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleOk(packet: AdbPacket) {
|
private handleOk(packet: AdbPacket) {
|
||||||
|
@ -143,70 +188,6 @@ export class AdbPacketDispatcher extends AutoDisposable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public start() {
|
|
||||||
this._running = true;
|
|
||||||
this._runningAbortController = new AbortController();
|
|
||||||
|
|
||||||
this.backend.readable!
|
|
||||||
.pipeThrough(
|
|
||||||
new StructDeserializeStream(AdbPacket),
|
|
||||||
{
|
|
||||||
preventAbort: true,
|
|
||||||
preventCancel: true,
|
|
||||||
signal: this._runningAbortController.signal,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.pipeTo(new WritableStream({
|
|
||||||
write: async (packet) => {
|
|
||||||
try {
|
|
||||||
this.logger?.onIncomingPacket?.(packet);
|
|
||||||
|
|
||||||
switch (packet.command) {
|
|
||||||
case AdbCommand.OK:
|
|
||||||
this.handleOk(packet);
|
|
||||||
return;
|
|
||||||
case AdbCommand.Close:
|
|
||||||
await this.handleClose(packet);
|
|
||||||
return;
|
|
||||||
case AdbCommand.Write:
|
|
||||||
if (this.sockets.has(packet.arg1)) {
|
|
||||||
await this.sockets.get(packet.arg1)!.enqueue(packet.payload!);
|
|
||||||
await this.sendPacket(AdbCommand.OK, packet.arg1, packet.arg0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Maybe the device is responding to a packet of last connection
|
|
||||||
// Just ignore it
|
|
||||||
return;
|
|
||||||
case AdbCommand.Open:
|
|
||||||
await this.handleOpen(packet);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const args: AdbPacketReceivedEventArgs = {
|
|
||||||
handled: false,
|
|
||||||
packet,
|
|
||||||
};
|
|
||||||
this.packetEvent.fire(args);
|
|
||||||
if (!args.handled) {
|
|
||||||
this.dispose();
|
|
||||||
throw new Error(`Unhandled packet with command '${packet.command}'`);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
if (!this._running) {
|
|
||||||
// ignore error when not running
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.errorEvent.fire(e as Error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
this._packetSerializeStream = new AdbPacketSerializeStream();
|
|
||||||
this._packetSerializeStream.readable.pipeTo(this.backend.writable!);
|
|
||||||
this._packetSerializeStreamWriter = this._packetSerializeStream.writable.getWriter();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async createSocket(serviceString: string): Promise<AdbSocket> {
|
public async createSocket(serviceString: string): Promise<AdbSocket> {
|
||||||
if (this.appendNullToServiceString) {
|
if (this.appendNullToServiceString) {
|
||||||
serviceString += '\0';
|
serviceString += '\0';
|
||||||
|
@ -263,14 +244,12 @@ export class AdbPacketDispatcher extends AutoDisposable {
|
||||||
}
|
}
|
||||||
|
|
||||||
public override dispose() {
|
public override dispose() {
|
||||||
this._running = false;
|
|
||||||
this._runningAbortController.abort();
|
|
||||||
|
|
||||||
for (const socket of this.sockets.values()) {
|
for (const socket of this.sockets.values()) {
|
||||||
socket.dispose();
|
socket.dispose();
|
||||||
}
|
}
|
||||||
this.sockets.clear();
|
this.sockets.clear();
|
||||||
|
|
||||||
|
this._abortController.abort();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,6 @@ import { AdbSocketController, AdbSocketInfo } from './controller';
|
||||||
export class AdbSocket implements AdbSocketInfo {
|
export class AdbSocket implements AdbSocketInfo {
|
||||||
private readonly controller: AdbSocketController;
|
private readonly controller: AdbSocketController;
|
||||||
|
|
||||||
public get backend() { return this.controller.backend; }
|
|
||||||
public get localId() { return this.controller.localId; }
|
public get localId() { return this.controller.localId; }
|
||||||
public get remoteId() { return this.controller.remoteId; }
|
public get remoteId() { return this.controller.remoteId; }
|
||||||
public get localCreated() { return this.controller.localCreated; }
|
public get localCreated() { return this.controller.localCreated; }
|
||||||
|
|
|
@ -104,7 +104,6 @@ export class AdbBufferedStream
|
||||||
implements AdbSocketInfo, StructAsyncDeserializeStream {
|
implements AdbSocketInfo, StructAsyncDeserializeStream {
|
||||||
protected readonly socket: AdbSocket;
|
protected readonly socket: AdbSocket;
|
||||||
|
|
||||||
public get backend() { return this.socket.backend; }
|
|
||||||
public get localId() { return this.socket.localId; }
|
public get localId() { return this.socket.localId; }
|
||||||
public get remoteId() { return this.socket.remoteId; }
|
public get remoteId() { return this.socket.remoteId; }
|
||||||
public get localCreated() { return this.socket.localCreated; }
|
public get localCreated() { return this.socket.localCreated; }
|
||||||
|
|
|
@ -1,11 +1,9 @@
|
||||||
import { Adb, AdbBufferedStream, AdbNoneSubprocessProtocol, AdbSubprocessProtocol, ReadableStream, TransformStream } from '@yume-chan/adb';
|
import { Adb, AdbBufferedStream, AdbNoneSubprocessProtocol, AdbSocket, AdbSubprocessProtocol, DecodeUtf8Stream, ReadableStream, TransformStream, WritableStreamDefaultWriter } from '@yume-chan/adb';
|
||||||
import { EventEmitter } from '@yume-chan/event';
|
import { EventEmitter } from '@yume-chan/event';
|
||||||
import Struct from '@yume-chan/struct';
|
import Struct from '@yume-chan/struct';
|
||||||
import type { H264EncodingInfo } from "./decoder";
|
|
||||||
import { AndroidMotionEventAction, ScrcpyControlMessageType, ScrcpyInjectKeyCodeControlMessage, ScrcpyInjectTextControlMessage, ScrcpyInjectTouchControlMessage, type AndroidKeyEventAction } from './message';
|
import { AndroidMotionEventAction, ScrcpyControlMessageType, ScrcpyInjectKeyCodeControlMessage, ScrcpyInjectTextControlMessage, ScrcpyInjectTouchControlMessage, type AndroidKeyEventAction } from './message';
|
||||||
import type { ScrcpyInjectScrollControlMessage1_22, ScrcpyOptions } from "./options";
|
import type { ScrcpyInjectScrollControlMessage1_22, ScrcpyOptions, VideoStreamPacket } from "./options";
|
||||||
import { pushServer, PushServerOptions } from "./push-server";
|
import { pushServer, PushServerOptions } from "./push-server";
|
||||||
import { decodeUtf8 } from "./utils";
|
|
||||||
|
|
||||||
function* splitLines(text: string): Generator<string, void, void> {
|
function* splitLines(text: string): Generator<string, void, void> {
|
||||||
let start = 0;
|
let start = 0;
|
||||||
|
@ -49,10 +47,9 @@ export class ScrcpyClient {
|
||||||
// Disable control for faster connection in 1.22+
|
// Disable control for faster connection in 1.22+
|
||||||
options.value.control = false;
|
options.value.control = false;
|
||||||
|
|
||||||
const client = new ScrcpyClient(device);
|
|
||||||
// Scrcpy server will open connections, before initializing encoder
|
// Scrcpy server will open connections, before initializing encoder
|
||||||
// Thus although an invalid encoder name is given, the start process will success
|
// Thus although an invalid encoder name is given, the start process will success
|
||||||
await client.start(path, version, options);
|
const client = await ScrcpyClient.start(device, path, version, options);
|
||||||
|
|
||||||
const encoderNameRegex = options.getOutputEncoderNameRegex();
|
const encoderNameRegex = options.getOutputEncoderNameRegex();
|
||||||
const encoders: string[] = [];
|
const encoders: string[] = [];
|
||||||
|
@ -68,70 +65,19 @@ export class ScrcpyClient {
|
||||||
return encoders;
|
return encoders;
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly device: Adb;
|
public static async start(
|
||||||
|
device: Adb,
|
||||||
public get backend() { return this.device.backend; }
|
|
||||||
|
|
||||||
private process: AdbSubprocessProtocol | undefined;
|
|
||||||
|
|
||||||
private controlStream: AdbBufferedStream | undefined;
|
|
||||||
|
|
||||||
private _stdout: TransformStream<ArrayBuffer, string>;
|
|
||||||
public get stdout() { return this._stdout.readable; }
|
|
||||||
|
|
||||||
public get exit() { return this.process?.exit; }
|
|
||||||
|
|
||||||
private _running = false;
|
|
||||||
public get running() { return this._running; }
|
|
||||||
|
|
||||||
private _screenWidth: number | undefined;
|
|
||||||
public get screenWidth() { return this._screenWidth; }
|
|
||||||
|
|
||||||
private _screenHeight: number | undefined;
|
|
||||||
public get screenHeight() { return this._screenHeight; }
|
|
||||||
|
|
||||||
private readonly encodingChangedEvent = new EventEmitter<H264EncodingInfo>();
|
|
||||||
public get onEncodingChanged() { return this.encodingChangedEvent.event; }
|
|
||||||
|
|
||||||
private _videoStream: ReadableStream<ArrayBuffer> | undefined;
|
|
||||||
public get onVideoData() { return this._videoStream; }
|
|
||||||
|
|
||||||
private readonly clipboardChangeEvent = new EventEmitter<string>();
|
|
||||||
public get onClipboardChange() { return this.clipboardChangeEvent.event; }
|
|
||||||
|
|
||||||
private options: ScrcpyOptions<any> | undefined;
|
|
||||||
private sendingTouchMessage = false;
|
|
||||||
|
|
||||||
public constructor(device: Adb) {
|
|
||||||
this.device = device;
|
|
||||||
|
|
||||||
this._stdout = new TransformStream<ArrayBuffer, string>({
|
|
||||||
transform(chunk, controller) {
|
|
||||||
const text = decodeUtf8(chunk);
|
|
||||||
for (const line of splitLines(text)) {
|
|
||||||
if (line === '') {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
controller.enqueue(line);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public async start(
|
|
||||||
path: string,
|
path: string,
|
||||||
version: string,
|
version: string,
|
||||||
options: ScrcpyOptions<any>
|
options: ScrcpyOptions<any>
|
||||||
) {
|
) {
|
||||||
this.options = options;
|
const connection = options.createConnection(device);
|
||||||
|
|
||||||
const connection = options.createConnection(this.device);
|
|
||||||
let process: AdbSubprocessProtocol | undefined;
|
let process: AdbSubprocessProtocol | undefined;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await connection.initialize();
|
await connection.initialize();
|
||||||
|
|
||||||
process = await this.device.subprocess.spawn(
|
process = await device.subprocess.spawn(
|
||||||
[
|
[
|
||||||
// cspell: disable-next-line
|
// cspell: disable-next-line
|
||||||
`CLASSPATH=${path}`,
|
`CLASSPATH=${path}`,
|
||||||
|
@ -148,8 +94,6 @@ export class ScrcpyClient {
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
process.stdout.pipeThrough(this._stdout);
|
|
||||||
|
|
||||||
const result = await Promise.race([
|
const result = await Promise.race([
|
||||||
process.exit,
|
process.exit,
|
||||||
connection.getStreams(),
|
connection.getStreams(),
|
||||||
|
@ -160,15 +104,7 @@ export class ScrcpyClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
const [videoStream, controlStream] = result;
|
const [videoStream, controlStream] = result;
|
||||||
|
return new ScrcpyClient(device, options, process, videoStream, controlStream);
|
||||||
this.process = process;
|
|
||||||
this.process.exit.then(() => this.handleProcessClosed());
|
|
||||||
this.videoStream = videoStream;
|
|
||||||
this.controlStream = controlStream;
|
|
||||||
|
|
||||||
this._running = true;
|
|
||||||
this.receiveVideo();
|
|
||||||
this.receiveControl();
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
await process?.kill();
|
await process?.kill();
|
||||||
throw e;
|
throw e;
|
||||||
|
@ -177,69 +113,92 @@ export class ScrcpyClient {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleProcessClosed() {
|
private options: ScrcpyOptions<any>;
|
||||||
this._running = false;
|
private process: AdbSubprocessProtocol;
|
||||||
}
|
|
||||||
|
|
||||||
private async receiveVideo() {
|
private _stdout: TransformStream<string, string>;
|
||||||
if (!this.videoStream) {
|
public get stdout() { return this._stdout.readable; }
|
||||||
throw new Error('receiveVideo started before initialization');
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
public get exit() { return this.process.exit; }
|
||||||
while (this._running) {
|
|
||||||
const { encodingInfo, videoData } = await this.options!.parseVideoStream(this.videoStream);
|
private _screenWidth: number | undefined;
|
||||||
if (encodingInfo) {
|
public get screenWidth() { return this._screenWidth; }
|
||||||
this._screenWidth = encodingInfo.croppedWidth;
|
|
||||||
this._screenHeight = encodingInfo.croppedHeight;
|
private _screenHeight: number | undefined;
|
||||||
this.encodingChangedEvent.fire(encodingInfo);
|
public get screenHeight() { return this._screenHeight; }
|
||||||
|
|
||||||
|
private _videoStream: TransformStream<VideoStreamPacket, VideoStreamPacket>;
|
||||||
|
public get videoStream() { return this._videoStream; }
|
||||||
|
|
||||||
|
private _controlStreamWriter: WritableStreamDefaultWriter<ArrayBuffer> | undefined;
|
||||||
|
|
||||||
|
private readonly clipboardChangeEvent = new EventEmitter<string>();
|
||||||
|
public get onClipboardChange() { return this.clipboardChangeEvent.event; }
|
||||||
|
|
||||||
|
private sendingTouchMessage = false;
|
||||||
|
|
||||||
|
public constructor(
|
||||||
|
device: Adb,
|
||||||
|
options: ScrcpyOptions<any>,
|
||||||
|
process: AdbSubprocessProtocol,
|
||||||
|
videoStream: AdbBufferedStream,
|
||||||
|
controlStream: AdbSocket | undefined,
|
||||||
|
) {
|
||||||
|
this.options = options;
|
||||||
|
this.process = process;
|
||||||
|
|
||||||
|
this._stdout = new TransformStream({
|
||||||
|
transform(chunk, controller) {
|
||||||
|
for (const line of splitLines(chunk)) {
|
||||||
|
if (line === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
controller.enqueue(line);
|
||||||
}
|
}
|
||||||
if (videoData) {
|
},
|
||||||
this.videoDataEvent.fire(videoData);
|
});
|
||||||
}
|
process.stdout
|
||||||
}
|
.pipeThrough(new DecodeUtf8Stream())
|
||||||
} catch (e) {
|
.pipeThrough(this._stdout);
|
||||||
if (!this._running) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async receiveControl() {
|
this._videoStream = new TransformStream();
|
||||||
if (!this.controlStream) {
|
const videoStreamWriter = this._videoStream.writable.getWriter();
|
||||||
// control disabled
|
(async () => {
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
while (true) {
|
while (true) {
|
||||||
const type = await this.controlStream.read(1);
|
const packet = await options.parseVideoStream(videoStream);
|
||||||
switch (new Uint8Array(type)[0]) {
|
if (packet.type === 'configuration') {
|
||||||
case 0:
|
this._screenWidth = packet.data.croppedWidth;
|
||||||
const { content } = await ClipboardMessage.deserialize(this.controlStream);
|
this._screenHeight = packet.data.croppedHeight;
|
||||||
this.clipboardChangeEvent.fire(content!);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new Error('unknown control message type');
|
|
||||||
}
|
}
|
||||||
|
videoStreamWriter.write(packet);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
})();
|
||||||
if (!this._running) {
|
|
||||||
return;
|
if (controlStream) {
|
||||||
}
|
const buffered = new AdbBufferedStream(controlStream);
|
||||||
|
this._controlStreamWriter = controlStream.writable.getWriter();
|
||||||
|
(async () => {
|
||||||
|
while (true) {
|
||||||
|
const type = await buffered.read(1);
|
||||||
|
switch (new Uint8Array(type)[0]) {
|
||||||
|
case 0:
|
||||||
|
const { content } = await ClipboardMessage.deserialize(buffered);
|
||||||
|
this.clipboardChangeEvent.fire(content!);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error('unknown control message type');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private checkControlStream(caller: string) {
|
private checkControlStream(caller: string) {
|
||||||
if (!this._running) {
|
if (!this._controlStreamWriter) {
|
||||||
throw new Error(`${caller} called before start`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.controlStream) {
|
|
||||||
throw new Error(`${caller} called with control disabled`);
|
throw new Error(`${caller} called with control disabled`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.controlStream;
|
return this._controlStreamWriter;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async injectKeyCode(message: Omit<ScrcpyInjectKeyCodeControlMessage, 'type'>) {
|
public async injectKeyCode(message: Omit<ScrcpyInjectKeyCodeControlMessage, 'type'>) {
|
||||||
|
@ -314,18 +273,7 @@ export class ScrcpyClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
public async close() {
|
public async close() {
|
||||||
if (!this._running) {
|
this._controlStreamWriter?.close();
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this._running = false;
|
|
||||||
|
|
||||||
this.videoStream?.close();
|
|
||||||
this.videoStream = undefined;
|
|
||||||
|
|
||||||
this.controlStream?.close();
|
|
||||||
this.controlStream = undefined;
|
|
||||||
|
|
||||||
await this.process?.kill();
|
await this.process?.kill();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,17 +29,17 @@ export abstract class ScrcpyClientConnection implements Disposable {
|
||||||
|
|
||||||
public initialize(): ValueOrPromise<void> { }
|
public initialize(): ValueOrPromise<void> { }
|
||||||
|
|
||||||
public abstract getStreams(): ValueOrPromise<[videoSteam: AdbBufferedStream, controlStream: AdbBufferedStream | undefined]>;
|
public abstract getStreams(): ValueOrPromise<[videoSteam: AdbBufferedStream, controlStream: AdbSocket | undefined]>;
|
||||||
|
|
||||||
public dispose(): void { }
|
public dispose(): void { }
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ScrcpyClientForwardConnection extends ScrcpyClientConnection {
|
export class ScrcpyClientForwardConnection extends ScrcpyClientConnection {
|
||||||
private async connect(): Promise<AdbBufferedStream> {
|
private async connect(): Promise<AdbSocket> {
|
||||||
return new AdbBufferedStream(await this.device.createSocket('localabstract:scrcpy'));
|
return await this.device.createSocket('localabstract:scrcpy');
|
||||||
}
|
}
|
||||||
|
|
||||||
private async connectAndRetry(): Promise<AdbBufferedStream> {
|
private async connectAndRetry(): Promise<AdbSocket> {
|
||||||
for (let i = 0; i < 100; i++) {
|
for (let i = 0; i < 100; i++) {
|
||||||
try {
|
try {
|
||||||
return await this.connect();
|
return await this.connect();
|
||||||
|
@ -51,7 +51,8 @@ export class ScrcpyClientForwardConnection extends ScrcpyClientConnection {
|
||||||
}
|
}
|
||||||
|
|
||||||
private async connectVideoStream(): Promise<AdbBufferedStream> {
|
private async connectVideoStream(): Promise<AdbBufferedStream> {
|
||||||
const stream = await this.connectAndRetry();
|
const socket = await this.connectAndRetry();
|
||||||
|
const stream = new AdbBufferedStream(socket);
|
||||||
if (this.options.sendDummyByte) {
|
if (this.options.sendDummyByte) {
|
||||||
// server will write a `0` to signal connection success
|
// server will write a `0` to signal connection success
|
||||||
await stream.read(1);
|
await stream.read(1);
|
||||||
|
@ -59,9 +60,9 @@ export class ScrcpyClientForwardConnection extends ScrcpyClientConnection {
|
||||||
return stream;
|
return stream;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getStreams(): Promise<[videoSteam: AdbBufferedStream, controlStream: AdbBufferedStream | undefined]> {
|
public async getStreams(): Promise<[videoSteam: AdbBufferedStream, controlStream: AdbSocket | undefined]> {
|
||||||
const videoStream = await this.connectVideoStream();
|
const videoStream = await this.connectVideoStream();
|
||||||
let controlStream: AdbBufferedStream | undefined;
|
let controlStream: AdbSocket | undefined;
|
||||||
if (this.options.control) {
|
if (this.options.control) {
|
||||||
controlStream = await this.connectAndRetry();
|
controlStream = await this.connectAndRetry();
|
||||||
}
|
}
|
||||||
|
@ -96,13 +97,13 @@ export class ScrcpyClientReverseConnection extends ScrcpyClientConnection {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async accept(): Promise<AdbBufferedStream> {
|
private async accept(): Promise<AdbSocket> {
|
||||||
return new AdbBufferedStream((await this.streams.read()).value!);
|
return (await this.streams.read()).value!;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getStreams(): Promise<[videoSteam: AdbBufferedStream, controlStream: AdbBufferedStream | undefined]> {
|
public async getStreams(): Promise<[videoSteam: AdbBufferedStream, controlStream: AdbSocket | undefined]> {
|
||||||
const videoStream = await this.accept();
|
const videoStream = new AdbBufferedStream(await this.accept());
|
||||||
let controlStream: AdbBufferedStream | undefined;
|
let controlStream: AdbSocket | undefined;
|
||||||
if (this.options.control) {
|
if (this.options.control) {
|
||||||
controlStream = await this.accept();
|
controlStream = await this.accept();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import type { Disposable } from "@yume-chan/event";
|
|
||||||
import type { WritableStream } from '@yume-chan/adb';
|
import type { WritableStream } from '@yume-chan/adb';
|
||||||
|
import type { Disposable } from "@yume-chan/event";
|
||||||
import type { AndroidCodecLevel, AndroidCodecProfile } from "../codec";
|
import type { AndroidCodecLevel, AndroidCodecProfile } from "../codec";
|
||||||
|
|
||||||
export interface H264EncodingInfo {
|
export interface H264Configuration {
|
||||||
profileIndex: number;
|
profileIndex: number;
|
||||||
constraintSet: number;
|
constraintSet: number;
|
||||||
levelIndex: number;
|
levelIndex: number;
|
||||||
|
@ -29,7 +29,7 @@ export interface H264Decoder extends Disposable {
|
||||||
|
|
||||||
readonly writable: WritableStream<ArrayBuffer>;
|
readonly writable: WritableStream<ArrayBuffer>;
|
||||||
|
|
||||||
changeEncoding(size: H264EncodingInfo): void;
|
configure(config: H264Configuration): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface H264DecoderConstructor {
|
export interface H264DecoderConstructor {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { WritableStream } from "@yume-chan/adb";
|
import { WritableStream } from "@yume-chan/adb";
|
||||||
import { PromiseResolver } from "@yume-chan/async";
|
import { PromiseResolver } from "@yume-chan/async";
|
||||||
import { AndroidCodecLevel, AndroidCodecProfile } from "../../codec";
|
import { AndroidCodecLevel, AndroidCodecProfile } from "../../codec";
|
||||||
import type { H264Decoder, H264EncodingInfo } from '../common';
|
import type { H264Configuration, H264Decoder } from '../common';
|
||||||
import { createTinyH264Wrapper, TinyH264Wrapper } from "./wrapper";
|
import { createTinyH264Wrapper, TinyH264Wrapper } from "./wrapper";
|
||||||
|
|
||||||
let cachedInitializePromise: Promise<{ YuvBuffer: typeof import('yuv-buffer'), YuvCanvas: typeof import('yuv-canvas').default; }> | undefined;
|
let cachedInitializePromise: Promise<{ YuvBuffer: typeof import('yuv-buffer'), YuvCanvas: typeof import('yuv-canvas').default; }> | undefined;
|
||||||
|
@ -45,7 +45,7 @@ export class TinyH264Decoder implements H264Decoder {
|
||||||
this._renderer = document.createElement('canvas');
|
this._renderer = document.createElement('canvas');
|
||||||
}
|
}
|
||||||
|
|
||||||
public async changeEncoding(size: H264EncodingInfo) {
|
public async configure(config: H264Configuration) {
|
||||||
this.dispose();
|
this.dispose();
|
||||||
|
|
||||||
this._initializer = new PromiseResolver<TinyH264Wrapper>();
|
this._initializer = new PromiseResolver<TinyH264Wrapper>();
|
||||||
|
@ -55,23 +55,23 @@ export class TinyH264Decoder implements H264Decoder {
|
||||||
this._yuvCanvas = YuvCanvas.attach(this._renderer);;
|
this._yuvCanvas = YuvCanvas.attach(this._renderer);;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { encodedWidth, encodedHeight } = size;
|
const { encodedWidth, encodedHeight } = config;
|
||||||
const chromaWidth = encodedWidth / 2;
|
const chromaWidth = encodedWidth / 2;
|
||||||
const chromaHeight = encodedHeight / 2;
|
const chromaHeight = encodedHeight / 2;
|
||||||
|
|
||||||
this._renderer.width = size.croppedWidth;
|
this._renderer.width = config.croppedWidth;
|
||||||
this._renderer.height = size.croppedHeight;
|
this._renderer.height = config.croppedHeight;
|
||||||
const format = YuvBuffer.format({
|
const format = YuvBuffer.format({
|
||||||
width: encodedWidth,
|
width: encodedWidth,
|
||||||
height: encodedHeight,
|
height: encodedHeight,
|
||||||
chromaWidth,
|
chromaWidth,
|
||||||
chromaHeight,
|
chromaHeight,
|
||||||
cropLeft: size.cropLeft,
|
cropLeft: config.cropLeft,
|
||||||
cropTop: size.cropTop,
|
cropTop: config.cropTop,
|
||||||
cropWidth: size.croppedWidth,
|
cropWidth: config.croppedWidth,
|
||||||
cropHeight: size.croppedHeight,
|
cropHeight: config.croppedHeight,
|
||||||
displayWidth: size.croppedWidth,
|
displayWidth: config.croppedWidth,
|
||||||
displayHeight: size.croppedHeight,
|
displayHeight: config.croppedHeight,
|
||||||
});
|
});
|
||||||
|
|
||||||
const wrapper = await createTinyH264Wrapper();
|
const wrapper = await createTinyH264Wrapper();
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import type { ValueOrPromise } from "@yume-chan/struct";
|
import type { ValueOrPromise } from "@yume-chan/struct";
|
||||||
import { AndroidCodecLevel, AndroidCodecProfile } from "../../codec";
|
import { AndroidCodecLevel, AndroidCodecProfile } from "../../codec";
|
||||||
import type { H264Decoder, H264EncodingInfo } from "../common";
|
import type { H264Configuration, H264Decoder } from "../common";
|
||||||
|
|
||||||
function toHex(value: number) {
|
function toHex(value: number) {
|
||||||
return value.toString(16).padStart(2, '0').toUpperCase();
|
return value.toString(16).padStart(2, '0').toUpperCase();
|
||||||
|
@ -41,11 +41,11 @@ export class WebCodecsDecoder implements H264Decoder {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public changeEncoding(encoding: H264EncodingInfo): ValueOrPromise<void> {
|
public configure(config: H264Configuration): ValueOrPromise<void> {
|
||||||
const { profileIndex, constraintSet, levelIndex } = encoding;
|
const { profileIndex, constraintSet, levelIndex } = config;
|
||||||
|
|
||||||
this._renderer.width = encoding.croppedWidth;
|
this._renderer.width = config.croppedWidth;
|
||||||
this._renderer.height = encoding.croppedHeight;
|
this._renderer.height = config.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
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { ScrcpyClientConnection, ScrcpyClientConnectionOptions, ScrcpyClientForw
|
||||||
import { AndroidKeyEventAction, ScrcpyControlMessageType } from "../../message";
|
import { AndroidKeyEventAction, ScrcpyControlMessageType } from "../../message";
|
||||||
import type { ScrcpyBackOrScreenOnEvent1_18 } from "../1_18";
|
import type { ScrcpyBackOrScreenOnEvent1_18 } from "../1_18";
|
||||||
import type { ScrcpyInjectScrollControlMessage1_22 } from "../1_22";
|
import type { ScrcpyInjectScrollControlMessage1_22 } from "../1_22";
|
||||||
import { type ScrcpyOptionValue, toScrcpyOptionValue, VideoStreamPacket, type ScrcpyOptions } from "../common";
|
import { toScrcpyOptionValue, type VideoStreamPacket, type ScrcpyOptions, type ScrcpyOptionValue } from "../common";
|
||||||
import { parse_sequence_parameter_set } from "./sps";
|
import { parse_sequence_parameter_set } from "./sps";
|
||||||
|
|
||||||
export enum ScrcpyLogLevel {
|
export enum ScrcpyLogLevel {
|
||||||
|
@ -216,15 +216,12 @@ export class ScrcpyOptions1_16<T extends ScrcpyOptionsInit1_16 = ScrcpyOptionsIn
|
||||||
public async parseVideoStream(stream: AdbBufferedStream): Promise<VideoStreamPacket> {
|
public async parseVideoStream(stream: AdbBufferedStream): Promise<VideoStreamPacket> {
|
||||||
if (this.value.sendFrameMeta === false) {
|
if (this.value.sendFrameMeta === false) {
|
||||||
return {
|
return {
|
||||||
videoData: await stream.read(1 * 1024 * 1024, true),
|
type: 'frame',
|
||||||
|
data: await stream.read(1 * 1024 * 1024, true),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const { pts, data } = await VideoPacket.deserialize(stream);
|
const { pts, data } = await VideoPacket.deserialize(stream);
|
||||||
if (!data || data.byteLength === 0) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pts === NoPts) {
|
if (pts === NoPts) {
|
||||||
const sequenceParameterSet = parse_sequence_parameter_set(data.slice(0));
|
const sequenceParameterSet = parse_sequence_parameter_set(data.slice(0));
|
||||||
|
|
||||||
|
@ -253,7 +250,8 @@ export class ScrcpyOptions1_16<T extends ScrcpyOptionsInit1_16 = ScrcpyOptionsIn
|
||||||
|
|
||||||
this._streamHeader = data;
|
this._streamHeader = data;
|
||||||
return {
|
return {
|
||||||
encodingInfo: {
|
type: 'configuration',
|
||||||
|
data: {
|
||||||
profileIndex,
|
profileIndex,
|
||||||
constraintSet,
|
constraintSet,
|
||||||
levelIndex,
|
levelIndex,
|
||||||
|
@ -269,18 +267,20 @@ export class ScrcpyOptions1_16<T extends ScrcpyOptionsInit1_16 = ScrcpyOptionsIn
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
let array: Uint8Array;
|
let frameData: ArrayBuffer;
|
||||||
if (this._streamHeader) {
|
if (this._streamHeader) {
|
||||||
array = new Uint8Array(this._streamHeader.byteLength + data!.byteLength);
|
frameData = new ArrayBuffer(this._streamHeader.byteLength + data.byteLength);
|
||||||
|
const array = new Uint8Array(frameData);
|
||||||
array.set(new Uint8Array(this._streamHeader));
|
array.set(new Uint8Array(this._streamHeader));
|
||||||
array.set(new Uint8Array(data!), this._streamHeader.byteLength);
|
array.set(new Uint8Array(data!), this._streamHeader.byteLength);
|
||||||
this._streamHeader = undefined;
|
this._streamHeader = undefined;
|
||||||
} else {
|
} else {
|
||||||
array = new Uint8Array(data!);
|
frameData = data;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
videoData: array.buffer,
|
type: 'frame',
|
||||||
|
data: frameData,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
// cspell: ignore autosync
|
// cspell: ignore autosync
|
||||||
|
|
||||||
import { ScrcpyOptions1_18, ScrcpyOptionsInit1_18 } from './1_18';
|
import { ScrcpyOptions1_18, type ScrcpyOptionsInit1_18 } from './1_18';
|
||||||
import { toScrcpyOptionValue } from "./common";
|
import { toScrcpyOptionValue } from "./common";
|
||||||
|
|
||||||
export interface ScrcpyOptionsInit1_21 extends ScrcpyOptionsInit1_18 {
|
export interface ScrcpyOptionsInit1_21 extends ScrcpyOptionsInit1_18 {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Adb } from "@yume-chan/adb";
|
import type { Adb } from "@yume-chan/adb";
|
||||||
import Struct from "@yume-chan/struct";
|
import Struct from "@yume-chan/struct";
|
||||||
import { ScrcpyClientConnection, ScrcpyClientConnectionOptions, ScrcpyClientForwardConnection, ScrcpyClientReverseConnection } from "../connection";
|
import { type ScrcpyClientConnection, ScrcpyClientConnectionOptions, ScrcpyClientForwardConnection, ScrcpyClientReverseConnection } from "../connection";
|
||||||
import { ScrcpyInjectScrollControlMessage1_16 } from "./1_16";
|
import { ScrcpyInjectScrollControlMessage1_16 } from "./1_16";
|
||||||
import { ScrcpyOptions1_21, type ScrcpyOptionsInit1_21 } from "./1_21";
|
import { ScrcpyOptions1_21, type ScrcpyOptionsInit1_21 } from "./1_21";
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import type { Adb, AdbBufferedStream } from "@yume-chan/adb";
|
import type { Adb, AdbBufferedStream } from "@yume-chan/adb";
|
||||||
import type { ScrcpyClientConnection } from "../connection";
|
import type { ScrcpyClientConnection } from "../connection";
|
||||||
import type { H264EncodingInfo } from "../decoder";
|
import type { H264Configuration } from "../decoder";
|
||||||
import type { ScrcpyBackOrScreenOnEvent1_18 } from "./1_18";
|
import type { ScrcpyBackOrScreenOnEvent1_18 } from "./1_18";
|
||||||
import type { ScrcpyInjectScrollControlMessage1_22 } from "./1_22";
|
import type { ScrcpyInjectScrollControlMessage1_22 } from "./1_22";
|
||||||
|
|
||||||
|
@ -28,12 +28,18 @@ export function toScrcpyOptionValue<T>(value: any, empty: T): string | T {
|
||||||
return `${value}`;
|
return `${value}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface VideoStreamPacket {
|
export interface VideoStreamConfigurationPacket {
|
||||||
encodingInfo?: H264EncodingInfo | undefined;
|
type: 'configuration';
|
||||||
|
data: H264Configuration;
|
||||||
videoData?: ArrayBuffer | undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface VideoStreamFramePacket {
|
||||||
|
type: 'frame';
|
||||||
|
data: ArrayBuffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type VideoStreamPacket = VideoStreamConfigurationPacket | VideoStreamFramePacket;
|
||||||
|
|
||||||
export interface ScrcpyOptions<T> {
|
export interface ScrcpyOptions<T> {
|
||||||
value: Partial<T>;
|
value: Partial<T>;
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue