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
|
||||
|
||||
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'
|
||||
version: 0.0.0
|
||||
dependencies:
|
||||
'@yume-chan/async': 2.1.4
|
||||
jest: 26.6.3
|
||||
tslib: 2.3.1
|
||||
typescript: 4.5.5
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
- canvas
|
||||
- supports-color
|
||||
- ts-node
|
||||
- utf-8-validate
|
||||
dev: false
|
||||
|
||||
file:projects/adb-credential-web.tgz:
|
||||
|
@ -13094,7 +13101,7 @@ packages:
|
|||
dev: false
|
||||
|
||||
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'
|
||||
version: 0.0.0
|
||||
dependencies:
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { AdbBackend, ReadableStream, TransformStream, WritableStream } from '@yume-chan/adb';
|
||||
import { EventEmitter } from '@yume-chan/event';
|
||||
import { AdbBackend, ReadableStream, ReadableWritablePair, TransformStream, WritableStream } from '@yume-chan/adb';
|
||||
|
||||
declare global {
|
||||
interface TCPSocket {
|
||||
|
@ -30,20 +29,8 @@ declare global {
|
|||
}
|
||||
}
|
||||
|
||||
export default class AdbDirectSocketsBackend implements AdbBackend {
|
||||
public static isSupported(): boolean {
|
||||
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;
|
||||
export class AdbDirectSocketsBackendStreams implements ReadableWritablePair<ArrayBuffer, ArrayBuffer>{
|
||||
private socket: TCPSocket;
|
||||
|
||||
private _readableTransformStream = new TransformStream<Uint8Array, ArrayBuffer>({
|
||||
transform(chunk, controller) {
|
||||
|
@ -56,44 +43,43 @@ export default class AdbDirectSocketsBackend implements AdbBackend {
|
|||
return this._readableTransformStream.readable;
|
||||
}
|
||||
|
||||
public get writable(): WritableStream<ArrayBuffer> | undefined {
|
||||
return this.socket?.writable;
|
||||
public get writable(): WritableStream<ArrayBuffer> {
|
||||
return this.socket.writable;
|
||||
}
|
||||
|
||||
private _connected = false;
|
||||
public get connected() { return this._connected; }
|
||||
constructor(socket: TCPSocket) {
|
||||
this.socket = socket;
|
||||
this.socket.readable.pipeTo(this._readableTransformStream.writable);
|
||||
}
|
||||
}
|
||||
|
||||
private readonly disconnectEvent = new EventEmitter<void>();
|
||||
public readonly onDisconnected = this.disconnectEvent.event;
|
||||
export default class AdbDirectSocketsBackend implements AdbBackend {
|
||||
public static isSupported(): boolean {
|
||||
return typeof window !== 'undefined' && !!window.navigator?.openTCPSocket;
|
||||
}
|
||||
|
||||
public constructor(address: string, port: number = 5555, name?: string) {
|
||||
this.address = address;
|
||||
public readonly serial: string;
|
||||
|
||||
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.serial = `${address}:${port}`;
|
||||
this.serial = `${host}:${port}`;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public async connect() {
|
||||
const socket = await navigator.openTCPSocket({
|
||||
remoteAddress: this.address,
|
||||
remoteAddress: this.host,
|
||||
remotePort: this.port,
|
||||
noDelay: true,
|
||||
});
|
||||
|
||||
this.socket = 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;
|
||||
return new AdbDirectSocketsBackendStreams(socket);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { AdbBackend, ReadableStream } from '@yume-chan/adb';
|
||||
import { EventEmitter } from '@yume-chan/event';
|
||||
import { AdbBackend, ReadableStream, ReadableWritablePair } from '@yume-chan/adb';
|
||||
|
||||
export const WebUsbDeviceFilter: USBDeviceFilter = {
|
||||
classCode: 0xFF,
|
||||
|
@ -7,6 +6,49 @@ export const WebUsbDeviceFilter: USBDeviceFilter = {
|
|||
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 {
|
||||
public static isSupported(): boolean {
|
||||
return !!window.navigator?.usb;
|
||||
|
@ -37,31 +79,11 @@ export class AdbWebUsbBackend implements AdbBackend {
|
|||
|
||||
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) {
|
||||
this._device = device;
|
||||
window.navigator.usb.addEventListener('disconnect', this.handleDisconnect);
|
||||
}
|
||||
|
||||
private handleDisconnect = (e: USBConnectionEvent) => {
|
||||
if (e.device === this._device) {
|
||||
this._connected = false;
|
||||
this.disconnectEvent.fire();
|
||||
}
|
||||
};
|
||||
|
||||
public async connect(): Promise<void> {
|
||||
public async connect() {
|
||||
if (!this._device.opened) {
|
||||
await this._device.open();
|
||||
}
|
||||
|
@ -84,49 +106,21 @@ export class AdbWebUsbBackend implements AdbBackend {
|
|||
await this._device.selectAlternateInterface(interface_.interfaceNumber, alternate.alternateSetting);
|
||||
}
|
||||
|
||||
let inEndpoint: USBEndpoint | undefined;
|
||||
let outEndpoint: USBEndpoint | undefined;
|
||||
|
||||
for (const endpoint of alternate.endpoints) {
|
||||
switch (endpoint.direction) {
|
||||
case 'in':
|
||||
this._readable = new ReadableStream({
|
||||
pull: async (controller) => {
|
||||
let result = await this._device.transferIn(endpoint.endpointNumber, endpoint.packetSize);
|
||||
|
||||
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;
|
||||
inEndpoint = endpoint;
|
||||
if (outEndpoint) {
|
||||
return new AdbWebUsbBackendStream(this._device, inEndpoint, outEndpoint);
|
||||
}
|
||||
break;
|
||||
case 'out':
|
||||
this._writable = new WritableStream({
|
||||
write: async (chunk) => {
|
||||
await this._device.transferOut(endpoint.endpointNumber, chunk);
|
||||
},
|
||||
close: () => {
|
||||
this.dispose();
|
||||
},
|
||||
}, {
|
||||
highWaterMark: 16 * 1024,
|
||||
size(chunk) { return chunk.byteLength; },
|
||||
});
|
||||
if (this.readable !== undefined) {
|
||||
this._connected = true;
|
||||
return;
|
||||
outEndpoint = endpoint;
|
||||
if (inEndpoint) {
|
||||
return new AdbWebUsbBackendStream(this._device, inEndpoint, outEndpoint);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
@ -138,11 +132,4 @@ export class AdbWebUsbBackend implements AdbBackend {
|
|||
|
||||
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",
|
||||
"compilerOptions": {
|
||||
"target": "ES2016",
|
||||
"lib": [
|
||||
"ESNext",
|
||||
"DOM"
|
||||
|
|
|
@ -30,6 +30,7 @@
|
|||
"build:watch": "build-ts-package --incremental"
|
||||
},
|
||||
"devDependencies": {
|
||||
"jest": "^26.6.3",
|
||||
"typescript": "^4.5.5",
|
||||
"@yume-chan/ts-package-builder": "^1.0.0"
|
||||
},
|
||||
|
|
|
@ -1,37 +1,11 @@
|
|||
import { AdbBackend, ReadableStream, WritableStream } from '@yume-chan/adb';
|
||||
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 {
|
||||
public readonly serial: string;
|
||||
|
||||
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) {
|
||||
this.serial = url;
|
||||
this.name = name;
|
||||
|
@ -48,22 +22,21 @@ export default class AdbWsBackend implements AdbBackend {
|
|||
};
|
||||
await resolver.promise;
|
||||
|
||||
this._readable = new ReadableStream({
|
||||
const readable = new ReadableStream({
|
||||
start: (controller) => {
|
||||
socket.onmessage = ({ data }: { data: ArrayBuffer; }) => {
|
||||
controller.enqueue(data);
|
||||
};
|
||||
socket.onclose = () => {
|
||||
controller.close();
|
||||
this._connected = false;
|
||||
this.disconnectEvent.fire();
|
||||
};
|
||||
}
|
||||
}, {
|
||||
highWaterMark: 16 * 1024,
|
||||
size(chunk) { return chunk.byteLength; },
|
||||
});
|
||||
this._writable = new WritableStream({
|
||||
|
||||
const writable = new WritableStream({
|
||||
write: (chunk) => {
|
||||
socket.send(chunk);
|
||||
},
|
||||
|
@ -72,23 +45,6 @@ export default class AdbWsBackend implements AdbBackend {
|
|||
size(chunk) { return chunk.byteLength; },
|
||||
});
|
||||
|
||||
this.socket = socket;
|
||||
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();
|
||||
return { readable, writable };
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,11 +2,11 @@ import { PromiseResolver } from '@yume-chan/async';
|
|||
import { DisposableList } from '@yume-chan/event';
|
||||
import { AdbAuthenticationHandler, AdbCredentialStore, AdbDefaultAuthenticators } from './auth';
|
||||
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 { AdbCommand } from './packet';
|
||||
import { AdbLogger, AdbPacketDispatcher, AdbSocket } from './socket';
|
||||
import { decodeUtf8, ReadableStream } from "./utils";
|
||||
import { decodeUtf8, ReadableStream, WritableStream } from "./utils";
|
||||
|
||||
export enum AdbPropKey {
|
||||
Product = 'ro.product.name',
|
||||
|
@ -16,17 +16,17 @@ export enum AdbPropKey {
|
|||
}
|
||||
|
||||
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;
|
||||
|
||||
public get backend(): AdbBackend { return this._backend; }
|
||||
|
||||
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; }
|
||||
|
||||
private _protocolVersion: number | undefined;
|
||||
|
@ -49,28 +49,28 @@ export class Adb {
|
|||
public readonly reverse: AdbReverseCommand;
|
||||
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.packetDispatcher = new AdbPacketDispatcher(backend, logger);
|
||||
this.packetDispatcher = new AdbPacketDispatcher(readable, writable, logger);
|
||||
|
||||
this.subprocess = new AdbSubprocess(this);
|
||||
this.power = new AdbPower(this);
|
||||
this.reverse = new AdbReverseCommand(this.packetDispatcher);
|
||||
this.tcpip = new AdbTcpIpCommand(this);
|
||||
|
||||
backend.onDisconnected(this.dispose, this);
|
||||
}
|
||||
|
||||
public async connect(
|
||||
public async authenticate(
|
||||
credentialStore: AdbCredentialStore,
|
||||
authenticators = AdbDefaultAuthenticators
|
||||
): Promise<void> {
|
||||
await this.backend.connect?.();
|
||||
this.packetDispatcher.maxPayloadSize = 0x1000;
|
||||
// TODO: Adb: properly set `calculateChecksum`
|
||||
// this.packetDispatcher.calculateChecksum = true;
|
||||
this.packetDispatcher.calculateChecksum = true;
|
||||
this.packetDispatcher.appendNullToServiceString = true;
|
||||
this.packetDispatcher.start();
|
||||
|
||||
const version = 0x01000001;
|
||||
const versionNoChecksum = 0x01000001;
|
||||
|
@ -140,20 +140,17 @@ export class Adb {
|
|||
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(
|
||||
AdbCommand.Connect,
|
||||
version,
|
||||
maxPayloadSize,
|
||||
`host::features=${features};\0`
|
||||
// The terminating `;` is required in formal definition
|
||||
// But ADB daemon can also work without it
|
||||
`host::features=${features};`
|
||||
);
|
||||
|
||||
try {
|
||||
await resolver.promise;
|
||||
this._connected = true;
|
||||
} finally {
|
||||
disposableList.dispose();
|
||||
}
|
||||
|
@ -236,6 +233,5 @@ export class Adb {
|
|||
|
||||
public async dispose(): Promise<void> {
|
||||
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 { ReadableStream, WritableStream } from "./utils";
|
||||
import type { ReadableWritablePair } from "./utils";
|
||||
|
||||
export interface AdbBackend {
|
||||
readonly serial: string;
|
||||
|
||||
readonly name: string | undefined;
|
||||
|
||||
readonly connected: boolean;
|
||||
|
||||
readonly onDisconnected: Event<void>;
|
||||
|
||||
readonly readable: ReadableStream<ArrayBuffer> | undefined;
|
||||
|
||||
readonly writable: WritableStream<ArrayBuffer> | undefined;
|
||||
|
||||
connect?(): ValueOrPromise<void>;
|
||||
|
||||
dispose(): ValueOrPromise<void>;
|
||||
connect(): ValueOrPromise<ReadableWritablePair<ArrayBuffer, ArrayBuffer>>;
|
||||
}
|
||||
|
|
|
@ -1,14 +1,10 @@
|
|||
import { AutoDisposable } from '@yume-chan/event';
|
||||
import { AdbBackend } from '../backend';
|
||||
import { AdbCommand } from '../packet';
|
||||
import { AutoResetEvent, chunkArrayLike, TransformStream, WritableStream, WritableStreamDefaultWriter } from '../utils';
|
||||
import { AdbPacketDispatcher } from './dispatcher';
|
||||
|
||||
export interface AdbSocketInfo {
|
||||
backend: AdbBackend;
|
||||
|
||||
localId: number;
|
||||
|
||||
remoteId: number;
|
||||
|
||||
localCreated: boolean;
|
||||
|
@ -35,7 +31,6 @@ export class AdbSocketController extends AutoDisposable implements AdbSocketInfo
|
|||
private readonly writeLock = this.addDisposable(new AutoResetEvent());
|
||||
|
||||
private readonly dispatcher!: AdbPacketDispatcher;
|
||||
public get backend() { return this.dispatcher.backend; }
|
||||
|
||||
public readonly localId!: number;
|
||||
public readonly remoteId!: number;
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import { AsyncOperationManager } from '@yume-chan/async';
|
||||
import { AutoDisposable, EventEmitter } from '@yume-chan/event';
|
||||
import { AdbBackend } from '../backend';
|
||||
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 { AdbLogger } from './logger';
|
||||
import { AdbSocket } from './socket';
|
||||
|
@ -32,7 +31,6 @@ export class AdbPacketDispatcher extends AutoDisposable {
|
|||
private readonly sockets = new Map<number, AdbSocketController>();
|
||||
private readonly logger: AdbLogger | undefined;
|
||||
|
||||
public readonly backend: AdbBackend;
|
||||
private _packetSerializeStream!: AdbPacketSerializeStream;
|
||||
private _packetSerializeStreamWriter!: WritableStreamDefaultWriter<AdbPacketInit>;
|
||||
|
||||
|
@ -50,15 +48,62 @@ export class AdbPacketDispatcher extends AutoDisposable {
|
|||
private readonly errorEvent = this.addDisposable(new EventEmitter<Error>());
|
||||
public get onError() { return this.errorEvent.event; }
|
||||
|
||||
private _running = false;
|
||||
public get running() { return this._running; }
|
||||
private _runningAbortController!: AbortController;
|
||||
private _abortController = new AbortController();
|
||||
|
||||
public constructor(backend: AdbBackend, logger?: AdbLogger) {
|
||||
public constructor(readable: ReadableStream<ArrayBuffer>, writable: WritableStream<ArrayBuffer>, logger?: AdbLogger) {
|
||||
super();
|
||||
|
||||
this.backend = backend;
|
||||
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) {
|
||||
|
@ -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> {
|
||||
if (this.appendNullToServiceString) {
|
||||
serviceString += '\0';
|
||||
|
@ -263,14 +244,12 @@ export class AdbPacketDispatcher extends AutoDisposable {
|
|||
}
|
||||
|
||||
public override dispose() {
|
||||
this._running = false;
|
||||
this._runningAbortController.abort();
|
||||
|
||||
for (const socket of this.sockets.values()) {
|
||||
socket.dispose();
|
||||
}
|
||||
this.sockets.clear();
|
||||
|
||||
this._abortController.abort();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,6 @@ import { AdbSocketController, AdbSocketInfo } from './controller';
|
|||
export class AdbSocket implements AdbSocketInfo {
|
||||
private readonly controller: AdbSocketController;
|
||||
|
||||
public get backend() { return this.controller.backend; }
|
||||
public get localId() { return this.controller.localId; }
|
||||
public get remoteId() { return this.controller.remoteId; }
|
||||
public get localCreated() { return this.controller.localCreated; }
|
||||
|
|
|
@ -104,7 +104,6 @@ export class AdbBufferedStream
|
|||
implements AdbSocketInfo, StructAsyncDeserializeStream {
|
||||
protected readonly socket: AdbSocket;
|
||||
|
||||
public get backend() { return this.socket.backend; }
|
||||
public get localId() { return this.socket.localId; }
|
||||
public get remoteId() { return this.socket.remoteId; }
|
||||
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 Struct from '@yume-chan/struct';
|
||||
import type { H264EncodingInfo } from "./decoder";
|
||||
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 { decodeUtf8 } from "./utils";
|
||||
|
||||
function* splitLines(text: string): Generator<string, void, void> {
|
||||
let start = 0;
|
||||
|
@ -49,10 +47,9 @@ export class ScrcpyClient {
|
|||
// Disable control for faster connection in 1.22+
|
||||
options.value.control = false;
|
||||
|
||||
const client = new ScrcpyClient(device);
|
||||
// Scrcpy server will open connections, before initializing encoder
|
||||
// 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 encoders: string[] = [];
|
||||
|
@ -68,70 +65,19 @@ export class ScrcpyClient {
|
|||
return encoders;
|
||||
}
|
||||
|
||||
private readonly 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(
|
||||
public static async start(
|
||||
device: Adb,
|
||||
path: string,
|
||||
version: string,
|
||||
options: ScrcpyOptions<any>
|
||||
) {
|
||||
this.options = options;
|
||||
|
||||
const connection = options.createConnection(this.device);
|
||||
const connection = options.createConnection(device);
|
||||
let process: AdbSubprocessProtocol | undefined;
|
||||
|
||||
try {
|
||||
await connection.initialize();
|
||||
|
||||
process = await this.device.subprocess.spawn(
|
||||
process = await device.subprocess.spawn(
|
||||
[
|
||||
// cspell: disable-next-line
|
||||
`CLASSPATH=${path}`,
|
||||
|
@ -148,8 +94,6 @@ export class ScrcpyClient {
|
|||
}
|
||||
);
|
||||
|
||||
process.stdout.pipeThrough(this._stdout);
|
||||
|
||||
const result = await Promise.race([
|
||||
process.exit,
|
||||
connection.getStreams(),
|
||||
|
@ -160,15 +104,7 @@ export class ScrcpyClient {
|
|||
}
|
||||
|
||||
const [videoStream, controlStream] = result;
|
||||
|
||||
this.process = process;
|
||||
this.process.exit.then(() => this.handleProcessClosed());
|
||||
this.videoStream = videoStream;
|
||||
this.controlStream = controlStream;
|
||||
|
||||
this._running = true;
|
||||
this.receiveVideo();
|
||||
this.receiveControl();
|
||||
return new ScrcpyClient(device, options, process, videoStream, controlStream);
|
||||
} catch (e) {
|
||||
await process?.kill();
|
||||
throw e;
|
||||
|
@ -177,69 +113,92 @@ export class ScrcpyClient {
|
|||
}
|
||||
}
|
||||
|
||||
private handleProcessClosed() {
|
||||
this._running = false;
|
||||
}
|
||||
private options: ScrcpyOptions<any>;
|
||||
private process: AdbSubprocessProtocol;
|
||||
|
||||
private async receiveVideo() {
|
||||
if (!this.videoStream) {
|
||||
throw new Error('receiveVideo started before initialization');
|
||||
}
|
||||
private _stdout: TransformStream<string, string>;
|
||||
public get stdout() { return this._stdout.readable; }
|
||||
|
||||
try {
|
||||
while (this._running) {
|
||||
const { encodingInfo, videoData } = await this.options!.parseVideoStream(this.videoStream);
|
||||
if (encodingInfo) {
|
||||
this._screenWidth = encodingInfo.croppedWidth;
|
||||
this._screenHeight = encodingInfo.croppedHeight;
|
||||
this.encodingChangedEvent.fire(encodingInfo);
|
||||
}
|
||||
if (videoData) {
|
||||
this.videoDataEvent.fire(videoData);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
if (!this._running) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
public get exit() { return this.process.exit; }
|
||||
|
||||
private async receiveControl() {
|
||||
if (!this.controlStream) {
|
||||
// control disabled
|
||||
return;
|
||||
}
|
||||
private _screenWidth: number | undefined;
|
||||
public get screenWidth() { return this._screenWidth; }
|
||||
|
||||
try {
|
||||
private _screenHeight: number | undefined;
|
||||
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);
|
||||
}
|
||||
},
|
||||
});
|
||||
process.stdout
|
||||
.pipeThrough(new DecodeUtf8Stream())
|
||||
.pipeThrough(this._stdout);
|
||||
|
||||
this._videoStream = new TransformStream();
|
||||
const videoStreamWriter = this._videoStream.writable.getWriter();
|
||||
(async () => {
|
||||
while (true) {
|
||||
const type = await this.controlStream.read(1);
|
||||
const packet = await options.parseVideoStream(videoStream);
|
||||
if (packet.type === 'configuration') {
|
||||
this._screenWidth = packet.data.croppedWidth;
|
||||
this._screenHeight = packet.data.croppedHeight;
|
||||
}
|
||||
videoStreamWriter.write(packet);
|
||||
}
|
||||
})();
|
||||
|
||||
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(this.controlStream);
|
||||
const { content } = await ClipboardMessage.deserialize(buffered);
|
||||
this.clipboardChangeEvent.fire(content!);
|
||||
break;
|
||||
default:
|
||||
throw new Error('unknown control message type');
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
if (!this._running) {
|
||||
return;
|
||||
}
|
||||
})();
|
||||
}
|
||||
}
|
||||
|
||||
private checkControlStream(caller: string) {
|
||||
if (!this._running) {
|
||||
throw new Error(`${caller} called before start`);
|
||||
}
|
||||
|
||||
if (!this.controlStream) {
|
||||
if (!this._controlStreamWriter) {
|
||||
throw new Error(`${caller} called with control disabled`);
|
||||
}
|
||||
|
||||
return this.controlStream;
|
||||
return this._controlStreamWriter;
|
||||
}
|
||||
|
||||
public async injectKeyCode(message: Omit<ScrcpyInjectKeyCodeControlMessage, 'type'>) {
|
||||
|
@ -314,18 +273,7 @@ export class ScrcpyClient {
|
|||
}
|
||||
|
||||
public async close() {
|
||||
if (!this._running) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._running = false;
|
||||
|
||||
this.videoStream?.close();
|
||||
this.videoStream = undefined;
|
||||
|
||||
this.controlStream?.close();
|
||||
this.controlStream = undefined;
|
||||
|
||||
this._controlStreamWriter?.close();
|
||||
await this.process?.kill();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,17 +29,17 @@ export abstract class ScrcpyClientConnection implements Disposable {
|
|||
|
||||
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 { }
|
||||
}
|
||||
|
||||
export class ScrcpyClientForwardConnection extends ScrcpyClientConnection {
|
||||
private async connect(): Promise<AdbBufferedStream> {
|
||||
return new AdbBufferedStream(await this.device.createSocket('localabstract:scrcpy'));
|
||||
private async connect(): Promise<AdbSocket> {
|
||||
return await this.device.createSocket('localabstract:scrcpy');
|
||||
}
|
||||
|
||||
private async connectAndRetry(): Promise<AdbBufferedStream> {
|
||||
private async connectAndRetry(): Promise<AdbSocket> {
|
||||
for (let i = 0; i < 100; i++) {
|
||||
try {
|
||||
return await this.connect();
|
||||
|
@ -51,7 +51,8 @@ export class ScrcpyClientForwardConnection extends ScrcpyClientConnection {
|
|||
}
|
||||
|
||||
private async connectVideoStream(): Promise<AdbBufferedStream> {
|
||||
const stream = await this.connectAndRetry();
|
||||
const socket = await this.connectAndRetry();
|
||||
const stream = new AdbBufferedStream(socket);
|
||||
if (this.options.sendDummyByte) {
|
||||
// server will write a `0` to signal connection success
|
||||
await stream.read(1);
|
||||
|
@ -59,9 +60,9 @@ export class ScrcpyClientForwardConnection extends ScrcpyClientConnection {
|
|||
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();
|
||||
let controlStream: AdbBufferedStream | undefined;
|
||||
let controlStream: AdbSocket | undefined;
|
||||
if (this.options.control) {
|
||||
controlStream = await this.connectAndRetry();
|
||||
}
|
||||
|
@ -96,13 +97,13 @@ export class ScrcpyClientReverseConnection extends ScrcpyClientConnection {
|
|||
});
|
||||
}
|
||||
|
||||
private async accept(): Promise<AdbBufferedStream> {
|
||||
return new AdbBufferedStream((await this.streams.read()).value!);
|
||||
private async accept(): Promise<AdbSocket> {
|
||||
return (await this.streams.read()).value!;
|
||||
}
|
||||
|
||||
public async getStreams(): Promise<[videoSteam: AdbBufferedStream, controlStream: AdbBufferedStream | undefined]> {
|
||||
const videoStream = await this.accept();
|
||||
let controlStream: AdbBufferedStream | undefined;
|
||||
public async getStreams(): Promise<[videoSteam: AdbBufferedStream, controlStream: AdbSocket | undefined]> {
|
||||
const videoStream = new AdbBufferedStream(await this.accept());
|
||||
let controlStream: AdbSocket | undefined;
|
||||
if (this.options.control) {
|
||||
controlStream = await this.accept();
|
||||
}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import type { Disposable } from "@yume-chan/event";
|
||||
import type { WritableStream } from '@yume-chan/adb';
|
||||
import type { Disposable } from "@yume-chan/event";
|
||||
import type { AndroidCodecLevel, AndroidCodecProfile } from "../codec";
|
||||
|
||||
export interface H264EncodingInfo {
|
||||
export interface H264Configuration {
|
||||
profileIndex: number;
|
||||
constraintSet: number;
|
||||
levelIndex: number;
|
||||
|
@ -29,7 +29,7 @@ export interface H264Decoder extends Disposable {
|
|||
|
||||
readonly writable: WritableStream<ArrayBuffer>;
|
||||
|
||||
changeEncoding(size: H264EncodingInfo): void;
|
||||
configure(config: H264Configuration): void;
|
||||
}
|
||||
|
||||
export interface H264DecoderConstructor {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { WritableStream } from "@yume-chan/adb";
|
||||
import { PromiseResolver } from "@yume-chan/async";
|
||||
import { AndroidCodecLevel, AndroidCodecProfile } from "../../codec";
|
||||
import type { H264Decoder, H264EncodingInfo } from '../common';
|
||||
import type { H264Configuration, H264Decoder } from '../common';
|
||||
import { createTinyH264Wrapper, TinyH264Wrapper } from "./wrapper";
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
public async changeEncoding(size: H264EncodingInfo) {
|
||||
public async configure(config: H264Configuration) {
|
||||
this.dispose();
|
||||
|
||||
this._initializer = new PromiseResolver<TinyH264Wrapper>();
|
||||
|
@ -55,23 +55,23 @@ export class TinyH264Decoder implements H264Decoder {
|
|||
this._yuvCanvas = YuvCanvas.attach(this._renderer);;
|
||||
}
|
||||
|
||||
const { encodedWidth, encodedHeight } = size;
|
||||
const { encodedWidth, encodedHeight } = config;
|
||||
const chromaWidth = encodedWidth / 2;
|
||||
const chromaHeight = encodedHeight / 2;
|
||||
|
||||
this._renderer.width = size.croppedWidth;
|
||||
this._renderer.height = size.croppedHeight;
|
||||
this._renderer.width = config.croppedWidth;
|
||||
this._renderer.height = config.croppedHeight;
|
||||
const format = YuvBuffer.format({
|
||||
width: encodedWidth,
|
||||
height: encodedHeight,
|
||||
chromaWidth,
|
||||
chromaHeight,
|
||||
cropLeft: size.cropLeft,
|
||||
cropTop: size.cropTop,
|
||||
cropWidth: size.croppedWidth,
|
||||
cropHeight: size.croppedHeight,
|
||||
displayWidth: size.croppedWidth,
|
||||
displayHeight: size.croppedHeight,
|
||||
cropLeft: config.cropLeft,
|
||||
cropTop: config.cropTop,
|
||||
cropWidth: config.croppedWidth,
|
||||
cropHeight: config.croppedHeight,
|
||||
displayWidth: config.croppedWidth,
|
||||
displayHeight: config.croppedHeight,
|
||||
});
|
||||
|
||||
const wrapper = await createTinyH264Wrapper();
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import type { ValueOrPromise } from "@yume-chan/struct";
|
||||
import { AndroidCodecLevel, AndroidCodecProfile } from "../../codec";
|
||||
import type { H264Decoder, H264EncodingInfo } from "../common";
|
||||
import type { H264Configuration, H264Decoder } from "../common";
|
||||
|
||||
function toHex(value: number) {
|
||||
return value.toString(16).padStart(2, '0').toUpperCase();
|
||||
|
@ -41,11 +41,11 @@ export class WebCodecsDecoder implements H264Decoder {
|
|||
});
|
||||
}
|
||||
|
||||
public changeEncoding(encoding: H264EncodingInfo): ValueOrPromise<void> {
|
||||
const { profileIndex, constraintSet, levelIndex } = encoding;
|
||||
public configure(config: H264Configuration): ValueOrPromise<void> {
|
||||
const { profileIndex, constraintSet, levelIndex } = config;
|
||||
|
||||
this._renderer.width = encoding.croppedWidth;
|
||||
this._renderer.height = encoding.croppedHeight;
|
||||
this._renderer.width = config.croppedWidth;
|
||||
this._renderer.height = config.croppedHeight;
|
||||
|
||||
// https://www.rfc-editor.org/rfc/rfc6381#section-3.3
|
||||
// ISO Base Media File Format Name Space
|
||||
|
|
|
@ -5,7 +5,7 @@ import { ScrcpyClientConnection, ScrcpyClientConnectionOptions, ScrcpyClientForw
|
|||
import { AndroidKeyEventAction, ScrcpyControlMessageType } from "../../message";
|
||||
import type { ScrcpyBackOrScreenOnEvent1_18 } from "../1_18";
|
||||
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";
|
||||
|
||||
export enum ScrcpyLogLevel {
|
||||
|
@ -216,15 +216,12 @@ export class ScrcpyOptions1_16<T extends ScrcpyOptionsInit1_16 = ScrcpyOptionsIn
|
|||
public async parseVideoStream(stream: AdbBufferedStream): Promise<VideoStreamPacket> {
|
||||
if (this.value.sendFrameMeta === false) {
|
||||
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);
|
||||
if (!data || data.byteLength === 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (pts === NoPts) {
|
||||
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;
|
||||
return {
|
||||
encodingInfo: {
|
||||
type: 'configuration',
|
||||
data: {
|
||||
profileIndex,
|
||||
constraintSet,
|
||||
levelIndex,
|
||||
|
@ -269,18 +267,20 @@ export class ScrcpyOptions1_16<T extends ScrcpyOptionsInit1_16 = ScrcpyOptionsIn
|
|||
};
|
||||
}
|
||||
|
||||
let array: Uint8Array;
|
||||
let frameData: ArrayBuffer;
|
||||
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(data!), this._streamHeader.byteLength);
|
||||
this._streamHeader = undefined;
|
||||
} else {
|
||||
array = new Uint8Array(data!);
|
||||
frameData = data;
|
||||
}
|
||||
|
||||
return {
|
||||
videoData: array.buffer,
|
||||
type: 'frame',
|
||||
data: frameData,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
// cspell: ignore autosync
|
||||
|
||||
import { ScrcpyOptions1_18, ScrcpyOptionsInit1_18 } from './1_18';
|
||||
import { ScrcpyOptions1_18, type ScrcpyOptionsInit1_18 } from './1_18';
|
||||
import { toScrcpyOptionValue } from "./common";
|
||||
|
||||
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 { ScrcpyClientConnection, ScrcpyClientConnectionOptions, ScrcpyClientForwardConnection, ScrcpyClientReverseConnection } from "../connection";
|
||||
import { type ScrcpyClientConnection, ScrcpyClientConnectionOptions, ScrcpyClientForwardConnection, ScrcpyClientReverseConnection } from "../connection";
|
||||
import { ScrcpyInjectScrollControlMessage1_16 } from "./1_16";
|
||||
import { ScrcpyOptions1_21, type ScrcpyOptionsInit1_21 } from "./1_21";
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import type { Adb, AdbBufferedStream } from "@yume-chan/adb";
|
||||
import type { ScrcpyClientConnection } from "../connection";
|
||||
import type { H264EncodingInfo } from "../decoder";
|
||||
import type { H264Configuration } from "../decoder";
|
||||
import type { ScrcpyBackOrScreenOnEvent1_18 } from "./1_18";
|
||||
import type { ScrcpyInjectScrollControlMessage1_22 } from "./1_22";
|
||||
|
||||
|
@ -28,12 +28,18 @@ export function toScrcpyOptionValue<T>(value: any, empty: T): string | T {
|
|||
return `${value}`;
|
||||
}
|
||||
|
||||
export interface VideoStreamPacket {
|
||||
encodingInfo?: H264EncodingInfo | undefined;
|
||||
|
||||
videoData?: ArrayBuffer | undefined;
|
||||
export interface VideoStreamConfigurationPacket {
|
||||
type: 'configuration';
|
||||
data: H264Configuration;
|
||||
}
|
||||
|
||||
export interface VideoStreamFramePacket {
|
||||
type: 'frame';
|
||||
data: ArrayBuffer;
|
||||
}
|
||||
|
||||
export type VideoStreamPacket = VideoStreamConfigurationPacket | VideoStreamFramePacket;
|
||||
|
||||
export interface ScrcpyOptions<T> {
|
||||
value: Partial<T>;
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue