mirror of
https://github.com/yume-chan/ya-webadb.git
synced 2025-10-05 19:42:15 +02:00
feat: migrate more things to streams
This commit is contained in:
parent
b7725567a6
commit
ef57682ec3
20 changed files with 2239 additions and 2007 deletions
|
@ -1,6 +1,6 @@
|
|||
// cspell: ignore scrollback
|
||||
|
||||
import { AdbShell, encodeUtf8 } from "@yume-chan/adb";
|
||||
import { AdbSubprocessProtocol, encodeUtf8 } from "@yume-chan/adb";
|
||||
import { AutoDisposable } from "@yume-chan/event";
|
||||
import { Terminal } from 'xterm';
|
||||
import { FitAddon } from 'xterm-addon-fit';
|
||||
|
@ -18,29 +18,43 @@ export class AdbTerminal extends AutoDisposable {
|
|||
|
||||
private readonly fitAddon = new FitAddon();
|
||||
|
||||
private _shell: AdbShell | undefined;
|
||||
public get socket() { return this._shell; }
|
||||
private _socket: AdbSubprocessProtocol | undefined;
|
||||
private _socketAbortController: AbortController | undefined;
|
||||
public get socket() { return this._socket; }
|
||||
public set socket(value) {
|
||||
if (this._shell) {
|
||||
if (this._socket) {
|
||||
// Remove event listeners
|
||||
this.dispose();
|
||||
this._socketAbortController?.abort();
|
||||
}
|
||||
|
||||
this._shell = value;
|
||||
this._socket = value;
|
||||
|
||||
if (value) {
|
||||
this.terminal.clear();
|
||||
this.terminal.reset();
|
||||
|
||||
this.addDisposable(value.onStdout(data => {
|
||||
this.terminal.write(new Uint8Array(data));
|
||||
}));
|
||||
this.addDisposable(value.onStderr(data => {
|
||||
this.terminal.write(new Uint8Array(data));
|
||||
}));
|
||||
this._socketAbortController = new AbortController();
|
||||
|
||||
value.stdout.pipeTo(new WritableStream({
|
||||
write: (chunk) => {
|
||||
this.terminal.write(new Uint8Array(chunk));
|
||||
},
|
||||
}), {
|
||||
signal: this._socketAbortController.signal,
|
||||
});
|
||||
value.stderr.pipeTo(new WritableStream({
|
||||
write: (chunk) => {
|
||||
this.terminal.write(new Uint8Array(chunk));
|
||||
},
|
||||
}), {
|
||||
signal: this._socketAbortController.signal,
|
||||
});
|
||||
|
||||
const _writer = value.stdin.getWriter();
|
||||
this.addDisposable(this.terminal.onData(data => {
|
||||
const buffer = encodeUtf8(data);
|
||||
value.write(buffer);
|
||||
_writer.write(buffer);
|
||||
}));
|
||||
|
||||
this.fit();
|
||||
|
@ -77,6 +91,6 @@ export class AdbTerminal extends AutoDisposable {
|
|||
this.fitAddon.fit();
|
||||
// Resize remote terminal
|
||||
const { rows, cols } = this.terminal;
|
||||
this._shell?.resize(rows, cols);
|
||||
this._socket?.resize(rows, cols);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ import { FileIconType } from "@fluentui/react-file-type-icons";
|
|||
import { getFileTypeIconNameFromExtensionOrType } from '@fluentui/react-file-type-icons/lib-commonjs/getFileTypeIconProps';
|
||||
import { DEFAULT_BASE_URL as FILE_TYPE_ICONS_BASE_URL } from '@fluentui/react-file-type-icons/lib-commonjs/initializeFileTypeIcons';
|
||||
import { useConst } from '@fluentui/react-hooks';
|
||||
import { AdbSyncEntryResponse, AdbSyncMaxPacketSize, LinuxFileType } from '@yume-chan/adb';
|
||||
import { AdbSyncEntryResponse, ADB_SYNC_MAX_PACKET_SIZE, LinuxFileType } from '@yume-chan/adb';
|
||||
import { action, autorun, makeAutoObservable, observable, runInAction } from "mobx";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { NextPage } from "next";
|
||||
|
@ -16,6 +16,23 @@ import { CommandBar, NoSsr } from '../components';
|
|||
import { globalState } from '../state';
|
||||
import { asyncEffect, chunkFile, formatSize, formatSpeed, Icons, pickFile, RouteStackProps } from '../utils';
|
||||
|
||||
/**
|
||||
* Because of internal buffer of upstream/downstream streams,
|
||||
* the progress value won't be 100% accurate. But it's usually good enough.
|
||||
*/
|
||||
export class ProgressStream extends TransformStream<ArrayBuffer, ArrayBuffer> {
|
||||
public constructor(onProgress: (value: number) => void) {
|
||||
let progress = 0;
|
||||
super({
|
||||
transform(chunk, controller) {
|
||||
progress += chunk.byteLength;
|
||||
onProgress(progress);
|
||||
controller.enqueue(chunk);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let StreamSaver: typeof import('streamsaver');
|
||||
if (typeof window !== 'undefined') {
|
||||
const { publicRuntimeConfig } = getConfig();
|
||||
|
@ -169,7 +186,7 @@ class FileManagerState {
|
|||
const sync = await globalState.device!.sync();
|
||||
try {
|
||||
const itemPath = path.resolve(this.path, this.selectedItems[0].name!);
|
||||
const readableStream = createReadableStreamFromBufferIterator(sync.read(itemPath));
|
||||
const readableStream = sync.read(itemPath);
|
||||
|
||||
const writeableStream = StreamSaver!.createWriteStream(this.selectedItems[0].name!, {
|
||||
size: this.selectedItems[0].size,
|
||||
|
@ -473,15 +490,17 @@ class FileManagerState {
|
|||
}), 1000);
|
||||
|
||||
try {
|
||||
await sync.write(
|
||||
const writable = sync.write(
|
||||
itemPath,
|
||||
chunkFile(file, AdbSyncMaxPacketSize),
|
||||
chunkFile(file, ADB_SYNC_MAX_PACKET_SIZE),
|
||||
(LinuxFileType.File << 12) | 0o666,
|
||||
file.lastModified / 1000,
|
||||
action((uploaded) => {
|
||||
this.uploadedSize = uploaded;
|
||||
}),
|
||||
);
|
||||
const readable: ReadableStream<ArrayBuffer> = file.stream();
|
||||
readable.pipeThrough();
|
||||
runInAction(() => {
|
||||
this.uploadSpeed = this.uploadedSize - this.debouncedUploadedSize;
|
||||
this.debouncedUploadedSize = this.uploadedSize;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { DefaultButton, ProgressIndicator, Stack } from "@fluentui/react";
|
||||
import { AdbSyncMaxPacketSize } from "@yume-chan/adb";
|
||||
import { ADB_SYNC_MAX_PACKET_SIZE } from "@yume-chan/adb";
|
||||
import { makeAutoObservable, observable, runInAction } from "mobx";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { NextPage } from "next";
|
||||
|
@ -7,6 +7,7 @@ import Head from "next/head";
|
|||
import React from "react";
|
||||
import { globalState } from "../state";
|
||||
import { chunkFile, pickFile, RouteStackProps } from "../utils";
|
||||
import { ProgressStream } from './file-manager';
|
||||
|
||||
enum Stage {
|
||||
Uploading,
|
||||
|
@ -57,6 +58,8 @@ class InstallPageState {
|
|||
};
|
||||
});
|
||||
|
||||
setTimeout(handler);
|
||||
const readable = file.stream();
|
||||
await globalState.device!.install(chunkFile(file, AdbSyncMaxPacketSize), uploaded => {
|
||||
runInAction(() => {
|
||||
if (uploaded !== file.size) {
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import { CommandBar, Dialog, Dropdown, ICommandBarItemProps, Icon, IconButton, IDropdownOption, LayerHost, Position, ProgressIndicator, SpinButton, Stack, Toggle, TooltipHost } from "@fluentui/react";
|
||||
import { useId } from "@fluentui/react-hooks";
|
||||
import { TransformStream } from '@yume-chan/adb';
|
||||
import { EventEmitter } from "@yume-chan/event";
|
||||
import { AndroidKeyCode, AndroidKeyEventAction, AndroidMotionEventAction, CodecOptions, DEFAULT_SERVER_PATH, H264Decoder, H264DecoderConstructor, pushServer, ScrcpyClient, ScrcpyLogLevel, ScrcpyOptions1_22, ScrcpyScreenOrientation, TinyH264Decoder, WebCodecsDecoder } from "@yume-chan/scrcpy";
|
||||
import { AndroidKeyCode, AndroidKeyEventAction, AndroidMotionEventAction, CodecOptions, DEFAULT_SERVER_PATH, H264Decoder, H264DecoderConstructor, pushServerStream, ScrcpyClient, ScrcpyLogLevel, ScrcpyOptions1_22, ScrcpyScreenOrientation, TinyH264Decoder, WebCodecsDecoder } from "@yume-chan/scrcpy";
|
||||
import SCRCPY_SERVER_VERSION from '@yume-chan/scrcpy/bin/version';
|
||||
import { action, autorun, makeAutoObservable, observable, runInAction } from "mobx";
|
||||
import { observer } from "mobx-react-lite";
|
||||
|
@ -406,11 +407,9 @@ class ScrcpyPageState {
|
|||
}), 1000);
|
||||
|
||||
try {
|
||||
await pushServer(globalState.device, serverBuffer, {
|
||||
onProgress: action((progress) => {
|
||||
this.serverUploadedSize = progress;
|
||||
}),
|
||||
});
|
||||
const writable = await pushServerStream(globalState.device);
|
||||
// TODO: Scrcpy: push server
|
||||
|
||||
runInAction(() => {
|
||||
this.serverUploadSpeed = this.serverUploadedSize - this.debouncedServerUploadedSize;
|
||||
this.debouncedServerUploadedSize = this.serverUploadedSize;
|
||||
|
@ -441,7 +440,8 @@ class ScrcpyPageState {
|
|||
|
||||
// Run scrcpy once will delete the server file
|
||||
// Re-push it
|
||||
await pushServer(globalState.device, serverBuffer);
|
||||
const writable = await pushServerStream(globalState.device);
|
||||
// TODO: Scrcpy: push server
|
||||
|
||||
const factory = this.selectedDecoder.factory;
|
||||
const decoder = new factory();
|
||||
|
@ -449,30 +449,6 @@ class ScrcpyPageState {
|
|||
this.decoder = decoder;
|
||||
});
|
||||
|
||||
const client = new ScrcpyClient(globalState.device);
|
||||
runInAction(() => this.log = []);
|
||||
client.onOutput(action(line => this.log.push(line)));
|
||||
client.onClose(this.stop);
|
||||
|
||||
client.onEncodingChanged(action((encoding) => {
|
||||
const { croppedWidth, croppedHeight, } = encoding;
|
||||
|
||||
this.log.push(`[client] Video size changed: ${croppedWidth}x${croppedHeight}`);
|
||||
|
||||
this.width = croppedWidth;
|
||||
this.height = croppedHeight;
|
||||
|
||||
decoder.changeEncoding(encoding);
|
||||
}));
|
||||
|
||||
client.onVideoData((data) => {
|
||||
decoder.feedData(data);
|
||||
});
|
||||
|
||||
client.onClipboardChange(content => {
|
||||
window.navigator.clipboard.writeText(content);
|
||||
});
|
||||
|
||||
const options = new ScrcpyOptions1_22({
|
||||
logLevel: ScrcpyLogLevel.Debug,
|
||||
maxSize: this.resolution,
|
||||
|
@ -489,16 +465,43 @@ class ScrcpyPageState {
|
|||
});
|
||||
|
||||
runInAction(() => {
|
||||
this.log = [];
|
||||
this.log.push(`[client] Server version: ${SCRCPY_SERVER_VERSION}`);
|
||||
this.log.push(`[client] Server arguments: ${options.formatServerArguments().join(' ')}`);
|
||||
});
|
||||
|
||||
await client.start(
|
||||
const client = await ScrcpyClient.start(
|
||||
globalState.device,
|
||||
DEFAULT_SERVER_PATH,
|
||||
SCRCPY_SERVER_VERSION,
|
||||
options,
|
||||
options
|
||||
);
|
||||
|
||||
client.stdout.pipeTo(new WritableStream({
|
||||
write: (line) => {
|
||||
this.log.push(line);
|
||||
}
|
||||
}));
|
||||
|
||||
client.close().then(() => this.stop());
|
||||
|
||||
client.videoStream.pipeThrough(new TransformStream({
|
||||
transform: (chunk, controller) => {
|
||||
if (chunk.type === 'configuration') {
|
||||
const { croppedWidth, croppedHeight, } = chunk.data;
|
||||
this.log.push(`[client] Video size changed: ${croppedWidth}x${croppedHeight}`);
|
||||
|
||||
this.width = croppedWidth;
|
||||
this.height = croppedHeight;
|
||||
}
|
||||
controller.enqueue(chunk);
|
||||
}
|
||||
})).pipeTo(decoder.writable);
|
||||
|
||||
client.onClipboardChange(content => {
|
||||
window.navigator.clipboard.writeText(content);
|
||||
});
|
||||
|
||||
runInAction(() => {
|
||||
this.client = client;
|
||||
this.running = true;
|
||||
|
|
|
@ -53,7 +53,7 @@ const Shell: NextPage = (): JSX.Element | null => {
|
|||
|
||||
try {
|
||||
connectingRef.current = true;
|
||||
const socket = await globalState.device.childProcess.shell();
|
||||
const socket = await globalState.device.subprocess.shell();
|
||||
terminal.socket = socket;
|
||||
} catch (e) {
|
||||
globalState.showErrorDialog(e instanceof Error ? e.message : `${e}`);
|
||||
|
|
3869
common/config/rush/pnpm-lock.yaml
generated
3869
common/config/rush/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
|
@ -37,7 +37,6 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@yume-chan/adb": "^0.0.10",
|
||||
"@yume-chan/event": "^0.0.10",
|
||||
"tslib": "^2.3.1"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,20 @@
|
|||
import { AdbBackend, ReadableStream, ReadableWritablePair, TransformStream, WritableStream } from '@yume-chan/adb';
|
||||
|
||||
/**
|
||||
* Transform an `ArrayBufferView` stream to an `ArrayBuffer` stream.
|
||||
*
|
||||
* The view must wrap the whole buffer (`byteOffset === 0` && `byteLength === buffer.byteLength`).
|
||||
*/
|
||||
export class ExtractViewBufferStream extends TransformStream<ArrayBufferView, ArrayBuffer>{
|
||||
constructor() {
|
||||
super({
|
||||
transform(chunk, controller) {
|
||||
controller.enqueue(chunk.buffer);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface TCPSocket {
|
||||
close(): Promise<void>;
|
||||
|
@ -32,13 +47,7 @@ declare global {
|
|||
export class AdbDirectSocketsBackendStreams implements ReadableWritablePair<ArrayBuffer, ArrayBuffer>{
|
||||
private socket: TCPSocket;
|
||||
|
||||
private _readableTransformStream = new TransformStream<Uint8Array, ArrayBuffer>({
|
||||
transform(chunk, controller) {
|
||||
// Although spec didn't say,
|
||||
// the chunk always has `byteOffset` of 0 and `byteLength` same as its buffer
|
||||
controller.enqueue(chunk.buffer);
|
||||
},
|
||||
});
|
||||
private _readableTransformStream: ExtractViewBufferStream;
|
||||
public get readable(): ReadableStream<ArrayBuffer> {
|
||||
return this._readableTransformStream.readable;
|
||||
}
|
||||
|
@ -49,6 +58,10 @@ export class AdbDirectSocketsBackendStreams implements ReadableWritablePair<Arra
|
|||
|
||||
constructor(socket: TCPSocket) {
|
||||
this.socket = socket;
|
||||
|
||||
// Although Direct Sockets spec didn't say,
|
||||
// WebTransport spec and File spec all have the `Uint8Array` wraps the while `ArrayBuffer`.
|
||||
this._readableTransformStream = new ExtractViewBufferStream();
|
||||
this.socket.readable.pipeTo(this._readableTransformStream.writable);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,7 +33,6 @@
|
|||
"dependencies": {
|
||||
"@types/w3c-web-usb": "^1.0.4",
|
||||
"@yume-chan/adb": "^0.0.10",
|
||||
"@yume-chan/event": "^0.0.10",
|
||||
"tslib": "^2.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
@ -36,8 +36,6 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@yume-chan/adb": "^0.0.10",
|
||||
"@yume-chan/async": "^2.1.4",
|
||||
"@yume-chan/event": "^0.0.10",
|
||||
"tslib": "^2.3.1"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { AdbBackend, ReadableStream, WritableStream } from '@yume-chan/adb';
|
||||
import { PromiseResolver } from '@yume-chan/async';
|
||||
|
||||
export default class AdbWsBackend implements AdbBackend {
|
||||
public readonly serial: string;
|
||||
|
@ -15,12 +14,12 @@ export default class AdbWsBackend implements AdbBackend {
|
|||
const socket = new WebSocket(this.serial);
|
||||
socket.binaryType = "arraybuffer";
|
||||
|
||||
const resolver = new PromiseResolver();
|
||||
socket.onopen = resolver.resolve;
|
||||
await new Promise((resolve, reject) => {
|
||||
socket.onopen = resolve;
|
||||
socket.onerror = () => {
|
||||
resolver.reject(new Error('WebSocket connect failed'));
|
||||
reject(new Error('WebSocket connect failed'));
|
||||
};
|
||||
await resolver.promise;
|
||||
});
|
||||
|
||||
const readable = new ReadableStream({
|
||||
start: (controller) => {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
// cspell: ignore RSASSA
|
||||
|
||||
import { AdbCredentialStore, calculateBase64EncodedLength, calculatePublicKey, calculatePublicKeyLength, decodeBase64, encodeBase64 } from "@yume-chan/adb";
|
||||
import { type AdbCredentialStore, calculateBase64EncodedLength, calculatePublicKey, calculatePublicKeyLength, decodeBase64, encodeBase64 } from "@yume-chan/adb";
|
||||
|
||||
const Utf8Encoder = new TextEncoder();
|
||||
const Utf8Decoder = new TextDecoder();
|
||||
|
|
|
@ -3,33 +3,12 @@ import { Adb } from '../../adb';
|
|||
import { AdbFeatures } from '../../features';
|
||||
import { AdbSocket } from '../../socket';
|
||||
import { AdbBufferedStream } from '../../stream';
|
||||
import { AutoResetEvent, QueuingStrategy, ReadableStream, TransformStream, WritableStream, WritableStreamDefaultWriter } from '../../utils';
|
||||
import { AutoResetEvent, ReadableStream, TransformStream, WritableStream, WritableStreamDefaultWriter } from '../../utils';
|
||||
import { AdbSyncEntryResponse, adbSyncOpenDir } from './list';
|
||||
import { adbSyncPull } from './pull';
|
||||
import { adbSyncPush } from './push';
|
||||
import { adbSyncLstat, adbSyncStat } from './stat';
|
||||
|
||||
class GuardedStream<T> extends TransformStream<T, T>{
|
||||
constructor(
|
||||
lock: AutoResetEvent,
|
||||
writableStrategy?: QueuingStrategy<T>,
|
||||
readableStrategy?: QueuingStrategy<T>
|
||||
) {
|
||||
super(
|
||||
{
|
||||
start() {
|
||||
return lock.wait();
|
||||
},
|
||||
flush() {
|
||||
lock.notify();
|
||||
},
|
||||
},
|
||||
writableStrategy,
|
||||
readableStrategy
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class AdbSync extends AutoDisposable {
|
||||
protected adb: Adb;
|
||||
|
||||
|
@ -104,17 +83,47 @@ export class AdbSync extends AutoDisposable {
|
|||
return results;
|
||||
}
|
||||
|
||||
public read(filename: string): ReadableStream<ArrayBuffer> {
|
||||
/**
|
||||
* Read the content of a file on device.
|
||||
*
|
||||
* @param filename The full path of the file on device to read.
|
||||
* @returns
|
||||
* A promise that resolves to a `ReadableStream`.
|
||||
*
|
||||
* If the promise doesn't resolve immediately, it means the sync object is busy processing another command.
|
||||
*/
|
||||
public async read(filename: string): Promise<ReadableStream<ArrayBuffer>> {
|
||||
await this.sendLock.wait();
|
||||
|
||||
const readable = adbSyncPull(this.stream, this.writer, filename);
|
||||
return readable.pipeThrough(new GuardedStream(this.sendLock));
|
||||
|
||||
const lockStream = new TransformStream<ArrayBuffer, ArrayBuffer>();
|
||||
readable
|
||||
.pipeTo(lockStream.writable)
|
||||
.then(() => {
|
||||
this.sendLock.notify();
|
||||
});
|
||||
|
||||
return lockStream.readable;
|
||||
}
|
||||
|
||||
public write(
|
||||
/**
|
||||
* Write (or overwrite) a file on device.
|
||||
*
|
||||
* @param filename The full path of the file on device to write.
|
||||
* @param mode The unix permissions of the file.
|
||||
* @param mtime The modified time of the file.
|
||||
* @returns
|
||||
* A promise that resolves to a `WritableStream`.
|
||||
*
|
||||
* If the promise doesn't resolve immediately, it means the sync object is busy processing another command.
|
||||
*/
|
||||
public async write(
|
||||
filename: string,
|
||||
mode?: number,
|
||||
mtime?: number,
|
||||
): WritableStream<ArrayBuffer> {
|
||||
const lockStream = new GuardedStream<ArrayBuffer>(this.sendLock);
|
||||
): Promise<WritableStream<ArrayBuffer>> {
|
||||
await this.sendLock.wait();
|
||||
|
||||
const writable = adbSyncPush(
|
||||
this.stream,
|
||||
|
@ -123,14 +132,22 @@ export class AdbSync extends AutoDisposable {
|
|||
mode,
|
||||
mtime,
|
||||
);
|
||||
lockStream.readable.pipeTo(writable);
|
||||
|
||||
const lockStream = new TransformStream<ArrayBuffer, ArrayBuffer>();
|
||||
// `lockStream`'s `flush` will be invoked before `writable` fully closes,
|
||||
// but `lockStream.readable.pipeTo` will wait for `writable` to close.
|
||||
lockStream.readable
|
||||
.pipeTo(writable)
|
||||
.then(() => {
|
||||
this.sendLock.notify();
|
||||
});
|
||||
|
||||
return lockStream.writable;
|
||||
}
|
||||
|
||||
public override dispose() {
|
||||
public override async dispose() {
|
||||
super.dispose();
|
||||
this.stream.close();
|
||||
this.writer.close();
|
||||
await this.writer.close();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,7 +35,6 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@yume-chan/adb": "^0.0.10",
|
||||
"@yume-chan/event": "^0.0.10",
|
||||
"tslib": "^2.3.1"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { Adb, AdbBufferedStream, AdbNoneSubprocessProtocol, AdbSocket, AdbSubprocessProtocol, DecodeUtf8Stream, ReadableStream, TransformStream, WritableStreamDefaultWriter } from '@yume-chan/adb';
|
||||
import { Adb, AdbBufferedStream, AdbNoneSubprocessProtocol, AdbSocket, AdbSubprocessProtocol, DecodeUtf8Stream, TransformStream, WritableStreamDefaultWriter } from '@yume-chan/adb';
|
||||
import { EventEmitter } from '@yume-chan/event';
|
||||
import Struct from '@yume-chan/struct';
|
||||
import { AndroidMotionEventAction, ScrcpyControlMessageType, ScrcpyInjectKeyCodeControlMessage, ScrcpyInjectTextControlMessage, ScrcpyInjectTouchControlMessage, type AndroidKeyEventAction } from './message';
|
||||
import type { ScrcpyInjectScrollControlMessage1_22, ScrcpyOptions, VideoStreamPacket } from "./options";
|
||||
import { pushServer, PushServerOptions } from "./push-server";
|
||||
import { PushServerOptions, pushServerStream } from "./push-server";
|
||||
|
||||
function* splitLines(text: string): Generator<string, void, void> {
|
||||
let start = 0;
|
||||
|
@ -27,12 +27,11 @@ const ClipboardMessage =
|
|||
.string('content', { lengthField: 'length' });
|
||||
|
||||
export class ScrcpyClient {
|
||||
public static pushServer(
|
||||
public static pushServerStream(
|
||||
device: Adb,
|
||||
file: ReadableStream<ArrayBuffer>,
|
||||
options?: PushServerOptions
|
||||
) {
|
||||
pushServer(device, file, options);
|
||||
return pushServerStream(device, options);
|
||||
}
|
||||
|
||||
public static async getEncoders(
|
||||
|
@ -128,7 +127,7 @@ export class ScrcpyClient {
|
|||
public get screenHeight() { return this._screenHeight; }
|
||||
|
||||
private _videoStream: TransformStream<VideoStreamPacket, VideoStreamPacket>;
|
||||
public get videoStream() { return this._videoStream; }
|
||||
public get videoStream() { return this._videoStream.readable; }
|
||||
|
||||
private _controlStreamWriter: WritableStreamDefaultWriter<ArrayBuffer> | undefined;
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import type { WritableStream } from '@yume-chan/adb';
|
||||
import type { Disposable } from "@yume-chan/event";
|
||||
import type { AndroidCodecLevel, AndroidCodecProfile } from "../codec";
|
||||
import type { VideoStreamPacket } from "../options";
|
||||
|
||||
export interface H264Configuration {
|
||||
profileIndex: number;
|
||||
|
@ -27,9 +28,7 @@ export interface H264Decoder extends Disposable {
|
|||
|
||||
readonly renderer: HTMLElement;
|
||||
|
||||
readonly writable: WritableStream<ArrayBuffer>;
|
||||
|
||||
configure(config: H264Configuration): void;
|
||||
readonly writable: WritableStream<VideoStreamPacket>;
|
||||
}
|
||||
|
||||
export interface H264DecoderConstructor {
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import { WritableStream } from "@yume-chan/adb";
|
||||
import { PromiseResolver } from "@yume-chan/async";
|
||||
import { AndroidCodecLevel, AndroidCodecProfile } from "../../codec";
|
||||
import type { VideoStreamPacket } from "../../options";
|
||||
import type { H264Configuration, H264Decoder } from '../common';
|
||||
import { createTinyH264Wrapper, TinyH264Wrapper } from "./wrapper";
|
||||
import { createTinyH264Wrapper, type TinyH264Wrapper } from "./wrapper";
|
||||
|
||||
let cachedInitializePromise: Promise<{ YuvBuffer: typeof import('yuv-buffer'), YuvCanvas: typeof import('yuv-canvas').default; }> | undefined;
|
||||
function initialize() {
|
||||
|
@ -25,16 +26,7 @@ export class TinyH264Decoder implements H264Decoder {
|
|||
private _renderer: HTMLCanvasElement;
|
||||
public get renderer() { return this._renderer; }
|
||||
|
||||
private _writable = new WritableStream<ArrayBuffer>({
|
||||
write: async (chunk) => {
|
||||
if (!this._initializer) {
|
||||
throw new Error('Decoder not initialized');
|
||||
}
|
||||
|
||||
const wrapper = await this._initializer.promise;
|
||||
wrapper.feed(chunk);
|
||||
}
|
||||
});
|
||||
private _writable: WritableStream<VideoStreamPacket>;
|
||||
public get writable() { return this._writable; }
|
||||
|
||||
private _yuvCanvas: import('yuv-canvas').default | undefined;
|
||||
|
@ -42,10 +34,29 @@ export class TinyH264Decoder implements H264Decoder {
|
|||
|
||||
public constructor() {
|
||||
initialize();
|
||||
|
||||
this._renderer = document.createElement('canvas');
|
||||
|
||||
this._writable = new WritableStream({
|
||||
write: async (packet) => {
|
||||
switch (packet.type) {
|
||||
case 'configuration':
|
||||
this.configure(packet.data);
|
||||
break;
|
||||
case 'frame':
|
||||
if (!this._initializer) {
|
||||
throw new Error('Decoder not initialized');
|
||||
}
|
||||
|
||||
public async configure(config: H264Configuration) {
|
||||
const wrapper = await this._initializer.promise;
|
||||
wrapper.feed(packet.data);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async configure(config: H264Configuration) {
|
||||
this.dispose();
|
||||
|
||||
this._initializer = new PromiseResolver<TinyH264Wrapper>();
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import type { ValueOrPromise } from "@yume-chan/struct";
|
||||
import { AndroidCodecLevel, AndroidCodecProfile } from "../../codec";
|
||||
import type { VideoStreamPacket } from "../../options";
|
||||
import type { H264Configuration, H264Decoder } from "../common";
|
||||
|
||||
function toHex(value: number) {
|
||||
|
@ -11,15 +11,7 @@ export class WebCodecsDecoder implements H264Decoder {
|
|||
|
||||
public readonly maxLevel = AndroidCodecLevel.Level5;
|
||||
|
||||
private _writable = new WritableStream<ArrayBuffer>({
|
||||
write: async (chunk) => {
|
||||
this.decoder.decode(new EncodedVideoChunk({
|
||||
type: 'key',
|
||||
timestamp: 0,
|
||||
data: chunk,
|
||||
}));
|
||||
}
|
||||
});
|
||||
private _writable: WritableStream<VideoStreamPacket>;
|
||||
public get writable() { return this._writable; }
|
||||
|
||||
private _renderer: HTMLCanvasElement;
|
||||
|
@ -39,9 +31,26 @@ export class WebCodecsDecoder implements H264Decoder {
|
|||
},
|
||||
error() { },
|
||||
});
|
||||
|
||||
this._writable = new WritableStream({
|
||||
write: async (packet) => {
|
||||
switch (packet.type) {
|
||||
case 'configuration':
|
||||
this.configure(packet.data);
|
||||
break;
|
||||
case 'frame':
|
||||
this.decoder.decode(new EncodedVideoChunk({
|
||||
type: 'key',
|
||||
timestamp: 0,
|
||||
data: packet.data,
|
||||
}));
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public configure(config: H264Configuration): ValueOrPromise<void> {
|
||||
private configure(config: H264Configuration) {
|
||||
const { profileIndex, constraintSet, levelIndex } = config;
|
||||
|
||||
this._renderer.width = config.croppedWidth;
|
||||
|
@ -56,14 +65,6 @@ export class WebCodecsDecoder implements H264Decoder {
|
|||
});
|
||||
}
|
||||
|
||||
feedData(data: ArrayBuffer): ValueOrPromise<void> {
|
||||
this.decoder.decode(new EncodedVideoChunk({
|
||||
type: 'key',
|
||||
timestamp: 0,
|
||||
data,
|
||||
}));
|
||||
}
|
||||
|
||||
public dispose() {
|
||||
this.decoder.close();
|
||||
}
|
||||
|
|
|
@ -1,13 +1,12 @@
|
|||
import { Adb } from "@yume-chan/adb";
|
||||
import { Adb, TransformStream } from "@yume-chan/adb";
|
||||
import { DEFAULT_SERVER_PATH } from "./options";
|
||||
|
||||
export interface PushServerOptions {
|
||||
path?: string;
|
||||
}
|
||||
|
||||
export async function pushServer(
|
||||
export async function pushServerStream(
|
||||
device: Adb,
|
||||
file: ReadableStream<ArrayBuffer>,
|
||||
options: PushServerOptions = {}
|
||||
) {
|
||||
const {
|
||||
|
@ -15,6 +14,16 @@ export async function pushServer(
|
|||
} = options;
|
||||
|
||||
const sync = await device.sync();
|
||||
const stream = sync.write(path);
|
||||
await file.pipeTo(stream);
|
||||
const writable = sync.write(path);
|
||||
|
||||
const lockStream = new TransformStream<ArrayBuffer, ArrayBuffer>();
|
||||
// Same as inside AdbSync,
|
||||
// can only use `pipeTo` to detect the writable is fully closed.
|
||||
lockStream.readable
|
||||
.pipeTo(writable)
|
||||
.then(() => {
|
||||
sync.dispose();
|
||||
});
|
||||
|
||||
return lockStream.writable;
|
||||
}
|
||||
|
|
|
@ -42,7 +42,6 @@
|
|||
"typescript": "^4.5.5",
|
||||
"@yume-chan/ts-package-builder": "^1.0.0",
|
||||
"@types/jest": "^27.4.0",
|
||||
"@types/node": "^17.0.17",
|
||||
"@types/bluebird": "^3.5.36"
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue