refactor(scrcpy): separate parts depend on ADB

This commit is contained in:
Simon Chan 2022-06-09 16:32:22 +08:00
parent 6158745ef5
commit ce8f062e96
12 changed files with 623 additions and 404 deletions

View file

@ -28,6 +28,7 @@
"Nalu", "Nalu",
"opendir", "opendir",
"PKCS", "PKCS",
"ponyfill",
"runtimes", "runtimes",
"Scrcpy", "Scrcpy",
"sendrecv", "sendrecv",

View file

@ -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 { ADB_SYNC_MAX_PACKET_SIZE, ChunkStream, InspectStream, ReadableStream, WritableStream } from '@yume-chan/adb';
import { EventEmitter } from "@yume-chan/event"; 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 SCRCPY_SERVER_VERSION from '@yume-chan/scrcpy/bin/version';
import { DemoModePanel, DeviceView, DeviceViewRef, ExternalLink } from "../components"; import { DemoModePanel, DeviceView, DeviceViewRef, ExternalLink } from "../components";
@ -223,6 +223,8 @@ const SettingItem = observer(function SettingItem({
}); });
class ScrcpyPageState { class ScrcpyPageState {
adbScrcpyClient: AdbScrcpyClient | null = null;
running = false; running = false;
deviceView: DeviceViewRef | null = null; deviceView: DeviceViewRef | null = null;
@ -252,7 +254,7 @@ class ScrcpyPageState {
controller.close(); controller.close();
}, },
}) })
.pipeTo(pushServer(GlobalState.device!)); .pipeTo(this.adbScrcpyClient!.pushServer());
} }
encoders: string[] = []; encoders: string[] = [];
@ -260,8 +262,7 @@ class ScrcpyPageState {
try { try {
await this.pushServer(); await this.pushServer();
const encoders = await ScrcpyClient.getEncoders( const encoders = await this.adbScrcpyClient!.getEncoders(
GlobalState.device!,
DEFAULT_SERVER_PATH, DEFAULT_SERVER_PATH,
SCRCPY_SERVER_VERSION, SCRCPY_SERVER_VERSION,
new ScrcpyOptions1_24({ new ScrcpyOptions1_24({
@ -296,8 +297,7 @@ class ScrcpyPageState {
try { try {
await this.pushServer(); await this.pushServer();
const displays = await ScrcpyClient.getDisplays( const displays = await this.adbScrcpyClient!.getDisplays(
GlobalState.device!,
DEFAULT_SERVER_PATH, DEFAULT_SERVER_PATH,
SCRCPY_SERVER_VERSION, SCRCPY_SERVER_VERSION,
new ScrcpyOptions1_24({ new ScrcpyOptions1_24({
@ -631,6 +631,8 @@ class ScrcpyPageState {
autorun(() => { autorun(() => {
if (GlobalState.device) { if (GlobalState.device) {
runInAction(() => { runInAction(() => {
this.adbScrcpyClient = new AdbScrcpyClient(GlobalState.device!);
this.encoders = []; this.encoders = [];
this.settings.encoderName = undefined; this.settings.encoderName = undefined;
@ -721,7 +723,7 @@ class ScrcpyPageState {
.pipeThrough(new ProgressStream(action((progress) => { .pipeThrough(new ProgressStream(action((progress) => {
this.serverUploadedSize = progress; this.serverUploadedSize = progress;
}))) })))
.pipeTo(pushServer(GlobalState.device)); .pipeTo(this.adbScrcpyClient!.pushServer());
runInAction(() => { runInAction(() => {
this.serverUploadSpeed = this.serverUploadedSize - this.debouncedServerUploadedSize; this.serverUploadSpeed = this.serverUploadedSize - this.debouncedServerUploadedSize;
@ -763,8 +765,7 @@ class ScrcpyPageState {
this.log.push(`[client] Server arguments: ${options.formatServerArguments().join(' ')}`); this.log.push(`[client] Server arguments: ${options.formatServerArguments().join(' ')}`);
}); });
const client = await ScrcpyClient.start( const client = await this.adbScrcpyClient!.start(
GlobalState.device,
DEFAULT_SERVER_PATH, DEFAULT_SERVER_PATH,
SCRCPY_SERVER_VERSION, SCRCPY_SERVER_VERSION,
options options

View file

@ -38,7 +38,6 @@ specifiers:
eslint: 8.8.0 eslint: 8.8.0
eslint-config-next: 12.1.6 eslint-config-next: 12.1.6
file-loader: ^6.2.0 file-loader: ^6.2.0
gh-release-fetch: ^2.0.4
jest: ^28.1.0 jest: ^28.1.0
json5: ^2.2.0 json5: ^2.2.0
mini-svg-data-uri: ^1.3.3 mini-svg-data-uri: ^1.3.3
@ -51,7 +50,6 @@ specifiers:
react-dom: ^17.0.2 react-dom: ^17.0.2
source-map-loader: ^3.0.1 source-map-loader: ^3.0.1
streamsaver: ^2.0.5 streamsaver: ^2.0.5
tinyh264: ^0.0.7
ts-jest: ^28.0.2 ts-jest: ^28.0.2
tslib: ^2.3.1 tslib: ^2.3.1
typescript: 4.7.2 typescript: 4.7.2
@ -62,8 +60,6 @@ specifiers:
xterm-addon-fit: ^0.5.0 xterm-addon-fit: ^0.5.0
xterm-addon-search: ^0.8.2 xterm-addon-search: ^0.8.2
xterm-addon-webgl: ^0.11.4 xterm-addon-webgl: ^0.11.4
yuv-buffer: ^1.0.0
yuv-canvas: ^1.2.7
dependencies: dependencies:
'@docusaurus/core': 2.0.0-beta.20_18c7f5bed56412102790ca2f4f7ad3a4 '@docusaurus/core': 2.0.0-beta.20_18c7f5bed56412102790ca2f4f7ad3a4
@ -103,7 +99,6 @@ dependencies:
eslint: 8.8.0 eslint: 8.8.0
eslint-config-next: 12.1.6_2286f1b09044d38231576974883dc607 eslint-config-next: 12.1.6_2286f1b09044d38231576974883dc607
file-loader: 6.2.0 file-loader: 6.2.0
gh-release-fetch: 2.0.6
jest: 28.1.0_@types+node@17.0.33 jest: 28.1.0_@types+node@17.0.33
json5: 2.2.1 json5: 2.2.1
mini-svg-data-uri: 1.4.4 mini-svg-data-uri: 1.4.4
@ -116,7 +111,6 @@ dependencies:
react-dom: 17.0.2_react@17.0.2 react-dom: 17.0.2_react@17.0.2
source-map-loader: 3.0.1 source-map-loader: 3.0.1
streamsaver: 2.0.6 streamsaver: 2.0.6
tinyh264: 0.0.7
ts-jest: 28.0.2_jest@28.1.0+typescript@4.7.2 ts-jest: 28.0.2_jest@28.1.0+typescript@4.7.2
tslib: 2.4.0 tslib: 2.4.0
typescript: 4.7.2 typescript: 4.7.2
@ -127,8 +121,6 @@ dependencies:
xterm-addon-fit: 0.5.0_xterm@4.18.0 xterm-addon-fit: 0.5.0_xterm@4.18.0
xterm-addon-search: 0.8.2_xterm@4.18.0 xterm-addon-search: 0.8.2_xterm@4.18.0
xterm-addon-webgl: 0.11.4_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: packages:
@ -3908,7 +3900,7 @@ packages:
dev: false dev: false
/archive-type/4.0.0: /archive-type/4.0.0:
resolution: {integrity: sha1-+S5yIzBW38aWlHJ0nCZ72wRrHXA=} resolution: {integrity: sha512-zV4Ky0v1F8dBrdYElwTvQhweQ0P7Kwc1aluqJsYtOBP01jXcWCyW2IEfI1YiqsG+Iy7ZR+o5LF1N+PGECBxHWA==}
engines: {node: '>=4'} engines: {node: '>=4'}
dependencies: dependencies:
file-type: 4.4.0 file-type: 4.4.0
@ -3994,7 +3986,7 @@ packages:
dev: false dev: false
/asynckit/0.4.0: /asynckit/0.4.0:
resolution: {integrity: sha1-x57Zf380y48robyXkLzDZkdLS3k=} resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
dev: false dev: false
/at-least-node/1.0.0: /at-least-node/1.0.0:
@ -4336,11 +4328,11 @@ packages:
dev: false dev: false
/buffer-crc32/0.2.13: /buffer-crc32/0.2.13:
resolution: {integrity: sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=} resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==}
dev: false dev: false
/buffer-fill/1.0.0: /buffer-fill/1.0.0:
resolution: {integrity: sha1-+PeLdniYiO858gXNY39o5wISKyw=} resolution: {integrity: sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ==}
dev: false dev: false
/buffer-from/1.1.2: /buffer-from/1.1.2:
@ -4365,7 +4357,7 @@ packages:
dev: false dev: false
/cacheable-request/2.1.4: /cacheable-request/2.1.4:
resolution: {integrity: sha1-DYCIAbY0KtM8kd+dC0TcCbkeXD0=} resolution: {integrity: sha512-vag0O2LKZ/najSoUwDbVlnlCFvhBE/7mGTY2B5FgCBDcRD+oVV1HYTOwM6JZfMg/hIcM6IwnTZ1uQQL5/X3xIQ==}
dependencies: dependencies:
clone-response: 1.0.2 clone-response: 1.0.2
get-stream: 3.0.0 get-stream: 3.0.0
@ -5072,7 +5064,7 @@ packages:
dev: false dev: false
/decode-uri-component/0.2.0: /decode-uri-component/0.2.0:
resolution: {integrity: sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=} resolution: {integrity: sha512-hjf+xovcEn31w/EUYdTXQh/8smFL/dzYjohQGEIgjyNavaJfBY2p5F527Bo1VPATxv0VYTUC2bOcXvqFwk78Og==}
engines: {node: '>=0.10'} engines: {node: '>=0.10'}
dev: false dev: false
@ -5113,7 +5105,7 @@ packages:
dev: false dev: false
/decompress-unzip/4.0.1: /decompress-unzip/4.0.1:
resolution: {integrity: sha1-3qrM39FK6vhVePczroIQ+bSEj2k=} resolution: {integrity: sha512-1fqeluvxgnn86MOh66u8FjbtJpAFv5wgCT9Iw8rcBqQcCo5tO8eiJw7NNTrvt9n4CRBVq7CstiS922oPgyGLrw==}
engines: {node: '>=4'} engines: {node: '>=4'}
dependencies: dependencies:
file-type: 3.9.0 file-type: 3.9.0
@ -5193,7 +5185,7 @@ packages:
dev: false dev: false
/delayed-stream/1.0.0: /delayed-stream/1.0.0:
resolution: {integrity: sha1-3zrhmayt+31ECqrgsp4icrJOxhk=} resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
engines: {node: '>=0.4.0'} engines: {node: '>=0.4.0'}
dev: false dev: false
@ -5988,7 +5980,7 @@ packages:
dev: false dev: false
/fd-slicer/1.1.0: /fd-slicer/1.1.0:
resolution: {integrity: sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=} resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==}
dependencies: dependencies:
pend: 1.2.0 pend: 1.2.0
dev: false dev: false
@ -6034,17 +6026,17 @@ packages:
dev: false dev: false
/file-type/3.9.0: /file-type/3.9.0:
resolution: {integrity: sha1-JXoHg4TR24CHvESdEH1SpSZyuek=} resolution: {integrity: sha512-RLoqTXE8/vPmMuTI88DAzhMYC99I8BWv7zYP4A1puo5HIjEJ5EX48ighy4ZyKMG9EDXxBgW6e++cn7d1xuFghA==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
dev: false dev: false
/file-type/4.4.0: /file-type/4.4.0:
resolution: {integrity: sha1-G2AOX8ofvcboDApwxxyNul95BsU=} resolution: {integrity: sha512-f2UbFQEk7LXgWpi5ntcO86OeA/cC80fuDDDaX/fZ2ZGel+AF7leRQqBBW1eJNiiQkrZlAoM6P+VYP5P6bOlDEQ==}
engines: {node: '>=4'} engines: {node: '>=4'}
dev: false dev: false
/file-type/5.2.0: /file-type/5.2.0:
resolution: {integrity: sha1-LdvqfHP/42No365J3DOMBYwritY=} resolution: {integrity: sha512-Iq1nJ6D2+yIO4c8HHg4fyVb8mAJieo1Oloy1mLLaB2PvezNedhBVm+QU7g0qM42aiMbRXTxKKwGD17rjKNJYVQ==}
engines: {node: '>=4'} engines: {node: '>=4'}
dev: false dev: false
@ -6054,7 +6046,7 @@ packages:
dev: false dev: false
/filename-reserved-regex/2.0.0: /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'} engines: {node: '>=4'}
dev: false dev: false
@ -6221,7 +6213,7 @@ packages:
dev: false dev: false
/from2/2.3.0: /from2/2.3.0:
resolution: {integrity: sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8=} resolution: {integrity: sha512-OMcX/4IC/uqEPVgGeyfN22LJk6AZrMkRZHxcHBMBvHScDGgwTm2GT2Wkgtocyd3JfZffjj2kYUDXXII0Fk9W0g==}
dependencies: dependencies:
inherits: 2.0.4 inherits: 2.0.4
readable-stream: 2.3.7 readable-stream: 2.3.7
@ -6316,7 +6308,7 @@ packages:
dev: false dev: false
/get-stream/2.3.1: /get-stream/2.3.1:
resolution: {integrity: sha1-Xzj5PzRgCWZu4BUKBUFn+Rvdld4=} resolution: {integrity: sha512-AUGhbbemXxrZJRD5cDvKtQxLuYaIbNtDTK8YqupCI393Q2KSTreEsLUN3ZxAWFGiKTzL6nKuzfcIvieflUX9qA==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
dependencies: dependencies:
object-assign: 4.1.1 object-assign: 4.1.1
@ -6324,7 +6316,7 @@ packages:
dev: false dev: false
/get-stream/3.0.0: /get-stream/3.0.0:
resolution: {integrity: sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=} resolution: {integrity: sha512-GlhdIUuVakc8SJ6kK0zAFbiGzRFzNnY4jUuEbV9UROo4Y+0Ny4fjvcZFVTeDA4odpFyOQzaw6hXukJSq/f28sQ==}
engines: {node: '>=4'} engines: {node: '>=4'}
dev: false dev: false
@ -6355,15 +6347,14 @@ packages:
get-intrinsic: 1.1.1 get-intrinsic: 1.1.1
dev: false dev: false
/gh-release-fetch/2.0.6: /gh-release-fetch/3.0.2:
resolution: {integrity: sha512-l+x05y91qHgdrh66TjM0ZQ+tg5tg6tY4nkMB1iZ+geWD6aMBGNe5//4JDIMFTiHEZPbo72my8FcK9i2WADWPCg==} resolution: {integrity: sha512-xcX1uaOVDvsm+io4bvJfBFpQCLfoI3DsFay2GBMUtEnNInbNFFZqxTh7X0WIorCDtOmtos5atp2BGHAGEzmlAg==}
engines: {node: '>=10'} engines: {node: ^12.20.0 || ^14.14.0 || >=16.0.0}
dependencies: dependencies:
'@types/download': 8.0.1 '@types/download': 8.0.1
'@types/node-fetch': 2.6.1 '@types/node-fetch': 2.6.1
'@types/semver': 7.3.9 '@types/semver': 7.3.9
download: 8.0.0 download: 8.0.0
make-dir: 3.1.0
node-fetch: 2.6.7 node-fetch: 2.6.7
semver: 7.3.7 semver: 7.3.7
transitivePeerDependencies: transitivePeerDependencies:
@ -6965,7 +6956,7 @@ packages:
dev: false dev: false
/into-stream/3.1.0: /into-stream/3.1.0:
resolution: {integrity: sha1-lvsKk2wSur1v8XUqF9BWFqvQlMY=} resolution: {integrity: sha512-TcdjPibTksa1NQximqep2r17ISRiNE9fwlfbg3F8ANdvP5/yrFTew86VcO//jk4QTaMlbjypPBq76HN2zaKfZQ==}
engines: {node: '>=4'} engines: {node: '>=4'}
dependencies: dependencies:
from2: 2.3.0 from2: 2.3.0
@ -7104,7 +7095,7 @@ packages:
dev: false dev: false
/is-natural-number/4.0.1: /is-natural-number/4.0.1:
resolution: {integrity: sha1-q5124dtM7VHjXeDHLr7PCfc0zeg=} resolution: {integrity: sha512-Y4LTamMe0DDQIIAlaer9eKebAlDSV6huy+TWhJVPlzZh2o4tRP5SQWFlLn5N0To4mDD22/qdOq+veo1cSISLgQ==}
dev: false dev: false
/is-negative-zero/2.0.2: /is-negative-zero/2.0.2:
@ -7154,7 +7145,7 @@ packages:
dev: false dev: false
/is-plain-obj/1.1.0: /is-plain-obj/1.1.0:
resolution: {integrity: sha1-caUMhCnfync8kqOQpKA7OfzVHT4=} resolution: {integrity: sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
dev: false dev: false
@ -7205,7 +7196,7 @@ packages:
dev: false dev: false
/is-stream/1.1.0: /is-stream/1.1.0:
resolution: {integrity: sha1-EtSj3U5o4Lec6428hBc66A2RykQ=} resolution: {integrity: sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
dev: false dev: false
@ -8045,7 +8036,7 @@ packages:
dev: false dev: false
/lowercase-keys/1.0.0: /lowercase-keys/1.0.0:
resolution: {integrity: sha1-TjNms55/VFfjXxMkvfb4jQv8cwY=} resolution: {integrity: sha512-RPlX0+PHuvxVDZ7xX+EBVAp4RsVxP/TdDSN2mJYdiq1Lc4Hz7EUSjUI7RZrKKlmrIzVhf6Jo2stj7++gVarS0A==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
dev: false dev: false
@ -8593,12 +8584,12 @@ packages:
dev: false dev: false
/p-finally/1.0.0: /p-finally/1.0.0:
resolution: {integrity: sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=} resolution: {integrity: sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==}
engines: {node: '>=4'} engines: {node: '>=4'}
dev: false dev: false
/p-is-promise/1.1.0: /p-is-promise/1.1.0:
resolution: {integrity: sha1-nJRWmJ6fZYgBewQ01WCXZ1w9oF4=} resolution: {integrity: sha512-zL7VE4JVS2IFSkR2GQKDSPEVxkoH43/p7oEnwpdCndKYJO0HVeRB7fA8TJwuLOTBREtK0ea8eHaxdwcpob5dmg==}
engines: {node: '>=4'} engines: {node: '>=4'}
dev: false dev: false
@ -8806,7 +8797,7 @@ packages:
dev: false dev: false
/pend/1.2.0: /pend/1.2.0:
resolution: {integrity: sha1-elfrVQpng/kRUzH89GY9XI4AelA=} resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==}
dev: false dev: false
/picocolors/1.0.0: /picocolors/1.0.0:
@ -8819,12 +8810,12 @@ packages:
dev: false dev: false
/pify/2.3.0: /pify/2.3.0:
resolution: {integrity: sha1-7RQaasBDqEnqWISY59yosVMw6Qw=} resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
dev: false dev: false
/pify/3.0.0: /pify/3.0.0:
resolution: {integrity: sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=} resolution: {integrity: sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==}
engines: {node: '>=4'} engines: {node: '>=4'}
dev: false dev: false
@ -8834,14 +8825,14 @@ packages:
dev: false dev: false
/pinkie-promise/2.0.1: /pinkie-promise/2.0.1:
resolution: {integrity: sha1-ITXW36ejWMBprJsXh3YogihFD/o=} resolution: {integrity: sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
dependencies: dependencies:
pinkie: 2.0.4 pinkie: 2.0.4
dev: false dev: false
/pinkie/2.0.4: /pinkie/2.0.4:
resolution: {integrity: sha1-clVrgM+g1IqXToDnckjoDtT3+HA=} resolution: {integrity: sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
dev: false dev: false
@ -10252,21 +10243,21 @@ packages:
dev: false dev: false
/sort-keys-length/1.0.1: /sort-keys-length/1.0.1:
resolution: {integrity: sha1-nLb09OnkgVWmqgZx7dM2/xR5oYg=} resolution: {integrity: sha512-GRbEOUqCxemTAk/b32F2xa8wDTs+Z1QHOkbhJDQTvv/6G3ZkbJ+frYWsTcc7cBB3Fu4wy4XlLCuNtJuMn7Gsvw==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
dependencies: dependencies:
sort-keys: 1.1.2 sort-keys: 1.1.2
dev: false dev: false
/sort-keys/1.1.2: /sort-keys/1.1.2:
resolution: {integrity: sha1-RBttTTRnmPG05J6JIK37oOVD+a0=} resolution: {integrity: sha512-vzn8aSqKgytVik0iwdBEi+zevbTYZogewTUM6dtpmGwEcdzbub/TX4bCzRhebDCRC3QzXgJsLRKB2V/Oof7HXg==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
dependencies: dependencies:
is-plain-obj: 1.1.0 is-plain-obj: 1.1.0
dev: false dev: false
/sort-keys/2.0.0: /sort-keys/2.0.0:
resolution: {integrity: sha1-ZYU1WEhh7JfXMNbPQYIuH1ZoQSg=} resolution: {integrity: sha512-/dPCrG1s3ePpWm6yBbxZq5Be1dXGLyLn9Z791chDC3NFrpkVbWGzkBwPN1knaciexFXgRJ7hzdnwZ4stHSDmjg==}
engines: {node: '>=4'} engines: {node: '>=4'}
dependencies: dependencies:
is-plain-obj: 1.1.0 is-plain-obj: 1.1.0
@ -10391,7 +10382,7 @@ packages:
dev: false dev: false
/strict-uri-encode/1.1.0: /strict-uri-encode/1.1.0:
resolution: {integrity: sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=} resolution: {integrity: sha512-R3f198pcvnB+5IpnBlRkphuE9n46WyVl8I39W/ZUTZLz4nqSP/oLYUrcnJrw462Ds8he4YKMov2efsTIw1BDGQ==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
dev: false dev: false
@ -11784,7 +11775,7 @@ packages:
dev: false dev: false
file:projects/demo.tgz_@mdx-js+react@1.6.22: 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 id: file:projects/demo.tgz
name: '@rush-temp/demo' name: '@rush-temp/demo'
version: 0.0.0 version: 0.0.0
@ -11844,7 +11835,7 @@ packages:
dev: false dev: false
file:projects/scrcpy.tgz_@types+node@17.0.33: 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 id: file:projects/scrcpy.tgz
name: '@rush-temp/scrcpy' name: '@rush-temp/scrcpy'
version: 0.0.0 version: 0.0.0
@ -11852,7 +11843,7 @@ packages:
'@jest/globals': 28.1.0 '@jest/globals': 28.1.0
'@types/dom-webcodecs': 0.1.4 '@types/dom-webcodecs': 0.1.4
'@yume-chan/async': 2.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 jest: 28.1.0_@types+node@17.0.33
tinyh264: 0.0.7 tinyh264: 0.0.7
tslib: 2.4.0 tslib: 2.4.0

View file

@ -1,4 +1,4 @@
// DO NOT MODIFY THIS FILE MANUALLY BUT DO COMMIT IT. It is generated and used by Rush. // DO NOT MODIFY THIS FILE MANUALLY BUT DO COMMIT IT. It is generated and used by Rush.
{ {
"pnpmShrinkwrapHash": "084447760927a169dd324c486492ee2effdc3820" "pnpmShrinkwrapHash": "3798fc5f6f6c673b1888e77923d3eeace5651923"
} }

View file

@ -2,17 +2,45 @@
TypeScript implementation of [Scrcpy](https://github.com/Genymobile/scrcpy) client. 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. **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). 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> $ npx fetch-scrcpy-server <version>
@ -21,30 +49,164 @@ $ npx fetch-scrcpy-server <version>
For example: 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 ```json
"scripts": { "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 ### Use the server binary
import SCRCPY_SERVER_VERSION from '@yume-chan/scrcpy/bin/version';
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 ## 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.16~1.17 | `ScrcpyOptions1_16` |
| 1.18~1.20 | `ScrcpyOptions1_18` | | 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.23 | `ScrcpyOptions1_23` |
| 1.24 | `ScrcpyOptions1_24` | | 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 ## 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. ```ts
* 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. 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 | | Name | Chrome | Firefox | Safari | Performance | Supported H.264 profile/level |
| ----------------- | ------ | ------- | ------ | ------------------------------- | ----------------------------- | | ----------------- | ------ | ------- | ------ | ------------------------------- | ----------------------------- |
| WebCodecs decoder | 94 | No | No | High with Hardware acceleration | High level 5 | | WebCodecs decoder | 94 | No | No | High with Hardware acceleration | High level 5 |
| TinyH264 decoder | 57 | 52 | 11 | Poor | Baseline level 4 | | TinyH264 decoder | 57 | 52 | 11 | Poor | Baseline level 4 |
TinyH264 decoder needs some extra setup: General usage:
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:
```ts ```ts
const client = new ScrcpyClient(adb); const decoder = new H264Decoder(); // `WebCodecsDecoder` or `TinyH264Decoder`
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,
}),
}));
document.body.appendChild(decoder.element); 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.

View file

@ -45,17 +45,28 @@
"@jest/globals": "^28.1.0", "@jest/globals": "^28.1.0",
"@types/dom-webcodecs": "^0.1.3", "@types/dom-webcodecs": "^0.1.3",
"@yume-chan/ts-package-builder": "^1.0.0", "@yume-chan/ts-package-builder": "^1.0.0",
"gh-release-fetch": "^2.0.4",
"jest": "^28.1.0", "jest": "^28.1.0",
"tinyh264": "^0.0.7", "typescript": "4.7.2"
"typescript": "4.7.2",
"yuv-buffer": "^1.0.0",
"yuv-canvas": "^1.2.7"
}, },
"peerDependencies": { "peerDependencies": {
"@types/dom-webcodecs": "^0.1.3", "@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-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
}
} }
} }

View 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');
}
}

View file

@ -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 { EventEmitter } from '@yume-chan/event';
import Struct from '@yume-chan/struct'; import Struct from '@yume-chan/struct';
import { AndroidMotionEventAction, ScrcpyControlMessageType, ScrcpyInjectKeyCodeControlMessage, ScrcpyInjectTextControlMessage, ScrcpyInjectTouchControlMessage, ScrcpySimpleControlMessage, type AndroidKeyEventAction } from './message.js'; import { AndroidMotionEventAction, ScrcpyControlMessageType, ScrcpyInjectKeyCodeControlMessage, ScrcpyInjectTextControlMessage, ScrcpyInjectTouchControlMessage, ScrcpySimpleControlMessage, type AndroidKeyEventAction } from './message.js';
import type { ScrcpyInjectScrollControlMessage1_22, ScrcpyOptions, VideoStreamPacket } from "./options/index.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 = const ClipboardMessage =
new Struct() new Struct()
.uint32('length') .uint32('length')
.string('content', { lengthField: 'length' }); .string('content', { lengthField: 'length' });
export class ScrcpyClient { 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 options: ScrcpyOptions<any>;
private process: AdbSubprocessProtocol;
private _abortController = new AbortController();
private _stdout: ReadableStream<string>; private _stdout: ReadableStream<string>;
public get stdout() { return this._stdout; } 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; private _screenWidth: number | undefined;
public get screenWidth() { return this._screenWidth; } public get screenWidth() { return this._screenWidth; }
@ -281,20 +37,26 @@ export class ScrcpyClient {
private lastTouchMessage = 0; private lastTouchMessage = 0;
public constructor( public constructor(
adb: Adb,
options: ScrcpyOptions<any>, options: ScrcpyOptions<any>,
process: AdbSubprocessProtocol,
stdout: ReadableStream<string>, stdout: ReadableStream<string>,
videoStream: AdbSocket, videoStream: ReadableStream<Uint8Array>,
controlStream: AdbSocket | undefined, controlStream: ReadableWritablePair<Uint8Array, Uint8Array> | undefined,
) { ) {
this._adb = adb;
this.options = options; 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(options.createVideoStreamTransformer())
.pipeThrough(new InspectStream(packet => { .pipeThrough(new InspectStream(packet => {
if (packet.type === 'configuration') { if (packet.type === 'configuration') {
@ -304,7 +66,7 @@ export class ScrcpyClient {
})); }));
if (controlStream) { if (controlStream) {
const buffered = new AdbBufferedStream(controlStream); const buffered = new BufferedStream(controlStream.readable);
this._controlStreamWriter = controlStream.writable.getWriter(); this._controlStreamWriter = controlStream.writable.getWriter();
(async () => { (async () => {
try { try {
@ -395,7 +157,7 @@ export class ScrcpyClient {
return; return;
} }
const buffer = this.options!.serializeInjectScrollControlMessage({ const buffer = this.options.serializeInjectScrollControlMessage({
...message, ...message,
type: this.getControlMessageTypeValue(ScrcpyControlMessageType.InjectScroll), type: this.getControlMessageTypeValue(ScrcpyControlMessageType.InjectScroll),
screenWidth: this.screenWidth, screenWidth: this.screenWidth,
@ -407,7 +169,7 @@ export class ScrcpyClient {
public async pressBackOrTurnOnScreen(action: AndroidKeyEventAction) { public async pressBackOrTurnOnScreen(action: AndroidKeyEventAction) {
const controlStream = this.checkControlStream('pressBackOrTurnOnScreen'); const controlStream = this.checkControlStream('pressBackOrTurnOnScreen');
const buffer = this.options!.serializeBackOrScreenOnControlMessage({ const buffer = this.options.serializeBackOrScreenOnControlMessage({
type: this.getControlMessageTypeValue(ScrcpyControlMessageType.BackOrScreenOn), type: this.getControlMessageTypeValue(ScrcpyControlMessageType.BackOrScreenOn),
action, action,
}); });
@ -429,7 +191,7 @@ export class ScrcpyClient {
} }
public async close() { public async close() {
// No need to close streams. Kill the process will destroy them from the other side. // No need to close streams. The device will close them when process was killed.
await this.process?.kill(); this._abortController.abort();
} }
} }

View file

@ -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 { Disposable } from "@yume-chan/event";
import type { ValueOrPromise } from "@yume-chan/struct"; import type { ValueOrPromise } from "@yume-chan/struct";
import { delay } from "./utils.js"; import { delay } from "./utils.js";
@ -29,17 +29,17 @@ export abstract class ScrcpyClientConnection implements Disposable {
public initialize(): ValueOrPromise<void> { } 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 { } public dispose(): void { }
} }
export class ScrcpyClientForwardConnection extends ScrcpyClientConnection { export class ScrcpyClientForwardConnection extends ScrcpyClientConnection {
private async connect(): Promise<AdbSocket> { private async connect(): Promise<ReadableWritablePair<Uint8Array, Uint8Array>> {
return await this.device.createSocket('localabstract:scrcpy'); 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++) { for (let i = 0; i < 100; i++) {
try { try {
return await this.connect(); return await this.connect();
@ -50,10 +50,10 @@ export class ScrcpyClientForwardConnection extends ScrcpyClientConnection {
throw new Error(`Can't connect to server after 100 retries`); throw new Error(`Can't connect to server after 100 retries`);
} }
private async connectVideoStream(): Promise<AdbSocket> { private async connectVideoStream(): Promise<ReadableStream<Uint8Array>> {
const stream = await this.connectAndRetry(); const { readable: videoStream } = await this.connectAndRetry();
if (this.options.sendDummyByte) { if (this.options.sendDummyByte) {
const reader = stream.readable.getReader(); const reader = videoStream.getReader();
const { done, value } = await reader.read(); const { done, value } = await reader.read();
// server will write a `0` to signal connection success // server will write a `0` to signal connection success
if (done || value.byteLength !== 1 || value[0] !== 0) { if (done || value.byteLength !== 1 || value[0] !== 0) {
@ -61,20 +61,20 @@ export class ScrcpyClientForwardConnection extends ScrcpyClientConnection {
} }
reader.releaseLock(); 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(); const videoStream = await this.connectVideoStream();
let controlStream: AdbSocket | undefined; let controlStream: ReadableWritablePair<Uint8Array, Uint8Array> | undefined;
if (this.options.control) { if (this.options.control) {
controlStream = await this.connectAndRetry(); controlStream = await this.connectAndRetry();
} }
// Server only writes device meta after control socket is connected (if enabled) // Server only writes device meta after control socket is connected (if enabled)
if (this.options.sendDeviceMeta) { if (this.options.sendDeviceMeta) {
const reader = videoStream.readable.getReader(); const reader = videoStream.getReader();
const { done, value } = await reader.read(); const { done, value } = await reader.read();
// 64 bytes device name + 2 bytes video width + 2 bytes video height // 64 bytes device name + 2 bytes video width + 2 bytes video height
if (done || value.byteLength !== 64 + 2 + 2) { if (done || value.byteLength !== 64 + 2 + 2) {
@ -88,7 +88,7 @@ export class ScrcpyClientForwardConnection extends ScrcpyClientConnection {
} }
export class ScrcpyClientReverseConnection extends ScrcpyClientConnection { export class ScrcpyClientReverseConnection extends ScrcpyClientConnection {
private streams!: ReadableStreamDefaultReader<AdbSocket>; private streams!: ReadableStreamDefaultReader<ReadableWritablePair<Uint8Array, Uint8Array>>;
private address!: string; private address!: string;
@ -100,7 +100,7 @@ export class ScrcpyClientReverseConnection extends ScrcpyClientConnection {
// ignore error // ignore error
} }
const queue = new TransformStream<AdbSocket>(); const queue = new TransformStream<ReadableWritablePair<Uint8Array, Uint8Array>>();
this.streams = queue.readable.getReader(); this.streams = queue.readable.getReader();
const writer = queue.writable.getWriter(); const writer = queue.writable.getWriter();
this.address = await this.device.reverse.add( 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!; return (await this.streams.read()).value!;
} }
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.accept(); const { readable: videoStream } = await this.accept();
let controlStream: AdbSocket | undefined; let controlStream: ReadableWritablePair<Uint8Array, Uint8Array> | undefined;
if (this.options.control) { if (this.options.control) {
controlStream = await this.accept(); controlStream = await this.accept();
} }
// Server only writes device meta after control socket is connected (if enabled) // Server only writes device meta after control socket is connected (if enabled)
if (this.options.sendDeviceMeta) { if (this.options.sendDeviceMeta) {
const reader = videoStream.readable.getReader(); const reader = videoStream.getReader();
const { done, value } = await reader.read(); const { done, value } = await reader.read();
// 64 bytes device name + 2 bytes video width + 2 bytes video height // 64 bytes device name + 2 bytes video width + 2 bytes video height
if (done || value.byteLength !== 64 + 2 + 2) { if (done || value.byteLength !== 64 + 2 + 2) {

View file

@ -1,8 +1,8 @@
export * from './adb-client.js';
export * from './client.js'; export * from './client.js';
export * from './codec.js'; export * from './codec.js';
export * from './connection.js'; export * from './connection.js';
export * from './decoder/index.js'; export * from './decoder/index.js';
export * from './message.js'; export * from './message.js';
export * from './options/index.js'; export * from './options/index.js';
export * from './push-server.js';
export * from './utils.js'; export * from './utils.js';

View file

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

View file

@ -17,7 +17,7 @@
"path": "../event/tsconfig.json" "path": "../event/tsconfig.json"
}, },
{ {
"path": "../struct/tsconfig.json" "path": "../struct/tsconfig.build.json"
} }
] ]
} }