mirror of
https://github.com/yume-chan/ya-webadb.git
synced 2025-10-05 02:39:26 +02:00
refactor(scrcpy): separate parts depend on ADB
This commit is contained in:
parent
6158745ef5
commit
ce8f062e96
12 changed files with 623 additions and 404 deletions
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
|
@ -28,6 +28,7 @@
|
|||
"Nalu",
|
||||
"opendir",
|
||||
"PKCS",
|
||||
"ponyfill",
|
||||
"runtimes",
|
||||
"Scrcpy",
|
||||
"sendrecv",
|
||||
|
|
|
@ -9,7 +9,7 @@ import { CSSProperties, ReactNode, useEffect, useState } from "react";
|
|||
|
||||
import { ADB_SYNC_MAX_PACKET_SIZE, ChunkStream, InspectStream, ReadableStream, WritableStream } from '@yume-chan/adb';
|
||||
import { EventEmitter } from "@yume-chan/event";
|
||||
import { AndroidKeyCode, AndroidKeyEventAction, AndroidMotionEventAction, CodecOptions, DEFAULT_SERVER_PATH, pushServer, ScrcpyClient, ScrcpyLogLevel, ScrcpyOptions1_24, ScrcpyVideoOrientation, TinyH264Decoder, WebCodecsDecoder, type H264Decoder, type H264DecoderConstructor, type VideoStreamPacket } from "@yume-chan/scrcpy";
|
||||
import { AdbScrcpyClient, AndroidKeyCode, AndroidKeyEventAction, AndroidMotionEventAction, CodecOptions, DEFAULT_SERVER_PATH, ScrcpyClient, ScrcpyLogLevel, ScrcpyOptions1_24, ScrcpyVideoOrientation, TinyH264Decoder, WebCodecsDecoder, type H264Decoder, type H264DecoderConstructor, type VideoStreamPacket } from "@yume-chan/scrcpy";
|
||||
import SCRCPY_SERVER_VERSION from '@yume-chan/scrcpy/bin/version';
|
||||
|
||||
import { DemoModePanel, DeviceView, DeviceViewRef, ExternalLink } from "../components";
|
||||
|
@ -223,6 +223,8 @@ const SettingItem = observer(function SettingItem({
|
|||
});
|
||||
|
||||
class ScrcpyPageState {
|
||||
adbScrcpyClient: AdbScrcpyClient | null = null;
|
||||
|
||||
running = false;
|
||||
|
||||
deviceView: DeviceViewRef | null = null;
|
||||
|
@ -252,7 +254,7 @@ class ScrcpyPageState {
|
|||
controller.close();
|
||||
},
|
||||
})
|
||||
.pipeTo(pushServer(GlobalState.device!));
|
||||
.pipeTo(this.adbScrcpyClient!.pushServer());
|
||||
}
|
||||
|
||||
encoders: string[] = [];
|
||||
|
@ -260,8 +262,7 @@ class ScrcpyPageState {
|
|||
try {
|
||||
await this.pushServer();
|
||||
|
||||
const encoders = await ScrcpyClient.getEncoders(
|
||||
GlobalState.device!,
|
||||
const encoders = await this.adbScrcpyClient!.getEncoders(
|
||||
DEFAULT_SERVER_PATH,
|
||||
SCRCPY_SERVER_VERSION,
|
||||
new ScrcpyOptions1_24({
|
||||
|
@ -296,8 +297,7 @@ class ScrcpyPageState {
|
|||
try {
|
||||
await this.pushServer();
|
||||
|
||||
const displays = await ScrcpyClient.getDisplays(
|
||||
GlobalState.device!,
|
||||
const displays = await this.adbScrcpyClient!.getDisplays(
|
||||
DEFAULT_SERVER_PATH,
|
||||
SCRCPY_SERVER_VERSION,
|
||||
new ScrcpyOptions1_24({
|
||||
|
@ -631,6 +631,8 @@ class ScrcpyPageState {
|
|||
autorun(() => {
|
||||
if (GlobalState.device) {
|
||||
runInAction(() => {
|
||||
this.adbScrcpyClient = new AdbScrcpyClient(GlobalState.device!);
|
||||
|
||||
this.encoders = [];
|
||||
this.settings.encoderName = undefined;
|
||||
|
||||
|
@ -721,7 +723,7 @@ class ScrcpyPageState {
|
|||
.pipeThrough(new ProgressStream(action((progress) => {
|
||||
this.serverUploadedSize = progress;
|
||||
})))
|
||||
.pipeTo(pushServer(GlobalState.device));
|
||||
.pipeTo(this.adbScrcpyClient!.pushServer());
|
||||
|
||||
runInAction(() => {
|
||||
this.serverUploadSpeed = this.serverUploadedSize - this.debouncedServerUploadedSize;
|
||||
|
@ -763,8 +765,7 @@ class ScrcpyPageState {
|
|||
this.log.push(`[client] Server arguments: ${options.formatServerArguments().join(' ')}`);
|
||||
});
|
||||
|
||||
const client = await ScrcpyClient.start(
|
||||
GlobalState.device,
|
||||
const client = await this.adbScrcpyClient!.start(
|
||||
DEFAULT_SERVER_PATH,
|
||||
SCRCPY_SERVER_VERSION,
|
||||
options
|
||||
|
|
85
common/config/rush/pnpm-lock.yaml
generated
85
common/config/rush/pnpm-lock.yaml
generated
|
@ -38,7 +38,6 @@ specifiers:
|
|||
eslint: 8.8.0
|
||||
eslint-config-next: 12.1.6
|
||||
file-loader: ^6.2.0
|
||||
gh-release-fetch: ^2.0.4
|
||||
jest: ^28.1.0
|
||||
json5: ^2.2.0
|
||||
mini-svg-data-uri: ^1.3.3
|
||||
|
@ -51,7 +50,6 @@ specifiers:
|
|||
react-dom: ^17.0.2
|
||||
source-map-loader: ^3.0.1
|
||||
streamsaver: ^2.0.5
|
||||
tinyh264: ^0.0.7
|
||||
ts-jest: ^28.0.2
|
||||
tslib: ^2.3.1
|
||||
typescript: 4.7.2
|
||||
|
@ -62,8 +60,6 @@ specifiers:
|
|||
xterm-addon-fit: ^0.5.0
|
||||
xterm-addon-search: ^0.8.2
|
||||
xterm-addon-webgl: ^0.11.4
|
||||
yuv-buffer: ^1.0.0
|
||||
yuv-canvas: ^1.2.7
|
||||
|
||||
dependencies:
|
||||
'@docusaurus/core': 2.0.0-beta.20_18c7f5bed56412102790ca2f4f7ad3a4
|
||||
|
@ -103,7 +99,6 @@ dependencies:
|
|||
eslint: 8.8.0
|
||||
eslint-config-next: 12.1.6_2286f1b09044d38231576974883dc607
|
||||
file-loader: 6.2.0
|
||||
gh-release-fetch: 2.0.6
|
||||
jest: 28.1.0_@types+node@17.0.33
|
||||
json5: 2.2.1
|
||||
mini-svg-data-uri: 1.4.4
|
||||
|
@ -116,7 +111,6 @@ dependencies:
|
|||
react-dom: 17.0.2_react@17.0.2
|
||||
source-map-loader: 3.0.1
|
||||
streamsaver: 2.0.6
|
||||
tinyh264: 0.0.7
|
||||
ts-jest: 28.0.2_jest@28.1.0+typescript@4.7.2
|
||||
tslib: 2.4.0
|
||||
typescript: 4.7.2
|
||||
|
@ -127,8 +121,6 @@ dependencies:
|
|||
xterm-addon-fit: 0.5.0_xterm@4.18.0
|
||||
xterm-addon-search: 0.8.2_xterm@4.18.0
|
||||
xterm-addon-webgl: 0.11.4_xterm@4.18.0
|
||||
yuv-buffer: 1.0.0
|
||||
yuv-canvas: 1.2.11
|
||||
|
||||
packages:
|
||||
|
||||
|
@ -3908,7 +3900,7 @@ packages:
|
|||
dev: false
|
||||
|
||||
/archive-type/4.0.0:
|
||||
resolution: {integrity: sha1-+S5yIzBW38aWlHJ0nCZ72wRrHXA=}
|
||||
resolution: {integrity: sha512-zV4Ky0v1F8dBrdYElwTvQhweQ0P7Kwc1aluqJsYtOBP01jXcWCyW2IEfI1YiqsG+Iy7ZR+o5LF1N+PGECBxHWA==}
|
||||
engines: {node: '>=4'}
|
||||
dependencies:
|
||||
file-type: 4.4.0
|
||||
|
@ -3994,7 +3986,7 @@ packages:
|
|||
dev: false
|
||||
|
||||
/asynckit/0.4.0:
|
||||
resolution: {integrity: sha1-x57Zf380y48robyXkLzDZkdLS3k=}
|
||||
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
|
||||
dev: false
|
||||
|
||||
/at-least-node/1.0.0:
|
||||
|
@ -4336,11 +4328,11 @@ packages:
|
|||
dev: false
|
||||
|
||||
/buffer-crc32/0.2.13:
|
||||
resolution: {integrity: sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=}
|
||||
resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==}
|
||||
dev: false
|
||||
|
||||
/buffer-fill/1.0.0:
|
||||
resolution: {integrity: sha1-+PeLdniYiO858gXNY39o5wISKyw=}
|
||||
resolution: {integrity: sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ==}
|
||||
dev: false
|
||||
|
||||
/buffer-from/1.1.2:
|
||||
|
@ -4365,7 +4357,7 @@ packages:
|
|||
dev: false
|
||||
|
||||
/cacheable-request/2.1.4:
|
||||
resolution: {integrity: sha1-DYCIAbY0KtM8kd+dC0TcCbkeXD0=}
|
||||
resolution: {integrity: sha512-vag0O2LKZ/najSoUwDbVlnlCFvhBE/7mGTY2B5FgCBDcRD+oVV1HYTOwM6JZfMg/hIcM6IwnTZ1uQQL5/X3xIQ==}
|
||||
dependencies:
|
||||
clone-response: 1.0.2
|
||||
get-stream: 3.0.0
|
||||
|
@ -5072,7 +5064,7 @@ packages:
|
|||
dev: false
|
||||
|
||||
/decode-uri-component/0.2.0:
|
||||
resolution: {integrity: sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=}
|
||||
resolution: {integrity: sha512-hjf+xovcEn31w/EUYdTXQh/8smFL/dzYjohQGEIgjyNavaJfBY2p5F527Bo1VPATxv0VYTUC2bOcXvqFwk78Og==}
|
||||
engines: {node: '>=0.10'}
|
||||
dev: false
|
||||
|
||||
|
@ -5113,7 +5105,7 @@ packages:
|
|||
dev: false
|
||||
|
||||
/decompress-unzip/4.0.1:
|
||||
resolution: {integrity: sha1-3qrM39FK6vhVePczroIQ+bSEj2k=}
|
||||
resolution: {integrity: sha512-1fqeluvxgnn86MOh66u8FjbtJpAFv5wgCT9Iw8rcBqQcCo5tO8eiJw7NNTrvt9n4CRBVq7CstiS922oPgyGLrw==}
|
||||
engines: {node: '>=4'}
|
||||
dependencies:
|
||||
file-type: 3.9.0
|
||||
|
@ -5193,7 +5185,7 @@ packages:
|
|||
dev: false
|
||||
|
||||
/delayed-stream/1.0.0:
|
||||
resolution: {integrity: sha1-3zrhmayt+31ECqrgsp4icrJOxhk=}
|
||||
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
|
||||
engines: {node: '>=0.4.0'}
|
||||
dev: false
|
||||
|
||||
|
@ -5988,7 +5980,7 @@ packages:
|
|||
dev: false
|
||||
|
||||
/fd-slicer/1.1.0:
|
||||
resolution: {integrity: sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=}
|
||||
resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==}
|
||||
dependencies:
|
||||
pend: 1.2.0
|
||||
dev: false
|
||||
|
@ -6034,17 +6026,17 @@ packages:
|
|||
dev: false
|
||||
|
||||
/file-type/3.9.0:
|
||||
resolution: {integrity: sha1-JXoHg4TR24CHvESdEH1SpSZyuek=}
|
||||
resolution: {integrity: sha512-RLoqTXE8/vPmMuTI88DAzhMYC99I8BWv7zYP4A1puo5HIjEJ5EX48ighy4ZyKMG9EDXxBgW6e++cn7d1xuFghA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
dev: false
|
||||
|
||||
/file-type/4.4.0:
|
||||
resolution: {integrity: sha1-G2AOX8ofvcboDApwxxyNul95BsU=}
|
||||
resolution: {integrity: sha512-f2UbFQEk7LXgWpi5ntcO86OeA/cC80fuDDDaX/fZ2ZGel+AF7leRQqBBW1eJNiiQkrZlAoM6P+VYP5P6bOlDEQ==}
|
||||
engines: {node: '>=4'}
|
||||
dev: false
|
||||
|
||||
/file-type/5.2.0:
|
||||
resolution: {integrity: sha1-LdvqfHP/42No365J3DOMBYwritY=}
|
||||
resolution: {integrity: sha512-Iq1nJ6D2+yIO4c8HHg4fyVb8mAJieo1Oloy1mLLaB2PvezNedhBVm+QU7g0qM42aiMbRXTxKKwGD17rjKNJYVQ==}
|
||||
engines: {node: '>=4'}
|
||||
dev: false
|
||||
|
||||
|
@ -6054,7 +6046,7 @@ packages:
|
|||
dev: false
|
||||
|
||||
/filename-reserved-regex/2.0.0:
|
||||
resolution: {integrity: sha1-q/c9+rc10EVECr/qLZHzieu/oik=}
|
||||
resolution: {integrity: sha512-lc1bnsSr4L4Bdif8Xb/qrtokGbq5zlsms/CYH8PP+WtCkGNF65DPiQY8vG3SakEdRn8Dlnm+gW/qWKKjS5sZzQ==}
|
||||
engines: {node: '>=4'}
|
||||
dev: false
|
||||
|
||||
|
@ -6221,7 +6213,7 @@ packages:
|
|||
dev: false
|
||||
|
||||
/from2/2.3.0:
|
||||
resolution: {integrity: sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8=}
|
||||
resolution: {integrity: sha512-OMcX/4IC/uqEPVgGeyfN22LJk6AZrMkRZHxcHBMBvHScDGgwTm2GT2Wkgtocyd3JfZffjj2kYUDXXII0Fk9W0g==}
|
||||
dependencies:
|
||||
inherits: 2.0.4
|
||||
readable-stream: 2.3.7
|
||||
|
@ -6316,7 +6308,7 @@ packages:
|
|||
dev: false
|
||||
|
||||
/get-stream/2.3.1:
|
||||
resolution: {integrity: sha1-Xzj5PzRgCWZu4BUKBUFn+Rvdld4=}
|
||||
resolution: {integrity: sha512-AUGhbbemXxrZJRD5cDvKtQxLuYaIbNtDTK8YqupCI393Q2KSTreEsLUN3ZxAWFGiKTzL6nKuzfcIvieflUX9qA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
dependencies:
|
||||
object-assign: 4.1.1
|
||||
|
@ -6324,7 +6316,7 @@ packages:
|
|||
dev: false
|
||||
|
||||
/get-stream/3.0.0:
|
||||
resolution: {integrity: sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=}
|
||||
resolution: {integrity: sha512-GlhdIUuVakc8SJ6kK0zAFbiGzRFzNnY4jUuEbV9UROo4Y+0Ny4fjvcZFVTeDA4odpFyOQzaw6hXukJSq/f28sQ==}
|
||||
engines: {node: '>=4'}
|
||||
dev: false
|
||||
|
||||
|
@ -6355,15 +6347,14 @@ packages:
|
|||
get-intrinsic: 1.1.1
|
||||
dev: false
|
||||
|
||||
/gh-release-fetch/2.0.6:
|
||||
resolution: {integrity: sha512-l+x05y91qHgdrh66TjM0ZQ+tg5tg6tY4nkMB1iZ+geWD6aMBGNe5//4JDIMFTiHEZPbo72my8FcK9i2WADWPCg==}
|
||||
engines: {node: '>=10'}
|
||||
/gh-release-fetch/3.0.2:
|
||||
resolution: {integrity: sha512-xcX1uaOVDvsm+io4bvJfBFpQCLfoI3DsFay2GBMUtEnNInbNFFZqxTh7X0WIorCDtOmtos5atp2BGHAGEzmlAg==}
|
||||
engines: {node: ^12.20.0 || ^14.14.0 || >=16.0.0}
|
||||
dependencies:
|
||||
'@types/download': 8.0.1
|
||||
'@types/node-fetch': 2.6.1
|
||||
'@types/semver': 7.3.9
|
||||
download: 8.0.0
|
||||
make-dir: 3.1.0
|
||||
node-fetch: 2.6.7
|
||||
semver: 7.3.7
|
||||
transitivePeerDependencies:
|
||||
|
@ -6965,7 +6956,7 @@ packages:
|
|||
dev: false
|
||||
|
||||
/into-stream/3.1.0:
|
||||
resolution: {integrity: sha1-lvsKk2wSur1v8XUqF9BWFqvQlMY=}
|
||||
resolution: {integrity: sha512-TcdjPibTksa1NQximqep2r17ISRiNE9fwlfbg3F8ANdvP5/yrFTew86VcO//jk4QTaMlbjypPBq76HN2zaKfZQ==}
|
||||
engines: {node: '>=4'}
|
||||
dependencies:
|
||||
from2: 2.3.0
|
||||
|
@ -7104,7 +7095,7 @@ packages:
|
|||
dev: false
|
||||
|
||||
/is-natural-number/4.0.1:
|
||||
resolution: {integrity: sha1-q5124dtM7VHjXeDHLr7PCfc0zeg=}
|
||||
resolution: {integrity: sha512-Y4LTamMe0DDQIIAlaer9eKebAlDSV6huy+TWhJVPlzZh2o4tRP5SQWFlLn5N0To4mDD22/qdOq+veo1cSISLgQ==}
|
||||
dev: false
|
||||
|
||||
/is-negative-zero/2.0.2:
|
||||
|
@ -7154,7 +7145,7 @@ packages:
|
|||
dev: false
|
||||
|
||||
/is-plain-obj/1.1.0:
|
||||
resolution: {integrity: sha1-caUMhCnfync8kqOQpKA7OfzVHT4=}
|
||||
resolution: {integrity: sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
dev: false
|
||||
|
||||
|
@ -7205,7 +7196,7 @@ packages:
|
|||
dev: false
|
||||
|
||||
/is-stream/1.1.0:
|
||||
resolution: {integrity: sha1-EtSj3U5o4Lec6428hBc66A2RykQ=}
|
||||
resolution: {integrity: sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
dev: false
|
||||
|
||||
|
@ -8045,7 +8036,7 @@ packages:
|
|||
dev: false
|
||||
|
||||
/lowercase-keys/1.0.0:
|
||||
resolution: {integrity: sha1-TjNms55/VFfjXxMkvfb4jQv8cwY=}
|
||||
resolution: {integrity: sha512-RPlX0+PHuvxVDZ7xX+EBVAp4RsVxP/TdDSN2mJYdiq1Lc4Hz7EUSjUI7RZrKKlmrIzVhf6Jo2stj7++gVarS0A==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
dev: false
|
||||
|
||||
|
@ -8593,12 +8584,12 @@ packages:
|
|||
dev: false
|
||||
|
||||
/p-finally/1.0.0:
|
||||
resolution: {integrity: sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=}
|
||||
resolution: {integrity: sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==}
|
||||
engines: {node: '>=4'}
|
||||
dev: false
|
||||
|
||||
/p-is-promise/1.1.0:
|
||||
resolution: {integrity: sha1-nJRWmJ6fZYgBewQ01WCXZ1w9oF4=}
|
||||
resolution: {integrity: sha512-zL7VE4JVS2IFSkR2GQKDSPEVxkoH43/p7oEnwpdCndKYJO0HVeRB7fA8TJwuLOTBREtK0ea8eHaxdwcpob5dmg==}
|
||||
engines: {node: '>=4'}
|
||||
dev: false
|
||||
|
||||
|
@ -8806,7 +8797,7 @@ packages:
|
|||
dev: false
|
||||
|
||||
/pend/1.2.0:
|
||||
resolution: {integrity: sha1-elfrVQpng/kRUzH89GY9XI4AelA=}
|
||||
resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==}
|
||||
dev: false
|
||||
|
||||
/picocolors/1.0.0:
|
||||
|
@ -8819,12 +8810,12 @@ packages:
|
|||
dev: false
|
||||
|
||||
/pify/2.3.0:
|
||||
resolution: {integrity: sha1-7RQaasBDqEnqWISY59yosVMw6Qw=}
|
||||
resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
dev: false
|
||||
|
||||
/pify/3.0.0:
|
||||
resolution: {integrity: sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=}
|
||||
resolution: {integrity: sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==}
|
||||
engines: {node: '>=4'}
|
||||
dev: false
|
||||
|
||||
|
@ -8834,14 +8825,14 @@ packages:
|
|||
dev: false
|
||||
|
||||
/pinkie-promise/2.0.1:
|
||||
resolution: {integrity: sha1-ITXW36ejWMBprJsXh3YogihFD/o=}
|
||||
resolution: {integrity: sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
dependencies:
|
||||
pinkie: 2.0.4
|
||||
dev: false
|
||||
|
||||
/pinkie/2.0.4:
|
||||
resolution: {integrity: sha1-clVrgM+g1IqXToDnckjoDtT3+HA=}
|
||||
resolution: {integrity: sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
dev: false
|
||||
|
||||
|
@ -10252,21 +10243,21 @@ packages:
|
|||
dev: false
|
||||
|
||||
/sort-keys-length/1.0.1:
|
||||
resolution: {integrity: sha1-nLb09OnkgVWmqgZx7dM2/xR5oYg=}
|
||||
resolution: {integrity: sha512-GRbEOUqCxemTAk/b32F2xa8wDTs+Z1QHOkbhJDQTvv/6G3ZkbJ+frYWsTcc7cBB3Fu4wy4XlLCuNtJuMn7Gsvw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
dependencies:
|
||||
sort-keys: 1.1.2
|
||||
dev: false
|
||||
|
||||
/sort-keys/1.1.2:
|
||||
resolution: {integrity: sha1-RBttTTRnmPG05J6JIK37oOVD+a0=}
|
||||
resolution: {integrity: sha512-vzn8aSqKgytVik0iwdBEi+zevbTYZogewTUM6dtpmGwEcdzbub/TX4bCzRhebDCRC3QzXgJsLRKB2V/Oof7HXg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
dependencies:
|
||||
is-plain-obj: 1.1.0
|
||||
dev: false
|
||||
|
||||
/sort-keys/2.0.0:
|
||||
resolution: {integrity: sha1-ZYU1WEhh7JfXMNbPQYIuH1ZoQSg=}
|
||||
resolution: {integrity: sha512-/dPCrG1s3ePpWm6yBbxZq5Be1dXGLyLn9Z791chDC3NFrpkVbWGzkBwPN1knaciexFXgRJ7hzdnwZ4stHSDmjg==}
|
||||
engines: {node: '>=4'}
|
||||
dependencies:
|
||||
is-plain-obj: 1.1.0
|
||||
|
@ -10391,7 +10382,7 @@ packages:
|
|||
dev: false
|
||||
|
||||
/strict-uri-encode/1.1.0:
|
||||
resolution: {integrity: sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=}
|
||||
resolution: {integrity: sha512-R3f198pcvnB+5IpnBlRkphuE9n46WyVl8I39W/ZUTZLz4nqSP/oLYUrcnJrw462Ds8he4YKMov2efsTIw1BDGQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
dev: false
|
||||
|
||||
|
@ -11784,7 +11775,7 @@ packages:
|
|||
dev: false
|
||||
|
||||
file:projects/demo.tgz_@mdx-js+react@1.6.22:
|
||||
resolution: {integrity: sha512-L7E5kSZb12r38EuntJ+rQjRmhduSBo0EANWMliv6GH6RQykgkVqHqscH1d4VvUM0KuVm5F0oeii1pJzymCdIrg==, tarball: file:projects/demo.tgz}
|
||||
resolution: {integrity: sha512-gl5GkBO/BxTaB9ztOpDJnkUtk9rdfKeVyZU0JUPYAKEfvhdPMgv+80lEL3WaUHb9zZMZ3Jy/Pslu74KL1PtsQg==, tarball: file:projects/demo.tgz}
|
||||
id: file:projects/demo.tgz
|
||||
name: '@rush-temp/demo'
|
||||
version: 0.0.0
|
||||
|
@ -11844,7 +11835,7 @@ packages:
|
|||
dev: false
|
||||
|
||||
file:projects/scrcpy.tgz_@types+node@17.0.33:
|
||||
resolution: {integrity: sha512-jCMSIItmtChytyjlkM594BanuzULuZ/Z9rVcQZxwh84HNtjqGnERhTR8zTUQTO6s/1QYgXaIKmtbDMauwAQTTg==, tarball: file:projects/scrcpy.tgz}
|
||||
resolution: {integrity: sha512-gWbbAg9u4YI8EMw0I5T7VUJlYhzGCP6cq3StmKWhqsSW2TjBECFNvmvjk7TB9BG3OohqjiFriDoEBElKqgLlgQ==, tarball: file:projects/scrcpy.tgz}
|
||||
id: file:projects/scrcpy.tgz
|
||||
name: '@rush-temp/scrcpy'
|
||||
version: 0.0.0
|
||||
|
@ -11852,7 +11843,7 @@ packages:
|
|||
'@jest/globals': 28.1.0
|
||||
'@types/dom-webcodecs': 0.1.4
|
||||
'@yume-chan/async': 2.1.4
|
||||
gh-release-fetch: 2.0.6
|
||||
gh-release-fetch: 3.0.2
|
||||
jest: 28.1.0_@types+node@17.0.33
|
||||
tinyh264: 0.0.7
|
||||
tslib: 2.4.0
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// DO NOT MODIFY THIS FILE MANUALLY BUT DO COMMIT IT. It is generated and used by Rush.
|
||||
{
|
||||
"pnpmShrinkwrapHash": "084447760927a169dd324c486492ee2effdc3820"
|
||||
"pnpmShrinkwrapHash": "3798fc5f6f6c673b1888e77923d3eeace5651923"
|
||||
}
|
||||
|
|
|
@ -2,17 +2,45 @@
|
|||
|
||||
TypeScript implementation of [Scrcpy](https://github.com/Genymobile/scrcpy) client.
|
||||
|
||||
It uses the official Scrcpy server releases.
|
||||
It's compatible with the official Scrcpy server binaries.
|
||||
|
||||
**WARNING:** The public API is UNSTABLE. If you have any questions, please open an issue.
|
||||
|
||||
## Download Server Binary
|
||||
- [Transport agnostic](#transport-agnostic)
|
||||
- [Prepare server binary](#prepare-server-binary)
|
||||
- [`fetch-scrcpy-server`](#fetch-scrcpy-server)
|
||||
- [Use the server binary](#use-the-server-binary)
|
||||
- [Node.js CommonJS](#nodejs-commonjs)
|
||||
- [Node.js ES module](#nodejs-es-module)
|
||||
- [Webpack 4](#webpack-4)
|
||||
- [Webpack 5](#webpack-5)
|
||||
- [Push and start server on device](#push-and-start-server-on-device)
|
||||
- [Using `@yume-chan/adb`](#using-yume-chanadb)
|
||||
- [Using other transportation](#using-other-transportation)
|
||||
- [Option versions](#option-versions)
|
||||
- [Consume the streams](#consume-the-streams)
|
||||
- [Video stream](#video-stream)
|
||||
- [Web Decoders](#web-decoders)
|
||||
- [WebCodecs decoder](#webcodecs-decoder)
|
||||
- [TinyH264 decoder](#tinyh264-decoder)
|
||||
|
||||
This package has a script `fetch-scrcpy-server` to help you download the official server binary.
|
||||
## Transport agnostic
|
||||
|
||||
Although it was initially written to use with `@yume-chan/adb`, the `ScrcpyClient` class can be used with any transportation. More details later.
|
||||
|
||||
## Prepare server binary
|
||||
|
||||
Scrcpy needs a server binary running on the device in order to work. This package doesn't ship with one.
|
||||
|
||||
You can download the server binary from official releases (https://github.com/Genymobile/scrcpy/releases) yourself, or use the built-in `fetch-scrcpy-server` script to automate the process.
|
||||
|
||||
The server binary is subject to [Apache License 2.0](https://github.com/Genymobile/scrcpy/blob/master/LICENSE).
|
||||
|
||||
Usage:
|
||||
### `fetch-scrcpy-server`
|
||||
|
||||
To use the script, first add `gh-release-fetch@3` to your `devDependencies`. It's not automatically installed to minimize download size.
|
||||
|
||||
Then you can execute it from terminal:
|
||||
|
||||
```
|
||||
$ npx fetch-scrcpy-server <version>
|
||||
|
@ -21,30 +49,164 @@ $ npx fetch-scrcpy-server <version>
|
|||
For example:
|
||||
|
||||
```
|
||||
$ npx fetch-scrcpy-server 1.21
|
||||
$ npx fetch-scrcpy-server 1.24
|
||||
```
|
||||
|
||||
You can also add it to the `postinstall` script of your `package.json` so it will run automatically when you do `npm install`:
|
||||
Or adding it to the `postinstall` script in `package.json`, so running `npm install` will automatically invoke the script.
|
||||
|
||||
```json
|
||||
"scripts": {
|
||||
"postinstall": "fetch-scrcpy-server 1.21",
|
||||
"postinstall": "fetch-scrcpy-server 1.24",
|
||||
},
|
||||
```
|
||||
|
||||
It will download the binary to `bin/scrcpy` and write the version string to `bin/version.js`. You can import the version string with
|
||||
The server binary will be named `bin/scrcpy-server`.
|
||||
|
||||
```js
|
||||
import SCRCPY_SERVER_VERSION from '@yume-chan/scrcpy/bin/version';
|
||||
### Use the server binary
|
||||
|
||||
The server binary file needs to be embedded in your application, the exact method depends on your runtime.
|
||||
|
||||
To name a few:
|
||||
|
||||
#### Node.js CommonJS
|
||||
|
||||
```ts
|
||||
const fs = require('fs');
|
||||
const path: string = require.resolve('@yume-chan/scrcpy/bin/scrcpy-server'); // Or your own server binary path
|
||||
const buffer: Buffer = fs.readFileSync(path);
|
||||
```
|
||||
|
||||
And import the server binary with [file-loader](https://v4.webpack.js.org/loaders/file-loader/) (Webpack 4) or [Asset Modules](https://webpack.js.org/guides/asset-modules/) (Webpack 5).
|
||||
#### Node.js ES module
|
||||
|
||||
```js
|
||||
import fs from 'node:fs/promises';
|
||||
import { createRequire } from 'node:module';
|
||||
|
||||
const path: string = createRequire(import.meta.url).resolve('@yume-chan/scrcpy/bin/scrcpy-server'); // Or your own server binary path
|
||||
const buffer: Buffer = await fs.readFile(path);
|
||||
```
|
||||
|
||||
In future it should be possible to use `import.meta.resolve` (https://nodejs.org/api/esm.html#importmetaresolvespecifier-parent) instead.
|
||||
|
||||
```ts
|
||||
const path: string = import.meta.resolve('@yume-chan/scrcpy/bin/scrcpy-server');
|
||||
```
|
||||
|
||||
#### Webpack 4
|
||||
|
||||
Requires installing and configuring file-loader (https://v4.webpack.js.org/loaders/file-loader/)
|
||||
|
||||
```ts
|
||||
import SCRCPY_SERVER_URL from '@yume-chan/scrcpy/bin/scrcpy-server'; // Or your own server binary path
|
||||
const buffer: ArrayBuffer = await fetch(SCRCPY_SERVER_URL).then(res => res.arrayBuffer());
|
||||
```
|
||||
|
||||
#### Webpack 5
|
||||
|
||||
Requires configuring Asset Modules (https://webpack.js.org/guides/asset-modules/)
|
||||
|
||||
```ts
|
||||
import SCRCPY_SERVER_URL from '@yume-chan/scrcpy/bin/scrcpy-server'; // Or your own server binary path
|
||||
const buffer: ArrayBuffer = await fetch(SCRCPY_SERVER_URL).then(res => res.arrayBuffer());
|
||||
```
|
||||
|
||||
## Push and start server on device
|
||||
|
||||
The the server binary needs to be copied to the device and run on it.
|
||||
|
||||
### Using `@yume-chan/adb`
|
||||
|
||||
The `Adb#sync()#write()` method can be used to push files to the device. Read more at `@yume-chan/adb`'s documentation (https://github.com/yume-chan/ya-webadb/tree/master/libraries/adb#readme).
|
||||
|
||||
This package also provides the `pushServer()` method as a shortcut for `Adb#sync().write()`, plus automatically close the `AdbSync` object when complete.
|
||||
|
||||
Example using `write()`:
|
||||
|
||||
```ts
|
||||
import { AdbScrcpyClient } from '@yume-chan/scrcpy';
|
||||
|
||||
const adbScrcpy = new AdbScrcpyClient(adb);
|
||||
const stream: WritableStream<Uint8Array> = adbScrcpy.pushServer();
|
||||
const writer = stream.getWriter();
|
||||
await writer.write(new Uint8Array(buffer));
|
||||
await writer.close();
|
||||
```
|
||||
|
||||
Example using `pipeTo()`:
|
||||
|
||||
```ts
|
||||
import { WrapReadableStream } from '@yume-chan/adb';
|
||||
import { AdbScrcpyClient } from '@yume-chan/scrcpy';
|
||||
|
||||
const adbScrcpy = new AdbScrcpyClient(adb);
|
||||
await fetch(SCRCPY_SERVER_URL)
|
||||
.then(response => new WrapReadableStream(response.body))
|
||||
.then(stream => stream.pipeTo(adbScrcpy.pushServer()))
|
||||
```
|
||||
|
||||
The `WrapReadableStream` is required because native `ReadableStream`s can't `pipeTo()` non-native `WritableStream`s (`@yume-chan/adb` is using ponyfill from `web-streams-polyfill`)
|
||||
|
||||
To start the server, use the `start()` method:
|
||||
|
||||
```js
|
||||
import { AdbScrcpyClient, DEFAULT_SERVER_PATH } from '@yume-chan/scrcpy';
|
||||
|
||||
const adbScrcpy = new AdbScrcpyClient(adb);
|
||||
const client: ScrcpyClient = await adbScrcpy.start(DEFAULT_SERVER_PATH, "1.24", new ScrcpyOptions1_24({
|
||||
// options
|
||||
}));
|
||||
```
|
||||
|
||||
The third argument is the server version. The server will refuse to start if it mismatches.
|
||||
|
||||
When using `fetch-scrcpy-server` to download server binary, the version string is saved to `bin/version.js`.
|
||||
|
||||
```js
|
||||
import SCRCPY_SERVER_VERSION from '@yume-chan/scrcpy/bin/version.js';
|
||||
|
||||
console.log(SCRCPY_SERVER_VERSION); // "1.24"
|
||||
```
|
||||
|
||||
### Using other transportation
|
||||
|
||||
You need to push and start the server yourself. After that, create the client using its constructor:
|
||||
|
||||
```ts
|
||||
import { ScrcpyClient } from '@yume-chan/scrcpy';
|
||||
|
||||
const stdout: ReadableStream<string>; // get the stream yourself
|
||||
const videoStream: ReadableStream<Uint8Array>; // get the stream yourself
|
||||
const controlStream: ReadableWritablePair<Uint8Array, Uint8Array> | undefined // get the stream yourself
|
||||
|
||||
const client = new ScrcpyClient(new ScrcpyOptions1_24({
|
||||
// options
|
||||
}), stdout, videoSteam, controlStream);
|
||||
```
|
||||
|
||||
Constrains:
|
||||
|
||||
1. The `stdout` stream will end when the server is closed.
|
||||
2. `cancel` the `stdout` will kill the server.
|
||||
3. `videoStream` will read from server's video socket, preserving packet boundaries.
|
||||
4. `controlStream.readable` will read from server's control socket.
|
||||
5. `controlStream.writable` will write to server's control socket.
|
||||
|
||||
The `controlStream` is optional if control is not enabled or handled elsewhere.
|
||||
|
||||
When the client is directly created, only the following methods in `options` will be used:
|
||||
|
||||
* `createVideoStreamTransformer()`
|
||||
* `getControlMessageTypes()`
|
||||
* `serializeInjectScrollControlMessage()`
|
||||
* `serializeBackOrScreenOnControlMessage()`
|
||||
|
||||
## Option versions
|
||||
|
||||
Scrcpy server has no backward compatibility on options input format. Currently the following versions are supported:
|
||||
Scrcpy server has many breaking changes between versions, so there is one option class for each version (range).
|
||||
|
||||
| versions | type |
|
||||
The latest one may continue to work for future server versions, but there is no guarantee.
|
||||
|
||||
| Version | Type |
|
||||
| --------- | ------------------- |
|
||||
| 1.16~1.17 | `ScrcpyOptions1_16` |
|
||||
| 1.18~1.20 | `ScrcpyOptions1_18` |
|
||||
|
@ -53,38 +215,94 @@ Scrcpy server has no backward compatibility on options input format. Currently t
|
|||
| 1.23 | `ScrcpyOptions1_23` |
|
||||
| 1.24 | `ScrcpyOptions1_24` |
|
||||
|
||||
You must use the correct type according to the server version.
|
||||
## Consume the streams
|
||||
|
||||
Both `stdout` and `videoStream` must be continuously read, otherwise the connection will stall.
|
||||
|
||||
```ts
|
||||
const abortController = new AbortController();
|
||||
|
||||
client.stdout
|
||||
.pipeTo(
|
||||
new WritableStream<string>({
|
||||
write: (line) => {
|
||||
// Handle the stdout line
|
||||
},
|
||||
}),
|
||||
{ signal: abortController.signal }
|
||||
)
|
||||
.catch(() => {})
|
||||
.then(() => {
|
||||
// Handle server exit
|
||||
});
|
||||
|
||||
client.videoStream.pipeTo(new WritableStream<VideoStreamPacket>({
|
||||
write: (packet) => {
|
||||
// Handle the video packet
|
||||
},
|
||||
}));
|
||||
|
||||
// to stop the server
|
||||
abortController.abort();
|
||||
```
|
||||
|
||||
## Video stream
|
||||
|
||||
The data from `onVideoData` event is a raw H.264 stream. You can process it as you want, or use the following built-in decoders to render it in browsers:
|
||||
The data from `videoStream` has two types: `configuration` and `frame`. How much parsed data is available depends on the server options.
|
||||
|
||||
* WebCodecs decoder: Uses the [WebCodecs API](https://developer.mozilla.org/en-US/docs/Web/API/WebCodecs_API). The video stream will be decoded into `VideoFrame`s and drawn on a 2D canvas.
|
||||
* TinyH264 decoder: TinyH264 compiles the old Android H.264 software decoder (now deprecated and removed) into WebAssembly, and wrap it in Web Worker to prevent blocking the main thread. The video stream will be decoded into YUV frames, then converted to RGB using a WebGL shader.
|
||||
```ts
|
||||
export interface VideoStreamConfigurationPacket {
|
||||
type: 'configuration';
|
||||
data: H264Configuration;
|
||||
}
|
||||
|
||||
export interface VideoStreamFramePacket {
|
||||
type: 'frame';
|
||||
keyframe?: boolean | undefined;
|
||||
pts?: bigint | undefined;
|
||||
data: Uint8Array;
|
||||
}
|
||||
```
|
||||
|
||||
When `sendFrameMeta: false` is set, `videoStream` only contains `frame` packets, and only the `data` field is available. It's commonly used when feeding into decoders like FFmpeg that can parse the H.264 stream itself, or saving to disk directly.
|
||||
|
||||
Otherwise, both `configuration` and `frame` packets are available.
|
||||
|
||||
* `configuration` packets contain the parsed SPS data, and can be used to initialize a video decoder.
|
||||
* `pts` (and `keyframe` field from server version 1.23) fields in `frame` packets are available to help decode the video.
|
||||
|
||||
## Web Decoders
|
||||
|
||||
There are two built-in decoders for using in Web Browsers:
|
||||
|
||||
| Name | Chrome | Firefox | Safari | Performance | Supported H.264 profile/level |
|
||||
| ----------------- | ------ | ------- | ------ | ------------------------------- | ----------------------------- |
|
||||
| WebCodecs decoder | 94 | No | No | High with Hardware acceleration | High level 5 |
|
||||
| TinyH264 decoder | 57 | 52 | 11 | Poor | Baseline level 4 |
|
||||
|
||||
TinyH264 decoder needs some extra setup:
|
||||
|
||||
1. `tinyh264`, `yuv-buffer` and `yuv-canvas` packages are peer dependencies. You must install them separately.
|
||||
2. The bundler you use must support the `new Worker(new URL('./worker.js', import.meta.url))` syntax. It's known to work with Webpack 5.
|
||||
|
||||
Example usage:
|
||||
General usage:
|
||||
|
||||
```ts
|
||||
const client = new ScrcpyClient(adb);
|
||||
const decoder = new WebCodecsDecoder(); // Or `new TinyH264Decoder()`
|
||||
client.onSizeChanged(size => decoder.setSize(size));
|
||||
client.onVideoData(data => decoder.feed(data));
|
||||
client.start(serverPath, serverVersion, new ScrcpyOptionsX_XX({
|
||||
...options,
|
||||
codecOptions: new CodecOptions({
|
||||
profile: decoder.maxProfile,
|
||||
level: decoder.maxLevel,
|
||||
}),
|
||||
}));
|
||||
const decoder = new H264Decoder(); // `WebCodecsDecoder` or `TinyH264Decoder`
|
||||
document.body.appendChild(decoder.element);
|
||||
|
||||
client.videoStream
|
||||
.pipeTo(decoder.writable)
|
||||
.catch(() => { });
|
||||
```
|
||||
|
||||
### WebCodecs decoder
|
||||
|
||||
Using the [WebCodecs API](https://developer.mozilla.org/en-US/docs/Web/API/WebCodecs_API). The video stream will be decoded into `VideoFrame`s and drawn onto a 2D canvas.
|
||||
|
||||
It has no dependencies and high compatibility/performance, but are only available on recent versions of Chrome.
|
||||
|
||||
### TinyH264 decoder
|
||||
|
||||
It's the old Android H.264 software decoder (now deprecated and removed), compiled into WebAssembly, and wrapped in Web Worker to prevent blocking the main thread.
|
||||
|
||||
The video stream will be decoded into YUV frames, then converted to RGB using a WebGL shader.
|
||||
|
||||
It depends on `tinyh264`, `yuv-buffer` and `yuv-canvas` packages, which are not automatically installed.
|
||||
|
||||
The bundler you use must also support the `new Worker(new URL('./worker.js', import.meta.url))` syntax. It's known to work with Webpack 5.
|
||||
|
|
|
@ -45,17 +45,28 @@
|
|||
"@jest/globals": "^28.1.0",
|
||||
"@types/dom-webcodecs": "^0.1.3",
|
||||
"@yume-chan/ts-package-builder": "^1.0.0",
|
||||
"gh-release-fetch": "^2.0.4",
|
||||
"jest": "^28.1.0",
|
||||
"tinyh264": "^0.0.7",
|
||||
"typescript": "4.7.2",
|
||||
"yuv-buffer": "^1.0.0",
|
||||
"yuv-canvas": "^1.2.7"
|
||||
"typescript": "4.7.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/dom-webcodecs": "^0.1.3",
|
||||
"tinyh264": "^0.0.6",
|
||||
"gh-release-fetch": "^3.0.2",
|
||||
"tinyh264": "^0.0.7",
|
||||
"yuv-buffer": "^1.0.0",
|
||||
"yuv-canvas": "^1.2.7"
|
||||
"yuv-canvas": "^1.2.11"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"gh-release-fetch": {
|
||||
"optional": true
|
||||
},
|
||||
"tinyh264": {
|
||||
"optional": true
|
||||
},
|
||||
"yuv-buffer": {
|
||||
"optional": true
|
||||
},
|
||||
"yuv-canvas": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
259
libraries/scrcpy/src/adb-client.ts
Normal file
259
libraries/scrcpy/src/adb-client.ts
Normal file
|
@ -0,0 +1,259 @@
|
|||
import { AdbCommandBase, AdbSubprocessNoneProtocol, AdbSubprocessProtocol, AdbSync, DecodeUtf8Stream, ReadableStream, TransformStream, WrapWritableStream, WritableStream } from "@yume-chan/adb";
|
||||
import { ScrcpyClient } from "./client.js";
|
||||
import { DEFAULT_SERVER_PATH, type ScrcpyOptions } from "./options/index.js";
|
||||
|
||||
function* splitLines(text: string): Generator<string, void, void> {
|
||||
let start = 0;
|
||||
|
||||
while (true) {
|
||||
const index = text.indexOf('\n', start);
|
||||
if (index === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const line = text.substring(start, index);
|
||||
yield line;
|
||||
|
||||
start = index + 1;
|
||||
}
|
||||
}
|
||||
|
||||
class SplitLinesStream extends TransformStream<string, string>{
|
||||
constructor() {
|
||||
super({
|
||||
transform(chunk, controller) {
|
||||
for (const line of splitLines(chunk)) {
|
||||
if (line === '') {
|
||||
continue;
|
||||
}
|
||||
controller.enqueue(line);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class ArrayToStream<T> extends ReadableStream<T>{
|
||||
private array!: T[];
|
||||
private index = 0;
|
||||
|
||||
constructor(array: T[]) {
|
||||
super({
|
||||
start: async () => {
|
||||
await Promise.resolve();
|
||||
this.array = array;
|
||||
},
|
||||
pull: (controller) => {
|
||||
if (this.index < this.array.length) {
|
||||
controller.enqueue(this.array[this.index]!);
|
||||
this.index += 1;
|
||||
} else {
|
||||
controller.close();
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class ConcatStream<T> extends ReadableStream<T>{
|
||||
private streams!: ReadableStream<T>[];
|
||||
private index = 0;
|
||||
private reader!: ReadableStreamDefaultReader<T>;
|
||||
|
||||
constructor(...streams: ReadableStream<T>[]) {
|
||||
super({
|
||||
start: async (controller) => {
|
||||
await Promise.resolve();
|
||||
|
||||
this.streams = streams;
|
||||
this.advance(controller);
|
||||
},
|
||||
pull: async (controller) => {
|
||||
const result = await this.reader.read();
|
||||
if (!result.done) {
|
||||
controller.enqueue(result.value);
|
||||
return;
|
||||
}
|
||||
this.advance(controller);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private advance(controller: ReadableStreamDefaultController<T>) {
|
||||
if (this.index < this.streams.length) {
|
||||
this.reader = this.streams[this.index]!.getReader();
|
||||
this.index += 1;
|
||||
} else {
|
||||
controller.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class AdbScrcpyClient extends AdbCommandBase {
|
||||
pushServer(
|
||||
path = DEFAULT_SERVER_PATH,
|
||||
) {
|
||||
let sync!: AdbSync;
|
||||
return new WrapWritableStream<Uint8Array>({
|
||||
start: async () => {
|
||||
sync = await this.adb.sync();
|
||||
return sync.write(path);
|
||||
},
|
||||
async close() {
|
||||
await sync.dispose();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async start(
|
||||
path: string,
|
||||
version: string,
|
||||
options: ScrcpyOptions<any>
|
||||
) {
|
||||
const connection = options.createConnection(this.adb);
|
||||
let process: AdbSubprocessProtocol | undefined;
|
||||
|
||||
try {
|
||||
await connection.initialize();
|
||||
|
||||
process = await this.adb.subprocess.spawn(
|
||||
[
|
||||
// cspell: disable-next-line
|
||||
`CLASSPATH=${path}`,
|
||||
'app_process',
|
||||
/* unused */ '/',
|
||||
'com.genymobile.scrcpy.Server',
|
||||
version,
|
||||
...options.formatServerArguments(),
|
||||
],
|
||||
{
|
||||
// Scrcpy server doesn't split stdout and stderr,
|
||||
// so disable Shell Protocol to simplify processing
|
||||
protocols: [AdbSubprocessNoneProtocol],
|
||||
}
|
||||
);
|
||||
|
||||
const stdout = process.stdout
|
||||
.pipeThrough(new DecodeUtf8Stream())
|
||||
.pipeThrough(new SplitLinesStream());
|
||||
|
||||
// Read stdout, otherwise `process.exit` won't resolve.
|
||||
const output: string[] = [];
|
||||
const abortController = new AbortController();
|
||||
const pipe = stdout
|
||||
.pipeTo(new WritableStream({
|
||||
write(chunk) {
|
||||
output.push(chunk);
|
||||
}
|
||||
}), {
|
||||
signal: abortController.signal,
|
||||
preventCancel: true,
|
||||
})
|
||||
.catch(() => { });
|
||||
|
||||
const result = await Promise.race([
|
||||
process.exit,
|
||||
connection.getStreams(),
|
||||
]);
|
||||
|
||||
if (typeof result === 'number') {
|
||||
const error = new Error('scrcpy server exited prematurely');
|
||||
(error as any).output = output;
|
||||
throw error;
|
||||
}
|
||||
|
||||
abortController.abort();
|
||||
await pipe;
|
||||
|
||||
const [videoStream, controlStream] = result;
|
||||
return new ScrcpyClient(
|
||||
options,
|
||||
new ConcatStream(
|
||||
new ArrayToStream(output),
|
||||
stdout,
|
||||
),
|
||||
videoStream,
|
||||
controlStream
|
||||
);
|
||||
} catch (e) {
|
||||
await process?.kill();
|
||||
throw e;
|
||||
} finally {
|
||||
connection.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method will modify the given `options`,
|
||||
* so don't reuse it elsewhere.
|
||||
*/
|
||||
public async getEncoders(
|
||||
path: string,
|
||||
version: string,
|
||||
options: ScrcpyOptions<any>
|
||||
): Promise<string[]> {
|
||||
// Provide an invalid encoder name
|
||||
// So the server will return all available encoders
|
||||
options.value.encoderName = '_';
|
||||
// Disable control for faster connection in 1.22+
|
||||
options.value.control = false;
|
||||
options.value.sendDeviceMeta = false;
|
||||
options.value.sendDummyByte = false;
|
||||
|
||||
// Scrcpy server will open connections, before initializing encoder
|
||||
// Thus although an invalid encoder name is given, the start process will success
|
||||
const client = await this.start(path, version, options);
|
||||
|
||||
const encoderNameRegex = options.getOutputEncoderNameRegex();
|
||||
const encoders: string[] = [];
|
||||
await client.stdout.pipeTo(new WritableStream({
|
||||
write(line) {
|
||||
const match = line.match(encoderNameRegex);
|
||||
if (match) {
|
||||
encoders.push(match[1]!);
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
return encoders;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method will modify the given `options`,
|
||||
* so don't reuse it elsewhere.
|
||||
*/
|
||||
async getDisplays(
|
||||
path: string,
|
||||
version: string,
|
||||
options: ScrcpyOptions<any>
|
||||
): Promise<number[]> {
|
||||
// Similar to `getEncoders`, pass an invalid option and parse the output
|
||||
options.value.displayId = -1;
|
||||
|
||||
options.value.control = false;
|
||||
options.value.sendDeviceMeta = false;
|
||||
options.value.sendDummyByte = false;
|
||||
|
||||
try {
|
||||
// Server will exit before opening connections when an invalid display id was given.
|
||||
await this.start(path, version, options);
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
const output = (e as any).output as string[];
|
||||
|
||||
const displayIdRegex = /\s+scrcpy --display (\d+)/;
|
||||
const displays: number[] = [];
|
||||
for (const line of output) {
|
||||
const match = line.match(displayIdRegex);
|
||||
if (match) {
|
||||
displays.push(Number.parseInt(match[1]!, 10));
|
||||
}
|
||||
}
|
||||
return displays;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('failed to get displays');
|
||||
}
|
||||
|
||||
}
|
|
@ -1,268 +1,24 @@
|
|||
import { AbortController, AdbBufferedStream, AdbSubprocessNoneProtocol, DecodeUtf8Stream, InspectStream, ReadableStream, TransformStream, WritableStream, type Adb, type AdbSocket, type AdbSubprocessProtocol, type WritableStreamDefaultWriter } from '@yume-chan/adb';
|
||||
import { AbortController, BufferedStream, InspectStream, ReadableStream, ReadableWritablePair, TransformStream, type WritableStreamDefaultWriter } from '@yume-chan/adb';
|
||||
import { EventEmitter } from '@yume-chan/event';
|
||||
import Struct from '@yume-chan/struct';
|
||||
import { AndroidMotionEventAction, ScrcpyControlMessageType, ScrcpyInjectKeyCodeControlMessage, ScrcpyInjectTextControlMessage, ScrcpyInjectTouchControlMessage, ScrcpySimpleControlMessage, type AndroidKeyEventAction } from './message.js';
|
||||
import type { ScrcpyInjectScrollControlMessage1_22, ScrcpyOptions, VideoStreamPacket } from "./options/index.js";
|
||||
|
||||
function* splitLines(text: string): Generator<string, void, void> {
|
||||
let start = 0;
|
||||
|
||||
while (true) {
|
||||
const index = text.indexOf('\n', start);
|
||||
if (index === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const line = text.substring(start, index);
|
||||
yield line;
|
||||
|
||||
start = index + 1;
|
||||
}
|
||||
}
|
||||
|
||||
class SplitLinesStream extends TransformStream<string, string>{
|
||||
constructor() {
|
||||
super({
|
||||
transform(chunk, controller) {
|
||||
for (const line of splitLines(chunk)) {
|
||||
if (line === '') {
|
||||
continue;
|
||||
}
|
||||
controller.enqueue(line);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class ArrayToStream<T> extends ReadableStream<T>{
|
||||
private array!: T[];
|
||||
private index = 0;
|
||||
|
||||
constructor(array: T[]) {
|
||||
super({
|
||||
start: async () => {
|
||||
await Promise.resolve();
|
||||
this.array = array;
|
||||
},
|
||||
pull: (controller) => {
|
||||
if (this.index < this.array.length) {
|
||||
controller.enqueue(this.array[this.index]!);
|
||||
this.index += 1;
|
||||
} else {
|
||||
controller.close();
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class ConcatStream<T> extends ReadableStream<T>{
|
||||
private streams!: ReadableStream<T>[];
|
||||
private index = 0;
|
||||
private reader!: ReadableStreamDefaultReader<T>;
|
||||
|
||||
constructor(...streams: ReadableStream<T>[]) {
|
||||
super({
|
||||
start: async (controller) => {
|
||||
await Promise.resolve();
|
||||
|
||||
this.streams = streams;
|
||||
this.advance(controller);
|
||||
},
|
||||
pull: async (controller) => {
|
||||
const result = await this.reader.read();
|
||||
if (!result.done) {
|
||||
controller.enqueue(result.value);
|
||||
return;
|
||||
}
|
||||
this.advance(controller);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private advance(controller: ReadableStreamDefaultController<T>) {
|
||||
if (this.index < this.streams.length) {
|
||||
this.reader = this.streams[this.index]!.getReader();
|
||||
this.index += 1;
|
||||
} else {
|
||||
controller.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const ClipboardMessage =
|
||||
new Struct()
|
||||
.uint32('length')
|
||||
.string('content', { lengthField: 'length' });
|
||||
|
||||
export class ScrcpyClient {
|
||||
/**
|
||||
* This method will modify the given `options`,
|
||||
* so don't reuse it elsewhere.
|
||||
*/
|
||||
public static async getEncoders(
|
||||
adb: Adb,
|
||||
path: string,
|
||||
version: string,
|
||||
options: ScrcpyOptions<any>
|
||||
): Promise<string[]> {
|
||||
// Provide an invalid encoder name
|
||||
// So the server will return all available encoders
|
||||
options.value.encoderName = '_';
|
||||
// Disable control for faster connection in 1.22+
|
||||
options.value.control = false;
|
||||
options.value.sendDeviceMeta = false;
|
||||
options.value.sendDummyByte = false;
|
||||
|
||||
// Scrcpy server will open connections, before initializing encoder
|
||||
// Thus although an invalid encoder name is given, the start process will success
|
||||
const client = await ScrcpyClient.start(adb, path, version, options);
|
||||
|
||||
const encoderNameRegex = options.getOutputEncoderNameRegex();
|
||||
const encoders: string[] = [];
|
||||
await client.stdout.pipeTo(new WritableStream({
|
||||
write(line) {
|
||||
const match = line.match(encoderNameRegex);
|
||||
if (match) {
|
||||
encoders.push(match[1]!);
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
return encoders;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method will modify the given `options`,
|
||||
* so don't reuse it elsewhere.
|
||||
*/
|
||||
public static async getDisplays(
|
||||
adb: Adb,
|
||||
path: string,
|
||||
version: string,
|
||||
options: ScrcpyOptions<any>
|
||||
): Promise<number[]> {
|
||||
// Similar to `getEncoders`, pass an invalid option and parse the output
|
||||
options.value.displayId = -1;
|
||||
|
||||
options.value.control = false;
|
||||
options.value.sendDeviceMeta = false;
|
||||
options.value.sendDummyByte = false;
|
||||
|
||||
try {
|
||||
// Server will exit before opening connections when an invalid display id was given.
|
||||
await ScrcpyClient.start(adb, path, version, options);
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
const output = (e as any).output as string[];
|
||||
|
||||
const displayIdRegex = /\s+scrcpy --display (\d+)/;
|
||||
const displays: number[] = [];
|
||||
for (const line of output) {
|
||||
const match = line.match(displayIdRegex);
|
||||
if (match) {
|
||||
displays.push(Number.parseInt(match[1]!, 10));
|
||||
}
|
||||
}
|
||||
return displays;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('failed to get displays');
|
||||
}
|
||||
|
||||
public static async start(
|
||||
adb: Adb,
|
||||
path: string,
|
||||
version: string,
|
||||
options: ScrcpyOptions<any>
|
||||
) {
|
||||
const connection = options.createConnection(adb);
|
||||
let process: AdbSubprocessProtocol | undefined;
|
||||
|
||||
try {
|
||||
await connection.initialize();
|
||||
|
||||
process = await adb.subprocess.spawn(
|
||||
[
|
||||
// cspell: disable-next-line
|
||||
`CLASSPATH=${path}`,
|
||||
'app_process',
|
||||
/* unused */ '/',
|
||||
'com.genymobile.scrcpy.Server',
|
||||
version,
|
||||
...options.formatServerArguments(),
|
||||
],
|
||||
{
|
||||
// Scrcpy server doesn't split stdout and stderr,
|
||||
// so disable Shell Protocol to simplify processing
|
||||
protocols: [AdbSubprocessNoneProtocol],
|
||||
}
|
||||
);
|
||||
|
||||
const stdout = process.stdout
|
||||
.pipeThrough(new DecodeUtf8Stream())
|
||||
.pipeThrough(new SplitLinesStream());
|
||||
|
||||
// Read stdout, otherwise `process.exit` won't resolve.
|
||||
const output: string[] = [];
|
||||
const abortController = new AbortController();
|
||||
const pipe = stdout
|
||||
.pipeTo(new WritableStream({
|
||||
write(chunk) {
|
||||
output.push(chunk);
|
||||
}
|
||||
}), {
|
||||
signal: abortController.signal,
|
||||
preventCancel: true,
|
||||
})
|
||||
.catch(() => { });
|
||||
|
||||
const result = await Promise.race([
|
||||
process.exit,
|
||||
connection.getStreams(),
|
||||
]);
|
||||
|
||||
if (typeof result === 'number') {
|
||||
const error = new Error('scrcpy server exited prematurely');
|
||||
(error as any).output = output;
|
||||
throw error;
|
||||
}
|
||||
|
||||
abortController.abort();
|
||||
await pipe;
|
||||
|
||||
const [videoStream, controlStream] = result;
|
||||
return new ScrcpyClient(
|
||||
adb,
|
||||
options,
|
||||
process,
|
||||
new ConcatStream(
|
||||
new ArrayToStream(output),
|
||||
stdout,
|
||||
),
|
||||
videoStream,
|
||||
controlStream
|
||||
);
|
||||
} catch (e) {
|
||||
await process?.kill();
|
||||
throw e;
|
||||
} finally {
|
||||
connection.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private _adb: Adb;
|
||||
public get adb() { return this._adb; }
|
||||
|
||||
private options: ScrcpyOptions<any>;
|
||||
private process: AdbSubprocessProtocol;
|
||||
|
||||
private _abortController = new AbortController();
|
||||
|
||||
private _stdout: ReadableStream<string>;
|
||||
public get stdout() { return this._stdout; }
|
||||
|
||||
public get exit() { return this.process.exit; }
|
||||
private _exit: Promise<void>;
|
||||
public get exit() { return this._exit; }
|
||||
|
||||
private _screenWidth: number | undefined;
|
||||
public get screenWidth() { return this._screenWidth; }
|
||||
|
@ -281,20 +37,26 @@ export class ScrcpyClient {
|
|||
private lastTouchMessage = 0;
|
||||
|
||||
public constructor(
|
||||
adb: Adb,
|
||||
options: ScrcpyOptions<any>,
|
||||
process: AdbSubprocessProtocol,
|
||||
stdout: ReadableStream<string>,
|
||||
videoStream: AdbSocket,
|
||||
controlStream: AdbSocket | undefined,
|
||||
videoStream: ReadableStream<Uint8Array>,
|
||||
controlStream: ReadableWritablePair<Uint8Array, Uint8Array> | undefined,
|
||||
) {
|
||||
this._adb = adb;
|
||||
this.options = options;
|
||||
this.process = process;
|
||||
|
||||
this._stdout = stdout;
|
||||
const transform = new TransformStream<string, string>();
|
||||
this._stdout = transform.readable;
|
||||
this._exit = stdout
|
||||
.pipeTo(
|
||||
transform.writable,
|
||||
{
|
||||
signal: this._abortController.signal,
|
||||
preventAbort: true,
|
||||
})
|
||||
.catch(() => { })
|
||||
.then(() => { transform.writable.close() });
|
||||
|
||||
this._videoStream = videoStream.readable
|
||||
this._videoStream = videoStream
|
||||
.pipeThrough(options.createVideoStreamTransformer())
|
||||
.pipeThrough(new InspectStream(packet => {
|
||||
if (packet.type === 'configuration') {
|
||||
|
@ -304,7 +66,7 @@ export class ScrcpyClient {
|
|||
}));
|
||||
|
||||
if (controlStream) {
|
||||
const buffered = new AdbBufferedStream(controlStream);
|
||||
const buffered = new BufferedStream(controlStream.readable);
|
||||
this._controlStreamWriter = controlStream.writable.getWriter();
|
||||
(async () => {
|
||||
try {
|
||||
|
@ -395,7 +157,7 @@ export class ScrcpyClient {
|
|||
return;
|
||||
}
|
||||
|
||||
const buffer = this.options!.serializeInjectScrollControlMessage({
|
||||
const buffer = this.options.serializeInjectScrollControlMessage({
|
||||
...message,
|
||||
type: this.getControlMessageTypeValue(ScrcpyControlMessageType.InjectScroll),
|
||||
screenWidth: this.screenWidth,
|
||||
|
@ -407,7 +169,7 @@ export class ScrcpyClient {
|
|||
public async pressBackOrTurnOnScreen(action: AndroidKeyEventAction) {
|
||||
const controlStream = this.checkControlStream('pressBackOrTurnOnScreen');
|
||||
|
||||
const buffer = this.options!.serializeBackOrScreenOnControlMessage({
|
||||
const buffer = this.options.serializeBackOrScreenOnControlMessage({
|
||||
type: this.getControlMessageTypeValue(ScrcpyControlMessageType.BackOrScreenOn),
|
||||
action,
|
||||
});
|
||||
|
@ -429,7 +191,7 @@ export class ScrcpyClient {
|
|||
}
|
||||
|
||||
public async close() {
|
||||
// No need to close streams. Kill the process will destroy them from the other side.
|
||||
await this.process?.kill();
|
||||
// No need to close streams. The device will close them when process was killed.
|
||||
this._abortController.abort();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import type { Adb, AdbSocket } from "@yume-chan/adb";
|
||||
import type { Adb, ReadableStream, ReadableWritablePair } from "@yume-chan/adb";
|
||||
import type { Disposable } from "@yume-chan/event";
|
||||
import type { ValueOrPromise } from "@yume-chan/struct";
|
||||
import { delay } from "./utils.js";
|
||||
|
@ -29,17 +29,17 @@ export abstract class ScrcpyClientConnection implements Disposable {
|
|||
|
||||
public initialize(): ValueOrPromise<void> { }
|
||||
|
||||
public abstract getStreams(): ValueOrPromise<[videoSteam: AdbSocket, controlStream: AdbSocket | undefined]>;
|
||||
public abstract getStreams(): ValueOrPromise<[videoSteam: ReadableStream<Uint8Array>, controlStream: ReadableWritablePair<Uint8Array, Uint8Array> | undefined]>;
|
||||
|
||||
public dispose(): void { }
|
||||
}
|
||||
|
||||
export class ScrcpyClientForwardConnection extends ScrcpyClientConnection {
|
||||
private async connect(): Promise<AdbSocket> {
|
||||
private async connect(): Promise<ReadableWritablePair<Uint8Array, Uint8Array>> {
|
||||
return await this.device.createSocket('localabstract:scrcpy');
|
||||
}
|
||||
|
||||
private async connectAndRetry(): Promise<AdbSocket> {
|
||||
private async connectAndRetry(): Promise<ReadableWritablePair<Uint8Array, Uint8Array>> {
|
||||
for (let i = 0; i < 100; i++) {
|
||||
try {
|
||||
return await this.connect();
|
||||
|
@ -50,10 +50,10 @@ export class ScrcpyClientForwardConnection extends ScrcpyClientConnection {
|
|||
throw new Error(`Can't connect to server after 100 retries`);
|
||||
}
|
||||
|
||||
private async connectVideoStream(): Promise<AdbSocket> {
|
||||
const stream = await this.connectAndRetry();
|
||||
private async connectVideoStream(): Promise<ReadableStream<Uint8Array>> {
|
||||
const { readable: videoStream } = await this.connectAndRetry();
|
||||
if (this.options.sendDummyByte) {
|
||||
const reader = stream.readable.getReader();
|
||||
const reader = videoStream.getReader();
|
||||
const { done, value } = await reader.read();
|
||||
// server will write a `0` to signal connection success
|
||||
if (done || value.byteLength !== 1 || value[0] !== 0) {
|
||||
|
@ -61,20 +61,20 @@ export class ScrcpyClientForwardConnection extends ScrcpyClientConnection {
|
|||
}
|
||||
reader.releaseLock();
|
||||
}
|
||||
return stream;
|
||||
return videoStream;
|
||||
}
|
||||
|
||||
public async getStreams(): Promise<[videoSteam: AdbSocket, controlStream: AdbSocket | undefined]> {
|
||||
public async getStreams(): Promise<[videoSteam: ReadableStream<Uint8Array>, controlStream: ReadableWritablePair<Uint8Array, Uint8Array> | undefined]> {
|
||||
const videoStream = await this.connectVideoStream();
|
||||
|
||||
let controlStream: AdbSocket | undefined;
|
||||
let controlStream: ReadableWritablePair<Uint8Array, Uint8Array> | undefined;
|
||||
if (this.options.control) {
|
||||
controlStream = await this.connectAndRetry();
|
||||
}
|
||||
|
||||
// Server only writes device meta after control socket is connected (if enabled)
|
||||
if (this.options.sendDeviceMeta) {
|
||||
const reader = videoStream.readable.getReader();
|
||||
const reader = videoStream.getReader();
|
||||
const { done, value } = await reader.read();
|
||||
// 64 bytes device name + 2 bytes video width + 2 bytes video height
|
||||
if (done || value.byteLength !== 64 + 2 + 2) {
|
||||
|
@ -88,7 +88,7 @@ export class ScrcpyClientForwardConnection extends ScrcpyClientConnection {
|
|||
}
|
||||
|
||||
export class ScrcpyClientReverseConnection extends ScrcpyClientConnection {
|
||||
private streams!: ReadableStreamDefaultReader<AdbSocket>;
|
||||
private streams!: ReadableStreamDefaultReader<ReadableWritablePair<Uint8Array, Uint8Array>>;
|
||||
|
||||
private address!: string;
|
||||
|
||||
|
@ -100,7 +100,7 @@ export class ScrcpyClientReverseConnection extends ScrcpyClientConnection {
|
|||
// ignore error
|
||||
}
|
||||
|
||||
const queue = new TransformStream<AdbSocket>();
|
||||
const queue = new TransformStream<ReadableWritablePair<Uint8Array, Uint8Array>>();
|
||||
this.streams = queue.readable.getReader();
|
||||
const writer = queue.writable.getWriter();
|
||||
this.address = await this.device.reverse.add(
|
||||
|
@ -113,21 +113,21 @@ export class ScrcpyClientReverseConnection extends ScrcpyClientConnection {
|
|||
);
|
||||
}
|
||||
|
||||
private async accept(): Promise<AdbSocket> {
|
||||
private async accept(): Promise<ReadableWritablePair<Uint8Array, Uint8Array>> {
|
||||
return (await this.streams.read()).value!;
|
||||
}
|
||||
|
||||
public async getStreams(): Promise<[videoSteam: AdbSocket, controlStream: AdbSocket | undefined]> {
|
||||
const videoStream = await this.accept();
|
||||
public async getStreams(): Promise<[videoSteam: ReadableStream<Uint8Array>, controlStream: ReadableWritablePair<Uint8Array, Uint8Array> | undefined]> {
|
||||
const { readable: videoStream } = await this.accept();
|
||||
|
||||
let controlStream: AdbSocket | undefined;
|
||||
let controlStream: ReadableWritablePair<Uint8Array, Uint8Array> | undefined;
|
||||
if (this.options.control) {
|
||||
controlStream = await this.accept();
|
||||
}
|
||||
|
||||
// Server only writes device meta after control socket is connected (if enabled)
|
||||
if (this.options.sendDeviceMeta) {
|
||||
const reader = videoStream.readable.getReader();
|
||||
const reader = videoStream.getReader();
|
||||
const { done, value } = await reader.read();
|
||||
// 64 bytes device name + 2 bytes video width + 2 bytes video height
|
||||
if (done || value.byteLength !== 64 + 2 + 2) {
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
export * from './adb-client.js';
|
||||
export * from './client.js';
|
||||
export * from './codec.js';
|
||||
export * from './connection.js';
|
||||
export * from './decoder/index.js';
|
||||
export * from './message.js';
|
||||
export * from './options/index.js';
|
||||
export * from './push-server.js';
|
||||
export * from './utils.js';
|
||||
|
|
|
@ -1,24 +0,0 @@
|
|||
import { WrapWritableStream, type Adb, type AdbSync } from "@yume-chan/adb";
|
||||
import { DEFAULT_SERVER_PATH } from "./options/index.js";
|
||||
|
||||
export interface PushServerOptions {
|
||||
path?: string;
|
||||
}
|
||||
|
||||
export function pushServer(
|
||||
device: Adb,
|
||||
options: PushServerOptions = {}
|
||||
) {
|
||||
const { path = DEFAULT_SERVER_PATH } = options;
|
||||
|
||||
let sync!: AdbSync;
|
||||
return new WrapWritableStream<Uint8Array>({
|
||||
async start() {
|
||||
sync = await device.sync();
|
||||
return sync.write(path);
|
||||
},
|
||||
async close() {
|
||||
await sync.dispose();
|
||||
},
|
||||
});
|
||||
}
|
|
@ -17,7 +17,7 @@
|
|||
"path": "../event/tsconfig.json"
|
||||
},
|
||||
{
|
||||
"path": "../struct/tsconfig.json"
|
||||
"path": "../struct/tsconfig.build.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue