feat: migrate scrcpy to streams

This commit is contained in:
Simon Chan 2022-02-16 23:43:41 +08:00
parent 8b78c9c331
commit b7725567a6
21 changed files with 308 additions and 460 deletions

View file

@ -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:

View file

@ -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;
} }
} }

View file

@ -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();
}
} }

View file

@ -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"

View file

@ -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"
}, },

View file

@ -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();
} }
} }

View file

@ -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();
} }
} }

View file

@ -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>;
} }

View file

@ -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;

View file

@ -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();
} }
} }

View file

@ -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; }

View file

@ -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; }

View file

@ -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();
} }
} }

View file

@ -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();
} }

View file

@ -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 {

View file

@ -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();

View file

@ -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

View file

@ -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,
}; };
} }

View file

@ -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 {

View file

@ -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";

View file

@ -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>;