diff --git a/apps/demo/components/connect.tsx b/apps/demo/components/connect.tsx index fbdacfe9..b24ba59e 100644 --- a/apps/demo/components/connect.tsx +++ b/apps/demo/components/connect.tsx @@ -143,9 +143,10 @@ function _Connect(): JSX.Element | null { let device: Adb | undefined; try { setConnecting(true); - device = await Adb.connect(selectedBackend, logger.logger); - await device.authenticate(CredentialStore); - globalState.setDevice(device); + const connection = await selectedBackend.connect(); + const adbConnection = Adb.createConnection(connection); + device = await Adb.authenticate(adbConnection, CredentialStore, undefined, logger.logger); + globalState.setDevice(selectedBackend, device); } catch (e) { device?.dispose(); throw e; @@ -160,7 +161,7 @@ function _Connect(): JSX.Element | null { const disconnect = useCallback(async () => { try { await globalState.device!.dispose(); - globalState.setDevice(undefined); + globalState.setDevice(undefined, undefined); } catch (e: any) { globalState.showErrorDialog(e.message); } diff --git a/apps/demo/components/log-view.tsx b/apps/demo/components/log-view.tsx index 01746008..8cb7de5a 100644 --- a/apps/demo/components/log-view.tsx +++ b/apps/demo/components/log-view.tsx @@ -1,5 +1,5 @@ import { IconButton, IListProps, List, mergeStyles, mergeStyleSets, Stack } from '@fluentui/react'; -import { AdbPacketInit, decodeUtf8 } from '@yume-chan/adb'; +import { AdbPacketCore, decodeUtf8 } from '@yume-chan/adb'; import { DisposableList } from '@yume-chan/event'; import { observer } from "mobx-react-lite"; import { PropsWithChildren, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; @@ -23,7 +23,7 @@ const classNames = mergeStyleSets({ }, }); -function serializePacket(packet: AdbPacketInit) { +function serializePacket(packet: AdbPacketCore) { const command = decodeUtf8(new Uint32Array([packet.command])); const parts = [ @@ -44,7 +44,7 @@ function serializePacket(packet: AdbPacketInit) { return parts.join(' '); } -const LogLine = withDisplayName('LoggerLine')(({ packet }: { packet: [string, AdbPacketInit]; }) => { +const LogLine = withDisplayName('LoggerLine')(({ packet }: { packet: [string, AdbPacketCore]; }) => { const string = useMemo(() => serializePacket(packet[1]), [packet]); return ( @@ -69,11 +69,11 @@ export interface LoggerProps { className?: string; } -function shouldVirtualize(props: IListProps<[string, AdbPacketInit]>) { +function shouldVirtualize(props: IListProps<[string, AdbPacketCore]>) { return !!props.items && props.items.length > 100; } -function renderCell(item?: [string, AdbPacketInit]) { +function renderCell(item?: [string, AdbPacketCore]) { if (!item) { return null; } @@ -86,7 +86,7 @@ function renderCell(item?: [string, AdbPacketInit]) { export const LogView = observer(({ className, }: LoggerProps) => { - const [packets, setPackets] = useState<[string, AdbPacketInit][]>([]); + const [packets, setPackets] = useState<[string, AdbPacketCore][]>([]); const scrollerRef = useRef(null); useEffect(() => { diff --git a/apps/demo/package.json b/apps/demo/package.json index 09913ddc..072a2fc1 100644 --- a/apps/demo/package.json +++ b/apps/demo/package.json @@ -42,6 +42,6 @@ "@types/react": "17.0.27", "eslint": "8.8.0", "eslint-config-next": "12.1.0", - "typescript": "^4.5.5" + "typescript": "^4.6.2" } } diff --git a/apps/demo/pages/framebuffer.tsx b/apps/demo/pages/framebuffer.tsx index 3eb825a5..c6b48c58 100644 --- a/apps/demo/pages/framebuffer.tsx +++ b/apps/demo/pages/framebuffer.tsx @@ -84,7 +84,7 @@ const FrameBuffer: NextPage = (): JSX.Element | null => { const url = canvas.toDataURL(); const a = document.createElement('a'); a.href = url; - a.download = `Screenshot of ${globalState.device!.name}.png`; + a.download = `Screenshot of ${globalState.backend!.name}.png`; a.click(); }, }, diff --git a/apps/demo/state/global.ts b/apps/demo/state/global.ts index 9e6a3c05..7991d1b8 100644 --- a/apps/demo/state/global.ts +++ b/apps/demo/state/global.ts @@ -1,8 +1,11 @@ -import { Adb } from "@yume-chan/adb"; +import { Adb, AdbBackend } from "@yume-chan/adb"; import { action, makeAutoObservable } from 'mobx'; export class GlobalState { + backend: AdbBackend | undefined = undefined; + device: Adb | undefined = undefined; + errorDialogVisible = false; errorDialogMessage = ''; @@ -15,7 +18,8 @@ export class GlobalState { }); } - setDevice(device: Adb | undefined) { + setDevice(backend: AdbBackend | undefined, device: Adb | undefined) { + this.backend = backend; this.device = device; } diff --git a/apps/demo/state/logger.ts b/apps/demo/state/logger.ts index 13be221c..bfd0cbac 100644 --- a/apps/demo/state/logger.ts +++ b/apps/demo/state/logger.ts @@ -1,4 +1,4 @@ -import { AdbLogger, AdbPacket, AdbPacketInit } from "@yume-chan/adb"; +import { AdbLogger, AdbPacket, AdbPacketCore } from "@yume-chan/adb"; import { EventEmitter } from "@yume-chan/event"; export class AdbEventLogger { @@ -8,7 +8,7 @@ export class AdbEventLogger { private readonly _incomingPacketEvent = new EventEmitter(); public get onIncomingPacket() { return this._incomingPacketEvent.event; } - private readonly _outgoingPacketEvent = new EventEmitter(); + private readonly _outgoingPacketEvent = new EventEmitter(); public get onOutgoingPacket() { return this._outgoingPacketEvent.event; } public constructor() { diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index f652eb95..7a961901 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -3764,7 +3764,7 @@ packages: - supports-color dev: false - /@typescript-eslint/parser/5.12.0_eslint@8.8.0+typescript@4.5.5: + /@typescript-eslint/parser/5.12.0_eslint@8.8.0+typescript@4.6.2: resolution: {integrity: sha512-MfSwg9JMBojMUoGjUmX+D2stoQj1CBYTCP0qnnVtu9A+YQXVKNtLjasYh+jozOcrb/wau8TCfWOkQTiOAruBog==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -3776,10 +3776,10 @@ packages: dependencies: '@typescript-eslint/scope-manager': 5.12.0 '@typescript-eslint/types': 5.12.0 - '@typescript-eslint/typescript-estree': 5.12.0_typescript@4.5.5 + '@typescript-eslint/typescript-estree': 5.12.0_typescript@4.6.2 debug: 4.3.3 eslint: 8.8.0 - typescript: 4.5.5 + typescript: 4.6.2 transitivePeerDependencies: - supports-color dev: false @@ -3817,7 +3817,7 @@ packages: - supports-color dev: false - /@typescript-eslint/typescript-estree/5.12.0_typescript@4.5.5: + /@typescript-eslint/typescript-estree/5.12.0_typescript@4.6.2: resolution: {integrity: sha512-Dd9gVeOqt38QHR0BEA8oRaT65WYqPYbIc5tRFQPkfLquVEFPD1HAtbZT98TLBkEcCkvwDYOAvuSvAD9DnQhMfQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -3832,8 +3832,8 @@ packages: globby: 11.1.0 is-glob: 4.0.3 semver: 7.3.5 - tsutils: 3.21.0_typescript@4.5.5 - typescript: 4.5.5 + tsutils: 3.21.0_typescript@4.6.2 + typescript: 4.6.2 transitivePeerDependencies: - supports-color dev: false @@ -6048,7 +6048,7 @@ packages: source-map: 0.6.1 dev: false - /eslint-config-next/12.1.0_a833109067da92148152ff2f5707ab26: + /eslint-config-next/12.1.0_84a9f7b24d99c162ec5512424f921235: resolution: {integrity: sha512-tBhuUgoDITcdcM7xFvensi9I5WTI4dnvH4ETGRg1U8ZKpXrZsWQFdOKIDzR3RLP5HR3xXrLviaMM4c3zVoE/pA==} peerDependencies: eslint: ^7.23.0 || ^8.0.0 @@ -6060,7 +6060,7 @@ packages: dependencies: '@next/eslint-plugin-next': 12.1.0 '@rushstack/eslint-patch': 1.1.0 - '@typescript-eslint/parser': 5.12.0_eslint@8.8.0+typescript@4.5.5 + '@typescript-eslint/parser': 5.12.0_eslint@8.8.0+typescript@4.6.2 eslint: 8.8.0 eslint-import-resolver-node: 0.3.6 eslint-import-resolver-typescript: 2.5.0_392f898cec7735a5f7a99430cbc0b4f4 @@ -6069,7 +6069,7 @@ packages: eslint-plugin-react: 7.28.0_eslint@8.8.0 eslint-plugin-react-hooks: 4.3.0_eslint@8.8.0 next: 12.1.0_react-dom@17.0.2+react@17.0.2 - typescript: 4.5.5 + typescript: 4.6.2 transitivePeerDependencies: - supports-color dev: false @@ -12269,14 +12269,14 @@ packages: tslib: 1.14.1 dev: false - /tsutils/3.21.0_typescript@4.5.5: + /tsutils/3.21.0_typescript@4.6.2: resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} engines: {node: '>= 6'} peerDependencies: typescript: '>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta' dependencies: tslib: 1.14.1 - typescript: 4.5.5 + typescript: 4.6.2 dev: false /type-check/0.3.2: @@ -12332,8 +12332,8 @@ packages: is-typedarray: 1.0.0 dev: false - /typescript/4.5.5: - resolution: {integrity: sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA==} + /typescript/4.6.2: + resolution: {integrity: sha512-HM/hFigTBHZhLXshn9sN37H085+hQGeJHJ/X7LpBWLID/fbc2acUMfU+lGD98X81sKP+pFa9f0DZmCwB9GnbAg==} engines: {node: '>=4.2.0'} hasBin: true dev: false @@ -13199,24 +13199,24 @@ packages: dev: false file:projects/adb-backend-direct-sockets.tgz: - resolution: {integrity: sha512-clMC9xKIUpsXhBxa22pfc+xZAuPgzTLDPc+9gDGeQfpVVJDMq+Sug51Q3hWSHzjLkqFOB0MF6Ekz7Ggl05FH7A==, tarball: file:projects/adb-backend-direct-sockets.tgz} + resolution: {integrity: sha512-7lYtoXDESytDSbVwRclsL5Zei0SyHDrla2MDnOylREFhq9k+jD5y4QlGlp5BA7i8jxCcFO4X+ycdjeo03LgRNg==, tarball: file:projects/adb-backend-direct-sockets.tgz} name: '@rush-temp/adb-backend-direct-sockets' version: 0.0.0 dependencies: '@types/jest': 27.4.0 tslib: 2.3.1 - typescript: 4.5.5 + typescript: 4.6.2 dev: false file:projects/adb-backend-webusb.tgz: - resolution: {integrity: sha512-uxXSrxrQ1mZ4GRMuZ6hlcZjv4L40zy9rYQ3d+XR47rVgLk3vAbyheenAGxSN6o5GcJnH4FFw0T1DXJEnraNm9g==, tarball: file:projects/adb-backend-webusb.tgz} + resolution: {integrity: sha512-hLTGiBjXJCtaLLQGVVCSSQF27mpAXhWVPqkZv/pXm+6Gnvl+AeTtwZzI3gBcWLRKZt8kNqG7ZPPoWk+TV+ZLow==, tarball: file:projects/adb-backend-webusb.tgz} name: '@rush-temp/adb-backend-webusb' version: 0.0.0 dependencies: '@types/w3c-web-usb': 1.0.5 jest: 26.6.3 tslib: 2.3.1 - typescript: 4.5.5 + typescript: 4.6.2 transitivePeerDependencies: - bufferutil - canvas @@ -13226,13 +13226,13 @@ packages: dev: false file:projects/adb-backend-ws.tgz: - resolution: {integrity: sha512-lHi6rENCAPuUaKIwKPsrZRhif+znoMleIoQu/5u5SZsVal6jPmk9boeB4gbPRACNIpNqYZJ1ptRAtJKU9kUpdQ==, tarball: file:projects/adb-backend-ws.tgz} + resolution: {integrity: sha512-gYyPR7NoztGZMkMMApsw/n4hjU2mXpW0Q5ZytAendhOjxFhlgVuzUNVIseEeCZ5WcKZJq8UDcbm3t1XzjNnspg==, tarball: file:projects/adb-backend-ws.tgz} name: '@rush-temp/adb-backend-ws' version: 0.0.0 dependencies: jest: 26.6.3 tslib: 2.3.1 - typescript: 4.5.5 + typescript: 4.6.2 transitivePeerDependencies: - bufferutil - canvas @@ -13242,23 +13242,23 @@ packages: dev: false file:projects/adb-credential-web.tgz: - resolution: {integrity: sha512-ovbVsEARMBPtPv/85e3cUh5VtHdqw4DQaZ/IwGEOmUmx5O6kN/uMRB5oG++g4/Bam+A+/HhY9WPjFsX5W3cKlg==, tarball: file:projects/adb-credential-web.tgz} + resolution: {integrity: sha512-ScBUE3flU6eg6FvrtT9WtXbzebLkWF3gktNBTRXXzccRNzkHeERHGEWV6+9ikmUUV76q+Hyn36dY1p81mWlf1w==, tarball: file:projects/adb-credential-web.tgz} name: '@rush-temp/adb-credential-web' version: 0.0.0 dependencies: tslib: 2.3.1 - typescript: 4.5.5 + typescript: 4.6.2 dev: false file:projects/adb.tgz: - resolution: {integrity: sha512-ERJiC9hMtOFRZhGBqQ76HXFVsjsGHajyWrPoHqOrAW4LqTdHf8jJueqhzPt6e9asmTuxcOynqz4MzbYnahcTtA==, tarball: file:projects/adb.tgz} + resolution: {integrity: sha512-5M3XhdG6BqRVn0LyI3z6Ig7qOFQwVJyH17fkNq+1cVYVALZHg2Q8MquZcY76GUfo7XR2Fl7Eiw5zNRBOkgUgbA==, tarball: file:projects/adb.tgz} name: '@rush-temp/adb' version: 0.0.0 dependencies: '@yume-chan/async': 2.1.4 jest: 26.6.3 tslib: 2.3.1 - typescript: 4.5.5 + typescript: 4.6.2 transitivePeerDependencies: - bufferutil - canvas @@ -13268,22 +13268,22 @@ packages: dev: false file:projects/android-bin.tgz: - resolution: {integrity: sha512-+POm1mf4P1s+NhgwA6IHN+GBBmc00lDi7k+geCc04ts836duRlnJfkIEeIIJmjQHVwDz36BONp8rCXAaX4U/fA==, tarball: file:projects/android-bin.tgz} + resolution: {integrity: sha512-FF55Xz9NTN3Og0oFF2ya/NBJb0Uz1x/kwJEE2cI93UqtHdJQ/eMwMm1mUcOxUcdIOkriy3eVCvVd1WWpv8Eqnw==, tarball: file:projects/android-bin.tgz} name: '@rush-temp/android-bin' version: 0.0.0 dependencies: tslib: 2.3.1 - typescript: 4.5.5 + typescript: 4.6.2 dev: false file:projects/dataview-bigint-polyfill.tgz: - resolution: {integrity: sha512-tlwAp44MyiGGyikI+6kFaCkVQjkpKmlMPm0hm6ts97uydxfdNgMnoGp0JW4J4WDvFOjforF3Z1+kfvBTVDHv1w==, tarball: file:projects/dataview-bigint-polyfill.tgz} + resolution: {integrity: sha512-lbBWjYYtVRQA5JWUloB96WIAXPAIsUQKUqqSoETJdsw+jbea3glhdHvLUxQAc0TjxF/rQk0g7dhF1jOtcmkS/A==, tarball: file:projects/dataview-bigint-polyfill.tgz} name: '@rush-temp/dataview-bigint-polyfill' version: 0.0.0 dependencies: jest: 26.6.3 tslib: 2.3.1 - typescript: 4.5.5 + typescript: 4.6.2 transitivePeerDependencies: - bufferutil - canvas @@ -13293,7 +13293,7 @@ packages: dev: false file:projects/demo.tgz_@mdx-js+react@1.6.22: - resolution: {integrity: sha512-Rjd2nSA/9Gu41TKCWSKZM9bMVXqUFoWr30etKv07GGFP1NVkM8sWcH9bPezqiQxXRcZ5eMXet47PSS6Db8Qmqg==, tarball: file:projects/demo.tgz} + resolution: {integrity: sha512-fb7TRHuIGkcJHZRpdp76shGm+70hVRjUcr2WWD7d+PbK0aQUPvDi7loy9XCdyI3deTF0APfR44agm1irnymRAw==, tarball: file:projects/demo.tgz} id: file:projects/demo.tgz name: '@rush-temp/demo' version: 0.0.0 @@ -13308,14 +13308,14 @@ packages: '@types/react': 17.0.27 '@yume-chan/async': 2.1.4 eslint: 8.8.0 - eslint-config-next: 12.1.0_a833109067da92148152ff2f5707ab26 + eslint-config-next: 12.1.0_84a9f7b24d99c162ec5512424f921235 mobx: 6.3.13 mobx-react-lite: 3.2.3_96b0034b8b6bfac1b4cc60715db17f02 next: 12.1.0_react-dom@17.0.2+react@17.0.2 react: 17.0.2 react-dom: 17.0.2_react@17.0.2 streamsaver: 2.0.6 - typescript: 4.5.5 + typescript: 4.6.2 xterm: 4.17.0 xterm-addon-fit: 0.5.0_xterm@4.17.0 xterm-addon-search: 0.8.2_xterm@4.17.0 @@ -13333,14 +13333,14 @@ packages: dev: false file:projects/event.tgz: - resolution: {integrity: sha512-eEBPgCD8YhbmkfWMY1Nrb7gijGq8+v0zayohQ4um92OzdQbm04nRS+TAD6JSP0YpH3pvKxUoe9QBVOAr98Ltwg==, tarball: file:projects/event.tgz} + resolution: {integrity: sha512-/nNG2wtotQQEddux5DQGXRTMa3h9X0y/IEn3jj07hsd/lb1DZz4EhXDaiuFHgxgYC9Vlt78yqcmI6vq3BynbMQ==, tarball: file:projects/event.tgz} name: '@rush-temp/event' version: 0.0.0 dependencies: '@yume-chan/async': 2.1.4 jest: 26.6.3 tslib: 2.3.1 - typescript: 4.5.5 + typescript: 4.6.2 transitivePeerDependencies: - bufferutil - canvas @@ -13350,7 +13350,7 @@ packages: dev: false file:projects/scrcpy.tgz: - resolution: {integrity: sha512-yOKNFXcflPuF8A0tA0dLEe4a6FS1ofKExq9AP/BruIYp9CG15JjNA8FI0UQ7zuJaSjCnGDjFzFnMF4tmSWfhrA==, tarball: file:projects/scrcpy.tgz} + resolution: {integrity: sha512-t5PfOgkF6otyHa83xANbVLVXoWPqh+5V2BS0NW4LDY9wvOww9BEr4RW/LRwuP9WszHZZ8pQKApBomeu+fon6Yw==, tarball: file:projects/scrcpy.tgz} name: '@rush-temp/scrcpy' version: 0.0.0 dependencies: @@ -13361,7 +13361,7 @@ packages: jest: 26.6.3 tinyh264: 0.0.7 tslib: 2.3.1 - typescript: 4.5.5 + typescript: 4.6.2 yuv-buffer: 1.0.0 yuv-canvas: 1.2.9 transitivePeerDependencies: @@ -13374,7 +13374,7 @@ packages: dev: false file:projects/struct.tgz: - resolution: {integrity: sha512-UuEJ78bVrGthdhMJa0kKI/dB7tprMDcMEaZ33H+A7ISjPz5U1bfYIXE8IpVUiNBmoKeT/Ce46+isU9cSf2Mzdg==, tarball: file:projects/struct.tgz} + resolution: {integrity: sha512-0FfQNcq/N527LPz/UOQ6get3JHRTsWkqg25yvF5iaCAYXpjdgIREQSGGUxru0+oSMaTl76cGEtSrbrOCa+FrWA==, tarball: file:projects/struct.tgz} name: '@rush-temp/struct' version: 0.0.0 dependencies: @@ -13383,7 +13383,7 @@ packages: bluebird: 3.7.2 jest: 26.6.3 tslib: 2.3.1 - typescript: 4.5.5 + typescript: 4.6.2 transitivePeerDependencies: - bufferutil - canvas @@ -13400,7 +13400,7 @@ packages: '@types/jest': 27.4.0 '@types/node': 17.0.18 json5: 2.2.0 - typescript: 4.5.5 + typescript: 4.6.2 dev: false file:projects/unofficial-adb-book.tgz_814045a8852c08eeea7e09b05a06dede: diff --git a/libraries/adb-backend-direct-sockets/package.json b/libraries/adb-backend-direct-sockets/package.json index 82946eaa..7481f606 100644 --- a/libraries/adb-backend-direct-sockets/package.json +++ b/libraries/adb-backend-direct-sockets/package.json @@ -31,7 +31,7 @@ "prepublishOnly": "npm run build" }, "devDependencies": { - "typescript": "^4.5.5", + "typescript": "^4.6.2", "@types/jest": "^27.4.0", "@yume-chan/ts-package-builder": "^1.0.0" }, diff --git a/libraries/adb-backend-webusb/package.json b/libraries/adb-backend-webusb/package.json index 1c49d1e1..76831f62 100644 --- a/libraries/adb-backend-webusb/package.json +++ b/libraries/adb-backend-webusb/package.json @@ -37,7 +37,7 @@ }, "devDependencies": { "jest": "^26.6.3", - "typescript": "^4.5.5", + "typescript": "^4.6.2", "@yume-chan/ts-package-builder": "^1.0.0" } } diff --git a/libraries/adb-backend-ws/package.json b/libraries/adb-backend-ws/package.json index f10120fa..897d472a 100644 --- a/libraries/adb-backend-ws/package.json +++ b/libraries/adb-backend-ws/package.json @@ -31,7 +31,7 @@ }, "devDependencies": { "jest": "^26.6.3", - "typescript": "^4.5.5", + "typescript": "^4.6.2", "@yume-chan/ts-package-builder": "^1.0.0" }, "dependencies": { diff --git a/libraries/adb-credential-web/package.json b/libraries/adb-credential-web/package.json index 1ffae23b..648e7dc7 100644 --- a/libraries/adb-credential-web/package.json +++ b/libraries/adb-credential-web/package.json @@ -29,7 +29,7 @@ "prepublishOnly": "npm run build" }, "devDependencies": { - "typescript": "^4.5.5", + "typescript": "^4.6.2", "@yume-chan/ts-package-builder": "^1.0.0" }, "dependencies": { diff --git a/libraries/adb/package.json b/libraries/adb/package.json index a33a827d..d6819026 100644 --- a/libraries/adb/package.json +++ b/libraries/adb/package.json @@ -39,7 +39,7 @@ }, "devDependencies": { "jest": "^26.6.3", - "typescript": "^4.5.5", + "typescript": "^4.6.2", "@yume-chan/ts-package-builder": "^1.0.0" } } diff --git a/libraries/adb/src/adb.ts b/libraries/adb/src/adb.ts index efd5afc1..39949db0 100644 --- a/libraries/adb/src/adb.ts +++ b/libraries/adb/src/adb.ts @@ -1,13 +1,11 @@ import { PromiseResolver } from '@yume-chan/async'; -import { DisposableList } from '@yume-chan/event'; import { AdbAuthenticationHandler, AdbCredentialStore, AdbDefaultAuthenticators } from './auth'; -import { AdbBackend } from './backend'; import { AdbFrameBuffer, AdbPower, AdbReverseCommand, AdbSubprocess, AdbSync, AdbTcpIpCommand, escapeArg, framebuffer, install } from './commands'; import { AdbFeatures } from './features'; -import { AdbCommand } from './packet'; +import { AdbCommand, AdbPacket, AdbPacketCore, AdbPacketInit, AdbPacketSerializeStream, calculateChecksum } from './packet'; import { AdbLogger, AdbPacketDispatcher, AdbSocket } from './socket'; -import { DecodeUtf8Stream, GatherStringStream, ReadableStream, WritableStream } from "./stream"; -import { decodeUtf8 } from "./utils"; +import { AbortController, DecodeUtf8Stream, GatherStringStream, pipeFrom, ReadableWritablePair, StructDeserializeStream, WritableStream } from "./stream"; +import { decodeUtf8, encodeUtf8 } from "./utils"; export enum AdbPropKey { Product = 'ro.product.name', @@ -16,20 +14,132 @@ export enum AdbPropKey { Features = 'features', } +export const VERSION_OMIT_CHECKSUM = 0x01000001; + export class Adb { - public static async connect(backend: AdbBackend, logger?: AdbLogger) { - const { readable, writable } = await backend.connect(); - return new Adb(backend, readable, writable, logger); + public static createConnection( + connection: ReadableWritablePair + ): ReadableWritablePair { + return { + readable: connection.readable.pipeThrough( + new StructDeserializeStream(AdbPacket) + ), + writable: pipeFrom( + connection.writable, + new AdbPacketSerializeStream() + ), + }; } - private readonly _backend: AdbBackend; + /** + * It's possible to call `authenticate` multiple times on a single connection, + * every time the device receives a `CNXN` packet it will reset its internal state, + * and begin authentication again. + */ + public static async authenticate( + connection: ReadableWritablePair, + credentialStore: AdbCredentialStore, + authenticators = AdbDefaultAuthenticators, + logger?: AdbLogger + ) { + let version = 0x01000001; + let maxPayloadSize = 0x100000; - public get backend(): AdbBackend { return this._backend; } + const features = [ + 'shell_v2', + 'cmd', + AdbFeatures.StatV2, + 'ls_v2', + 'fixed_push_mkdir', + 'apex', + 'abb', + 'fixed_push_symlink_timestamp', + 'abb_exec', + 'remount_shell', + 'track_app', + 'sendrecv_v2', + 'sendrecv_v2_brotli', + 'sendrecv_v2_lz4', + 'sendrecv_v2_zstd', + 'sendrecv_v2_dry_run_send', + ].join(','); + + const resolver = new PromiseResolver(); + const authHandler = new AdbAuthenticationHandler(authenticators, credentialStore); + + const abortController = new AbortController(); + const pipe = connection.readable + .pipeTo(new WritableStream({ + async write(packet: AdbPacket) { + logger?.onIncomingPacket?.(packet); + + switch (packet.command) { + case AdbCommand.Connect: + version = Math.min(version, packet.arg0); + maxPayloadSize = Math.min(maxPayloadSize, packet.arg1); + resolver.resolve(decodeUtf8(packet.payload)); + break; + case AdbCommand.Auth: + const response = await authHandler.handle(packet); + await sendPacket(response); + break; + case AdbCommand.Close: + // Last connection was interrupted + // Ignore this packet, device will recover + break; + default: + throw new Error('Device not in correct state. Reconnect your device and try again'); + } + } + }), { + preventCancel: true, + signal: abortController.signal, + }) + .catch((e) => { resolver.reject(e); }); + + const writer = connection.writable.getWriter(); + async function sendPacket(init: AdbPacketCore) { + logger?.onOutgoingPacket?.(init); + + // Always send checksum in auth steps + // Because we don't know if the device will ignore it yet. + await writer.write(calculateChecksum(init)); + } + + await sendPacket({ + command: AdbCommand.Connect, + arg0: version, + arg1: maxPayloadSize, + // The terminating `;` is required in formal definition + // But ADB daemon (all versions) can still work without it + payload: encodeUtf8(`host::features=${features};`), + }); + + try { + const banner = await resolver.promise; + + // Stop piping before creating Adb object + // Because AdbPacketDispatcher will try to lock the streams when initializing + abortController.abort(); + await pipe; + + writer.releaseLock(); + + return new Adb( + connection, + version, + maxPayloadSize, + banner, + logger + ); + } finally { + abortController.abort(); + writer.releaseLock(); + } + } private readonly packetDispatcher: AdbPacketDispatcher; - public get name() { return this.backend.name; } - private _protocolVersion: number | undefined; public get protocolVersion() { return this._protocolVersion; } @@ -51,13 +161,23 @@ export class Adb { public readonly tcpip: AdbTcpIpCommand; public constructor( - backend: AdbBackend, - readable: ReadableStream, - writable: WritableStream, + connection: ReadableWritablePair, + version: number, + maxPayloadSize: number, + banner: string, logger?: AdbLogger ) { - this._backend = backend; - this.packetDispatcher = new AdbPacketDispatcher(readable, writable, logger); + this.parseBanner(banner); + this.packetDispatcher = new AdbPacketDispatcher(connection, logger); + + this._protocolVersion = version; + if (version >= VERSION_OMIT_CHECKSUM) { + this.packetDispatcher.calculateChecksum = false; + // Android prior to 9.0.0 uses char* to parse service string + // thus requires an extra null character + this.packetDispatcher.appendNullToServiceString = false; + } + this.packetDispatcher.maxPayloadSize = maxPayloadSize; this.subprocess = new AdbSubprocess(this); this.power = new AdbPower(this); @@ -65,98 +185,6 @@ export class Adb { this.tcpip = new AdbTcpIpCommand(this); } - public async authenticate( - credentialStore: AdbCredentialStore, - authenticators = AdbDefaultAuthenticators - ): Promise { - this.packetDispatcher.maxPayloadSize = 0x1000; - this.packetDispatcher.calculateChecksum = true; - this.packetDispatcher.appendNullToServiceString = true; - - const version = 0x01000001; - const versionNoChecksum = 0x01000001; - const maxPayloadSize = 0x100000; - - const features = [ - 'shell_v2', - 'cmd', - AdbFeatures.StatV2, - 'ls_v2', - 'fixed_push_mkdir', - 'apex', - 'abb', - 'fixed_push_symlink_timestamp', - 'abb_exec', - 'remount_shell', - 'track_app', - 'sendrecv_v2', - 'sendrecv_v2_brotli', - 'sendrecv_v2_lz4', - 'sendrecv_v2_zstd', - 'sendrecv_v2_dry_run_send', - ].join(','); - - const resolver = new PromiseResolver(); - const authHandler = new AdbAuthenticationHandler(authenticators, credentialStore); - const disposableList = new DisposableList(); - disposableList.add(this.packetDispatcher.onPacket(async (e) => { - e.handled = true; - - const { packet } = e; - try { - switch (packet.command) { - case AdbCommand.Connect: - this.packetDispatcher.maxPayloadSize = Math.min(maxPayloadSize, packet.arg1); - - const finalVersion = Math.min(version, packet.arg0); - this._protocolVersion = finalVersion; - - if (finalVersion >= versionNoChecksum) { - this.packetDispatcher.calculateChecksum = false; - // Android prior to 9.0.0 uses char* to parse service string - // thus requires an extra null character - this.packetDispatcher.appendNullToServiceString = false; - } - - this.parseBanner(decodeUtf8(packet.payload!)); - resolver.resolve(); - break; - case AdbCommand.Auth: - const authPacket = await authHandler.handle(e.packet); - await this.packetDispatcher.sendPacket(authPacket); - break; - case AdbCommand.Close: - // Last connection was interrupted - // Ignore this packet, device will recover - break; - default: - throw new Error('Device not in correct state. Reconnect your device and try again'); - } - } catch (e) { - resolver.reject(e); - } - })); - - disposableList.add(this.packetDispatcher.onError(e => { - resolver.reject(e); - })); - - await this.packetDispatcher.sendPacket( - AdbCommand.Connect, - version, - maxPayloadSize, - // The terminating `;` is required in formal definition - // But ADB daemon can also work without it - `host::features=${features};` - ); - - try { - await resolver.promise; - } finally { - disposableList.dispose(); - } - } - private parseBanner(banner: string): void { this._features = []; diff --git a/libraries/adb/src/auth.ts b/libraries/adb/src/auth.ts index b6b2d858..f0f3d8c3 100644 --- a/libraries/adb/src/auth.ts +++ b/libraries/adb/src/auth.ts @@ -2,7 +2,7 @@ import { PromiseResolver } from '@yume-chan/async'; import { Disposable } from '@yume-chan/event'; import { ValueOrPromise } from '@yume-chan/struct'; import { calculatePublicKey, calculatePublicKeyLength, sign } from './crypto'; -import { AdbCommand, AdbPacket, AdbPacketInit } from './packet'; +import { AdbCommand, AdbPacket, AdbPacketCore } from './packet'; import { calculateBase64EncodedLength, encodeBase64 } from './utils'; export type AdbKeyIterable = Iterable | AsyncIterable; @@ -44,13 +44,13 @@ export interface AdbAuthenticator { ( credentialStore: AdbCredentialStore, getNextRequest: () => Promise - ): AsyncIterable; + ): AsyncIterable; } export const AdbSignatureAuthenticator: AdbAuthenticator = async function* ( credentialStore: AdbCredentialStore, getNextRequest: () => Promise, -): AsyncIterable { +): AsyncIterable { for await (const key of credentialStore.iterateKeys()) { const packet = await getNextRequest(); @@ -58,7 +58,7 @@ export const AdbSignatureAuthenticator: AdbAuthenticator = async function* ( return; } - const signature = sign(key, packet.payload!); + const signature = sign(key, packet.payload); yield { command: AdbCommand.Auth, arg0: AdbAuthType.Signature, @@ -71,7 +71,7 @@ export const AdbSignatureAuthenticator: AdbAuthenticator = async function* ( export const AdbPublicKeyAuthenticator: AdbAuthenticator = async function* ( credentialStore: AdbCredentialStore, getNextRequest: () => Promise, -): AsyncIterable { +): AsyncIterable { const packet = await getNextRequest(); if (packet.arg0 !== AdbAuthType.Token) { @@ -119,7 +119,7 @@ export class AdbAuthenticationHandler implements Disposable { private pendingRequest = new PromiseResolver(); - private iterator: AsyncIterator | undefined; + private iterator: AsyncIterator | undefined; public constructor( authenticators: readonly AdbAuthenticator[], @@ -133,7 +133,7 @@ export class AdbAuthenticationHandler implements Disposable { return this.pendingRequest.promise; }; - private async* runAuthenticator(): AsyncGenerator { + private async* runAuthenticator(): AsyncGenerator { for (const authenticator of this.authenticators) { for await (const packet of authenticator(this.credentialStore, this.getNextRequest)) { // If the authenticator yielded a response @@ -151,7 +151,7 @@ export class AdbAuthenticationHandler implements Disposable { throw new Error('Cannot authenticate with device'); } - public async handle(packet: AdbPacket): Promise { + public async handle(packet: AdbPacket): Promise { if (!this.iterator) { this.iterator = this.runAuthenticator(); } diff --git a/libraries/adb/src/commands/reverse.ts b/libraries/adb/src/commands/reverse.ts index 979f8512..04841db3 100644 --- a/libraries/adb/src/commands/reverse.ts +++ b/libraries/adb/src/commands/reverse.ts @@ -52,7 +52,7 @@ export class AdbReverseCommand extends AutoDisposable { return; } - const address = decodeUtf8(e.packet.payload!); + const address = decodeUtf8(e.packet.payload); // Address format: `tcp:12345\0` const port = Number.parseInt(address.substring(4)); if (this.localPortToHandler.has(port)) { diff --git a/libraries/adb/src/packet.ts b/libraries/adb/src/packet.ts index bbc11274..9a925e08 100644 --- a/libraries/adb/src/packet.ts +++ b/libraries/adb/src/packet.ts @@ -19,6 +19,8 @@ const AdbPacketHeader = .uint32('checksum') .int32('magic'); +type AdbPacketHeaderInit = typeof AdbPacketHeader['TInit']; + export const AdbPacket = new Struct({ littleEndian: true }) .fields(AdbPacketHeader) @@ -26,34 +28,40 @@ export const AdbPacket = export type AdbPacket = typeof AdbPacket['TDeserializeResult']; -export type AdbPacketInit = Omit; +// All the useful fields +export type AdbPacketCore = Omit; + +// All fields except `magic`, which can be calculated in `AdbPacketSerializeStream` +export type AdbPacketInit = Omit; + +export function calculateChecksum(payload: Uint8Array): number; +export function calculateChecksum(init: AdbPacketCore): AdbPacketInit; +export function calculateChecksum(payload: Uint8Array | AdbPacketCore): number | AdbPacketInit { + if (payload instanceof Uint8Array) { + return payload.reduce((result, item) => result + item, 0); + } else { + (payload as AdbPacketInit).checksum = calculateChecksum(payload.payload); + return payload as AdbPacketInit; + } +} export class AdbPacketSerializeStream extends TransformStream{ - public calculateChecksum = true; - public constructor() { super({ transform: async (init, controller) => { - let checksum: number; - if (this.calculateChecksum && init.payload) { - const array = init.payload; - checksum = array.reduce((result, item) => result + item, 0); - } else { - checksum = 0; - } + // This syntax is ugly, but I don't want to create an new object. + (init as unknown as AdbPacketHeaderInit).magic = init.command ^ 0xFFFFFFFF; + (init as unknown as AdbPacketHeaderInit).payloadLength = init.payload.byteLength; - const packet = { - ...init, - checksum, - magic: init.command ^ 0xFFFFFFFF, - payloadLength: init.payload.byteLength, - }; + controller.enqueue( + AdbPacketHeader.serialize( + init as unknown as AdbPacketHeaderInit + ) + ); - controller.enqueue(AdbPacketHeader.serialize(packet)); - - if (packet.payloadLength) { + if (init.payload.byteLength) { // Enqueue payload separately to avoid copying - controller.enqueue(packet.payload); + controller.enqueue(init.payload); } }, }); diff --git a/libraries/adb/src/socket/dispatcher.ts b/libraries/adb/src/socket/dispatcher.ts index 4bc0e00b..495826fc 100644 --- a/libraries/adb/src/socket/dispatcher.ts +++ b/libraries/adb/src/socket/dispatcher.ts @@ -1,7 +1,7 @@ import { AsyncOperationManager } from '@yume-chan/async'; import { AutoDisposable, EventEmitter } from '@yume-chan/event'; -import { AdbCommand, AdbPacket, AdbPacketInit, AdbPacketSerializeStream } from '../packet'; -import { AbortController, ReadableStream, StructDeserializeStream, WritableStream, WritableStreamDefaultWriter } from '../stream'; +import { AdbCommand, AdbPacket, AdbPacketCore, AdbPacketInit, calculateChecksum } from '../packet'; +import { AbortController, ReadableWritablePair, WritableStream, WritableStreamDefaultWriter } from '../stream'; import { decodeUtf8, encodeUtf8 } from '../utils'; import { AdbSocketController } from './controller'; import { AdbLogger } from './logger'; @@ -32,12 +32,10 @@ export class AdbPacketDispatcher extends AutoDisposable { private readonly sockets = new Map(); private readonly logger: AdbLogger | undefined; - private _packetSerializeStream!: AdbPacketSerializeStream; private _packetSerializeStreamWriter!: WritableStreamDefaultWriter; public maxPayloadSize = 0; - public get calculateChecksum() { return this._packetSerializeStream.calculateChecksum; } - public set calculateChecksum(value: boolean) { this._packetSerializeStream.calculateChecksum = value; } + public calculateChecksum = true; public appendNullToServiceString = true; private readonly packetEvent = this.addDisposable(new EventEmitter()); @@ -51,17 +49,16 @@ export class AdbPacketDispatcher extends AutoDisposable { private _abortController = new AbortController(); - public constructor(readable: ReadableStream, writable: WritableStream, logger?: AdbLogger) { + public constructor( + connection: ReadableWritablePair, + logger?: AdbLogger + ) { super(); this.logger = logger; - readable - .pipeThrough( - new StructDeserializeStream(AdbPacket), - { signal: this._abortController.signal, preventCancel: true } - ) - .pipeTo(new WritableStream({ + connection.readable + .pipeTo(new WritableStream({ write: async (packet) => { try { this.logger?.onIncomingPacket?.(packet); @@ -75,7 +72,7 @@ export class AdbPacketDispatcher extends AutoDisposable { return; case AdbCommand.Write: if (this.sockets.has(packet.arg1)) { - await this.sockets.get(packet.arg1)!.enqueue(packet.payload!); + await this.sockets.get(packet.arg1)!.enqueue(packet.payload); await this.sendPacket(AdbCommand.OK, packet.arg1, packet.arg0); } @@ -106,17 +103,13 @@ export class AdbPacketDispatcher extends AutoDisposable { throw e; } } - })) + }), { + preventCancel: false, + signal: this._abortController.signal, + }) .catch(() => { }); - this._packetSerializeStream = new AdbPacketSerializeStream(); - this._packetSerializeStream.readable - .pipeTo( - writable, - { signal: this._abortController.signal, preventClose: true } - ) - .catch(() => { });; - this._packetSerializeStreamWriter = this._packetSerializeStream.writable.getWriter(); + this._packetSerializeStreamWriter = connection.writable.getWriter(); } private handleOk(packet: AdbPacket) { @@ -174,7 +167,7 @@ export class AdbPacketDispatcher extends AutoDisposable { this.initializers.resolve(localId, undefined); const remoteId = packet.arg0; - const serviceString = decodeUtf8(packet.payload!); + const serviceString = decodeUtf8(packet.payload); const controller = new AdbSocketController({ dispatcher: this, @@ -235,7 +228,7 @@ export class AdbPacketDispatcher extends AutoDisposable { arg1?: number, payload: string | Uint8Array = EmptyUint8Array, ): Promise { - let init: AdbPacketInit; + let init: AdbPacketCore; if (arg0 === undefined) { init = packetOrCommand as AdbPacketInit; } else { @@ -256,8 +249,14 @@ export class AdbPacketDispatcher extends AutoDisposable { throw new Error('payload too large'); } + if (this.calculateChecksum) { + calculateChecksum(init); + } else { + (init as AdbPacketInit).checksum = 0; + } + this.logger?.onOutgoingPacket?.(init); - await this._packetSerializeStreamWriter.write(init); + await this._packetSerializeStreamWriter.write(init as AdbPacketInit); } public override dispose() { @@ -271,6 +270,8 @@ export class AdbPacketDispatcher extends AutoDisposable { this._abortController.abort(); } catch { } + this._packetSerializeStreamWriter.releaseLock(); + super.dispose(); } } diff --git a/libraries/adb/src/socket/logger.ts b/libraries/adb/src/socket/logger.ts index 50f699cf..08f39f40 100644 --- a/libraries/adb/src/socket/logger.ts +++ b/libraries/adb/src/socket/logger.ts @@ -1,10 +1,10 @@ -import { AdbPacket, AdbPacketInit } from '../packet'; +import { AdbPacket, AdbPacketCore } from '../packet'; import { AdbSocket } from './socket'; export interface AdbLogger { onIncomingPacket?(packet: AdbPacket): void; - onOutgoingPacket?(packet: AdbPacketInit): void; + onOutgoingPacket?(packet: AdbPacketCore): void; onSocketOpened?(socket: AdbSocket): void; diff --git a/libraries/adb/src/stream/buffered.ts b/libraries/adb/src/stream/buffered.ts index c5cd1e4d..832a6ae2 100644 --- a/libraries/adb/src/stream/buffered.ts +++ b/libraries/adb/src/stream/buffered.ts @@ -1,6 +1,7 @@ import { StructAsyncDeserializeStream } from '@yume-chan/struct'; import { AdbSocket, AdbSocketInfo } from '../socket'; import { ReadableStream, ReadableStreamDefaultReader } from './detect'; +import { PushReadableStream } from "./transform"; export class BufferedStreamEndedError extends Error { public constructor() { @@ -26,10 +27,9 @@ export class BufferedStream { /** * * @param length - * @param readToEnd When `true`, allow less data to be returned if the stream has reached its end. * @returns */ - public async read(length: number, readToEnd: boolean = false): Promise { + public async read(length: number): Promise { let array: Uint8Array; let index: number; if (this.buffer) { @@ -44,16 +44,11 @@ export class BufferedStream { index = buffer.byteLength; this.buffer = undefined; } else { - const result = await this.reader.read(); - if (result.done) { - if (readToEnd) { - return new Uint8Array(0); - } else { - throw new Error('Unexpected end of stream'); - } + const { done, value } = await this.reader.read(); + if (done) { + throw new Error('Unexpected end of stream'); } - const { value } = result; if (value.byteLength === length) { return value; } @@ -71,16 +66,11 @@ export class BufferedStream { while (index < length) { const left = length - index; - const result = await this.reader.read(); - if (result.done) { - if (readToEnd) { - return new Uint8Array(0); - } else { - throw new Error('Unexpected end of stream'); - } + const { done, value } = await this.reader.read(); + if (done) { + throw new Error('Unexpected end of stream'); } - const { value } = result; if (value.byteLength === left) { array.set(value, index); return array; @@ -99,6 +89,40 @@ export class BufferedStream { return array; } + /** + * Return a readable stream with unconsumed data (if any) and + * all data from the wrapped stream. + * @returns A `ReadableStream` + */ + public release(): ReadableStream { + if (this.buffer) { + return new PushReadableStream(async controller => { + // Put the remaining data back to the stream + await controller.enqueue(this.buffer!); + + // Manually pipe the stream + while (true) { + try { + const { done, value } = await this.reader.read(); + if (done) { + controller.close(); + break; + } else { + await controller.enqueue(value); + } + } catch (e) { + controller.error(e); + break; + } + } + }); + } else { + // Simply release the reader and return the stream + this.reader.releaseLock(); + return this.stream; + } + } + public close() { this.reader.cancel(); } diff --git a/libraries/adb/src/stream/transform.ts b/libraries/adb/src/stream/transform.ts index 284ef5fb..640b071f 100644 --- a/libraries/adb/src/stream/transform.ts +++ b/libraries/adb/src/stream/transform.ts @@ -335,6 +335,16 @@ export class SplitLineStream extends TransformStream { } } +/** + * Create a new `WritableStream` that, when written to, will write that chunk to + * `pair.writable`, when pipe `pair.readable` to `writable`. + * + * It's the opposite of `ReadableStream.pipeThrough`. + * + * @param writable The `WritableStream` to write to. + * @param pair A `TransformStream` that converts chunks. + * @returns A new `WritableStream`. + */ export function pipeFrom(writable: WritableStream, pair: ReadableWritablePair) { const writer = pair.writable.getWriter(); const pipe = pair.readable diff --git a/libraries/android-bin/package.json b/libraries/android-bin/package.json index 319b8550..29d278a5 100644 --- a/libraries/android-bin/package.json +++ b/libraries/android-bin/package.json @@ -30,7 +30,7 @@ "prepublishOnly": "npm run build" }, "devDependencies": { - "typescript": "^4.5.5", + "typescript": "^4.6.2", "@yume-chan/ts-package-builder": "^1.0.0" }, "dependencies": { diff --git a/libraries/dataview-bigint-polyfill/package.json b/libraries/dataview-bigint-polyfill/package.json index fd753f48..41d5bc77 100644 --- a/libraries/dataview-bigint-polyfill/package.json +++ b/libraries/dataview-bigint-polyfill/package.json @@ -39,7 +39,7 @@ }, "devDependencies": { "jest": "^26.6.3", - "typescript": "^4.5.5", + "typescript": "^4.6.2", "@yume-chan/ts-package-builder": "^1.0.0" } } diff --git a/libraries/event/package.json b/libraries/event/package.json index c997cc3e..892b3101 100644 --- a/libraries/event/package.json +++ b/libraries/event/package.json @@ -37,7 +37,7 @@ }, "devDependencies": { "jest": "^26.6.3", - "typescript": "^4.5.5", + "typescript": "^4.6.2", "@yume-chan/ts-package-builder": "^1.0.0" } } diff --git a/libraries/scrcpy/package.json b/libraries/scrcpy/package.json index a4f41ed9..24cf43cc 100644 --- a/libraries/scrcpy/package.json +++ b/libraries/scrcpy/package.json @@ -48,7 +48,7 @@ "gh-release-fetch": "^2.0.4", "jest": "^26.6.3", "tinyh264": "^0.0.7", - "typescript": "^4.5.5", + "typescript": "^4.6.2", "yuv-buffer": "^1.0.0", "yuv-canvas": "^1.2.7" }, diff --git a/libraries/scrcpy/src/client.ts b/libraries/scrcpy/src/client.ts index bfe525e6..bc3b0fbe 100644 --- a/libraries/scrcpy/src/client.ts +++ b/libraries/scrcpy/src/client.ts @@ -1,4 +1,4 @@ -import { Adb, AdbBufferedStream, AdbNoneSubprocessProtocol, AdbSocket, AdbSubprocessProtocol, DecodeUtf8Stream, PushReadableStream, ReadableStream, TransformStream, WritableStreamDefaultWriter } from '@yume-chan/adb'; +import { Adb, AdbBufferedStream, AdbNoneSubprocessProtocol, AdbSocket, AdbSubprocessProtocol, DecodeUtf8Stream, InspectStream, ReadableStream, TransformStream, WritableStreamDefaultWriter } from '@yume-chan/adb'; import { EventEmitter } from '@yume-chan/event'; import Struct from '@yume-chan/struct'; import { AndroidMotionEventAction, ScrcpyControlMessageType, ScrcpyInjectKeyCodeControlMessage, ScrcpyInjectTextControlMessage, ScrcpyInjectTouchControlMessage, type AndroidKeyEventAction } from './message'; @@ -155,20 +155,14 @@ export class ScrcpyClient { }, })); - this._videoStream = new PushReadableStream(async controller => { - try { - while (true) { - const packet = await options.parseVideoStream(videoStream); - if (packet.type === 'configuration') { - this._screenWidth = packet.data.croppedWidth; - this._screenHeight = packet.data.croppedHeight; - } - await controller.enqueue(packet); + this._videoStream = options + .parseVideoStream(videoStream) + .pipeThrough(new InspectStream(packet => { + if (packet.type === 'configuration') { + this._screenWidth = packet.data.croppedWidth; + this._screenHeight = packet.data.croppedHeight; } - } catch { - controller.close(); - } - }); + })); if (controlStream) { const buffered = new AdbBufferedStream(controlStream); diff --git a/libraries/scrcpy/src/options/1_16/index.ts b/libraries/scrcpy/src/options/1_16/index.ts index cd461cb8..ea12e495 100644 --- a/libraries/scrcpy/src/options/1_16/index.ts +++ b/libraries/scrcpy/src/options/1_16/index.ts @@ -1,4 +1,4 @@ -import type { Adb, AdbBufferedStream } from "@yume-chan/adb"; +import { BufferedStreamEndedError, ReadableStream, TransformStream, type Adb, type AdbBufferedStream } from "@yume-chan/adb"; import Struct, { placeholder } from "@yume-chan/struct"; import type { AndroidCodecLevel, AndroidCodecProfile } from "../../codec"; import { ScrcpyClientConnection, ScrcpyClientConnectionOptions, ScrcpyClientForwardConnection, ScrcpyClientReverseConnection } from "../../connection"; @@ -135,8 +135,6 @@ export const ScrcpyInjectScrollControlMessage1_16 = export class ScrcpyOptions1_16 implements ScrcpyOptions { public value: Partial; - private _streamHeader: Uint8Array | undefined; - public constructor(value: Partial) { if (new.target === ScrcpyOptions1_16 && value.logLevel === ScrcpyLogLevel.Verbose) { @@ -213,74 +211,97 @@ export class ScrcpyOptions1_16 { + public parseVideoStream(stream: AdbBufferedStream): ReadableStream { + // Optimized path for video frames only if (this.value.sendFrameMeta === false) { - return { - type: 'frame', - data: await stream.read(1 * 1024 * 1024, true), - }; + return stream + .release() + .pipeThrough(new TransformStream({ + transform(chunk, controller) { + controller.enqueue({ + type: 'frame', + data: chunk, + }); + }, + })); } - const { pts, data } = await VideoPacket.deserialize(stream); - if (pts === NoPts) { - const sequenceParameterSet = parse_sequence_parameter_set(data.slice().buffer); + let header: Uint8Array | undefined; - const { - profile_idc: profileIndex, - constraint_set: constraintSet, - level_idc: levelIndex, - pic_width_in_mbs_minus1, - pic_height_in_map_units_minus1, - frame_mbs_only_flag, - frame_crop_left_offset, - frame_crop_right_offset, - frame_crop_top_offset, - frame_crop_bottom_offset, - } = sequenceParameterSet; + return new ReadableStream({ + async pull(controller) { + try { + const { pts, data } = await VideoPacket.deserialize(stream); + if (pts === NoPts) { + const sequenceParameterSet = parse_sequence_parameter_set(data.slice().buffer); - const encodedWidth = (pic_width_in_mbs_minus1 + 1) * 16; - const encodedHeight = (pic_height_in_map_units_minus1 + 1) * (2 - frame_mbs_only_flag) * 16; - const cropLeft = frame_crop_left_offset * 2; - const cropRight = frame_crop_right_offset * 2; - const cropTop = frame_crop_top_offset * 2; - const cropBottom = frame_crop_bottom_offset * 2; + const { + profile_idc: profileIndex, + constraint_set: constraintSet, + level_idc: levelIndex, + pic_width_in_mbs_minus1, + pic_height_in_map_units_minus1, + frame_mbs_only_flag, + frame_crop_left_offset, + frame_crop_right_offset, + frame_crop_top_offset, + frame_crop_bottom_offset, + } = sequenceParameterSet; - const croppedWidth = encodedWidth - cropLeft - cropRight; - const croppedHeight = encodedHeight - cropTop - cropBottom; + const encodedWidth = (pic_width_in_mbs_minus1 + 1) * 16; + const encodedHeight = (pic_height_in_map_units_minus1 + 1) * (2 - frame_mbs_only_flag) * 16; + const cropLeft = frame_crop_left_offset * 2; + const cropRight = frame_crop_right_offset * 2; + const cropTop = frame_crop_top_offset * 2; + const cropBottom = frame_crop_bottom_offset * 2; - this._streamHeader = data; - return { - type: 'configuration', - data: { - profileIndex, - constraintSet, - levelIndex, - encodedWidth, - encodedHeight, - cropLeft, - cropRight, - cropTop, - cropBottom, - croppedWidth, - croppedHeight, + const croppedWidth = encodedWidth - cropLeft - cropRight; + const croppedHeight = encodedHeight - cropTop - cropBottom; + + header = data; + controller.enqueue({ + type: 'configuration', + data: { + profileIndex, + constraintSet, + levelIndex, + encodedWidth, + encodedHeight, + cropLeft, + cropRight, + cropTop, + cropBottom, + croppedWidth, + croppedHeight, + } + }); + return; + } + + let frameData: Uint8Array; + if (header) { + frameData = new Uint8Array(header.byteLength + data.byteLength); + frameData.set(header); + frameData.set(data!, header.byteLength); + header = undefined; + } else { + frameData = data; + } + + controller.enqueue({ + type: 'frame', + data: frameData, + }); + } catch (e) { + if (e instanceof BufferedStreamEndedError) { + controller.close(); + return; + } + + throw e; } - }; - } - - let frameData: Uint8Array; - if (this._streamHeader) { - frameData = new Uint8Array(this._streamHeader.byteLength + data.byteLength); - frameData.set(this._streamHeader); - frameData.set(data!, this._streamHeader.byteLength); - this._streamHeader = undefined; - } else { - frameData = data; - } - - return { - type: 'frame', - data: frameData, - }; + } + }); } public serializeBackOrScreenOnControlMessage( diff --git a/libraries/scrcpy/src/options/common.ts b/libraries/scrcpy/src/options/common.ts index 325dbb31..0907f3c5 100644 --- a/libraries/scrcpy/src/options/common.ts +++ b/libraries/scrcpy/src/options/common.ts @@ -1,4 +1,4 @@ -import type { Adb, AdbBufferedStream } from "@yume-chan/adb"; +import type { Adb, AdbBufferedStream, ReadableStream } from "@yume-chan/adb"; import type { ScrcpyClientConnection } from "../connection"; import type { H264Configuration } from "../decoder"; import type { ScrcpyBackOrScreenOnEvent1_18 } from "./1_18"; @@ -49,7 +49,7 @@ export interface ScrcpyOptions { createConnection(adb: Adb): ScrcpyClientConnection; - parseVideoStream(stream: AdbBufferedStream): Promise; + parseVideoStream(stream: AdbBufferedStream): ReadableStream; serializeBackOrScreenOnControlMessage( message: ScrcpyBackOrScreenOnEvent1_18, diff --git a/libraries/struct/package.json b/libraries/struct/package.json index 0cc3ba69..aa2263b0 100644 --- a/libraries/struct/package.json +++ b/libraries/struct/package.json @@ -39,7 +39,7 @@ }, "devDependencies": { "jest": "^26.6.3", - "typescript": "^4.5.5", + "typescript": "^4.6.2", "@yume-chan/ts-package-builder": "^1.0.0", "@types/jest": "^27.4.0", "@types/bluebird": "^3.5.36"