commit d1f2452b2867131c43477b7ce4db638c14e5081f Author: 没复习 <2353956224@qq.com> Date: Wed Jul 30 13:39:32 2025 +0800 初始化 diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..9cb2bd2 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,6 @@ +src/public/**/*.js +vendor/**/*.js +vendor/**/*.ts +src/app/Util.ts +*.js +typings/**/*.d.ts diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..29a98e8 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,22 @@ +{ + "extends": [ + "plugin:@typescript-eslint/recommended", + "plugin:prettier/recommended", + "prettier" + ], + "plugins": [ + "progress", + "@typescript-eslint", + "prettier" + ], + "parserOptions": { + "ecmaVersion": 2020, + "sourceType": "module" + }, + "rules": { + "progress/activate": 1, + "import/no-absolute-path": "off" + }, + "overrides": [ + ] +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9e637ff --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/node_modules +/dist +/build +/.idea diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..9227b32 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +include=dev diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..15ed5a6 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,7 @@ +{ + "semi": true, + "trailingComma": "all", + "singleQuote": true, + "printWidth": 120, + "tabWidth": 4 +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a143f8a --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +Copyright (C) 2021 by Netris, JSC. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..dd3c5f8 --- /dev/null +++ b/README.md @@ -0,0 +1,227 @@ +# ws scrcpy + +Web client for [Genymobile/scrcpy][scrcpy] and more. + +## Requirements + +Browser must support the following technologies: +* WebSockets +* Media Source Extensions and h264 decoding; +* WebWorkers +* WebAssembly + +Server: +* Node.js v10+ +* node-gyp ([installation](https://github.com/nodejs/node-gyp#installation)) +* `adb` executable must be available in the PATH environment variable + +Device: +* Android 5.0+ (API 21+) +* Enabled [adb debugging](https://developer.android.com/studio/command-line/adb.html#Enabling) +* On some devices, you also need to enable +[an additional option](https://github.com/Genymobile/scrcpy/issues/70#issuecomment-373286323) +to control it using keyboard and mouse. + +## Build and Start + +Make sure you have installed [node.js](https://nodejs.org/en/download/), +[node-gyp](https://github.com/nodejs/node-gyp) and +[build tools](https://github.com/nodejs/node-gyp#installation) +```shell +git clone https://github.com/NetrisTV/ws-scrcpy.git +cd ws-scrcpy + +## For stable version find latest tag and switch to it: +# git tag -l +# git checkout vX.Y.Z + +npm install +npm start +``` + +## Supported features + +### Android + +#### Screen casting +The modified [version][fork] of [Genymobile/scrcpy][scrcpy] used to stream +H264-video, which then decoded by one of included decoders: + +##### Mse Player + +Based on [xevokk/h264-converter][xevokk/h264-converter]. +HTML5 Video.
+Requires [Media Source API][MSE] and `video/mp4; codecs="avc1.42E01E"` +[support][isTypeSupported]. Creates mp4 containers from NALU, received from a +device, then feeds them to [MediaSource][MediaSource]. In theory, it can use +hardware acceleration. + +##### Broadway Player + +Based on [mbebenita/Broadway][broadway] and +[131/h264-live-player][h264-live-player].
+Software video-decoder compiled into wasm-module. +Requires [WebAssembly][wasm] and preferably [WebGL][webgl] support. + +##### TinyH264 Player + +Based on [udevbe/tinyh264][tinyh264].
+Software video-decoder compiled into wasm-module. A slightly updated version of +[mbebenita/Broadway][broadway]. +Requires [WebAssembly][wasm], [WebWorkers][workers], [WebGL][webgl] support. + +##### WebCodecs Player + +Decoding is done by browser built-in (software/hardware) media decoder. +Requires [WebCodecs][webcodecs] support. At the moment, available only in +[Chromium](https://www.chromestatus.com/feature/5669293909868544) and derivatives. + +#### Remote control +* Touch events (including multi-touch) +* Multi-touch emulation: CTRL to start with center at the center of +the screen, SHIFT + CTRL to start with center at the +current point +* Mouse wheel and touchpad vertical/horizontal scrolling +* Capturing keyboard events +* Injecting text (ASCII only) +* Copy to/from device clipboard +* Device "rotation" + +#### File push +Drag & drop an APK file to push it to the `/data/local/tmp` directory. You can +install it manually from the included [xtermjs/xterm.js][xterm.js] terminal +emulator (see below). + +#### Remote shell +Control your device from `adb shell` in your browser. + +#### Debug WebPages/WebView +[/docs/Devtools.md](/docs/Devtools.md) + +#### File listing +* List files +* Upload files by drag & drop +* Download files + +### iOS + +***Experimental Feature***: *is not built by default* +(see [custom build](#custom-build)) + +#### Screen Casting + +Requires [ws-qvh][ws-qvh] available in `PATH`. + +#### MJPEG Server + +Enable `USE_WDA_MJPEG_SERVER` in the build configuration file +(see [custom build](#custom-build)). + +Alternative way to stream screen content. It does not +require additional software as `ws-qvh`, but may require more resources as each +frame encoded as jpeg image. + +#### Remote control + +To control device we use [appium/WebDriverAgent][WebDriverAgent]. +Functionality limited to: +* Simple touch +* Scroll +* Home button click + +Make sure you did properly [setup WebDriverAgent](https://appium.io/docs/en/drivers/ios-xcuitest-real-devices/). +WebDriverAgent project is located under `node_modules/appium-webdriveragent/`. + +You might want to enable `AssistiveTouch` on your device: `Settings/General/Accessibility`. + +## Custom Build + +You can customize project before build by overriding the +[default configuration](/webpack/default.build.config.json) in +[build.config.override.json](/build.config.override.json): +* `INCLUDE_APPL` - include code for iOS device tracking and control +* `INCLUDE_GOOG` - include code for Android device tracking and control +* `INCLUDE_ADB_SHELL` - [remote shell](#remote-shell) for android devices +([xtermjs/xterm.js][xterm.js], [Tyriar/node-pty][node-pty]) +* `INCLUDE_DEV_TOOLS` - [dev tools](#debug-webpageswebview) for web pages and +web views on android devices +* `INCLUDE_FILE_LISTING` - minimalistic [file management](#file-listing) +* `USE_BROADWAY` - include [Broadway Player](#broadway-player) +* `USE_H264_CONVERTER` - include [Mse Player](#mse-player) +* `USE_TINY_H264` - include [TinyH264 Player](#tinyh264-player) +* `USE_WEBCODECS` - include [WebCodecs Player](#webcodecs-player) +* `USE_WDA_MJPEG_SERVER` - configure WebDriverAgent to start MJPEG server +* `USE_QVH_SERVER` - include support for [ws-qvh][ws-qvh] +* `SCRCPY_LISTENS_ON_ALL_INTERFACES` - WebSocket server in `scrcpy-server.jar` +will listen for connections on all available interfaces. When `true`, it allows +connecting to device directly from a browser. Otherwise, the connection must be +established over adb. + +## Run configuration + +You can specify a path to a configuration file in `WS_SCRCPY_CONFIG` +environment variable. + +If you want to have another pathname than "/" you can specify it in the +`WS_SCRCPY_PATHNAME` environment variable. + +Configuration file format: [Configuration.d.ts](/src/types/Configuration.d.ts). + +Configuration file example: [config.example.yaml](/config.example.yaml). + +## Known issues + +* The server on the Android Emulator listens on the internal interface and not +available from the outside. Select `proxy over adb` from the interfaces list. +* TinyH264Player may fail to start, try to reload the page. +* MsePlayer reports too many dropped frames in quality statistics: needs +further investigation. +* On Safari file upload does not show progress (it works in one piece). + +## Security warning +Be advised and keep in mind: +* There is no encryption between browser and node.js server (you can [configure](#run-configuration) HTTPS). +* There is no encryption between browser and WebSocket server on android device. +* There is no authorization on any level. +* The modified version of scrcpy with integrated WebSocket server is listening +for connections on all network interfaces (see [custom build](#custom-build)). +* The modified version of scrcpy will keep running after the last client +disconnected. + +## Related projects +* [Genymobile/scrcpy][scrcpy] +* [xevokk/h264-converter][xevokk/h264-converter] +* [131/h264-live-player][h264-live-player] +* [mbebenita/Broadway][broadway] +* [DeviceFarmer/adbkit][adbkit] +* [xtermjs/xterm.js][xterm.js] +* [udevbe/tinyh264][tinyh264] +* [danielpaulus/quicktime_video_hack][qvh] + +## scrcpy websocket fork + +Currently, support of WebSocket protocol added to v1.19 of scrcpy +* [Prebuilt package](/vendor/Genymobile/scrcpy/scrcpy-server.jar) +* [Source code][fork] + +[fork]: https://github.com/NetrisTV/scrcpy/tree/feature/websocket-v1.19.x + +[scrcpy]: https://github.com/Genymobile/scrcpy +[xevokk/h264-converter]: https://github.com/xevokk/h264-converter +[h264-live-player]: https://github.com/131/h264-live-player +[broadway]: https://github.com/mbebenita/Broadway +[adbkit]: https://github.com/DeviceFarmer/adbkit +[xterm.js]: https://github.com/xtermjs/xterm.js +[tinyh264]: https://github.com/udevbe/tinyh264 +[node-pty]: https://github.com/Tyriar/node-pty +[WebDriverAgent]: https://github.com/appium/WebDriverAgent +[qvh]: https://github.com/danielpaulus/quicktime_video_hack +[ws-qvh]: https://github.com/NetrisTV/ws-qvh + +[MSE]: https://developer.mozilla.org/en-US/docs/Web/API/Media_Source_Extensions_API +[isTypeSupported]: https://developer.mozilla.org/en-US/docs/Web/API/MediaSource/isTypeSupported +[MediaSource]: https://developer.mozilla.org/en-US/docs/Web/API/MediaSource +[wasm]: https://developer.mozilla.org/en-US/docs/WebAssembly +[webgl]: https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API +[workers]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API +[webcodecs]: https://w3c.github.io/webcodecs/ diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..67e9e0a --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,4 @@ +# Reporting Security Issues + +If you discover a security issue, please report it by sending an +email to [drauggres@gmail.com](mailto:drauggres@gmail.com). diff --git a/build.config.override.json b/build.config.override.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/build.config.override.json @@ -0,0 +1 @@ +{} diff --git a/config.example.yaml b/config.example.yaml new file mode 100644 index 0000000..64f8454 --- /dev/null +++ b/config.example.yaml @@ -0,0 +1,40 @@ +# Run configuration example. See full config file spec in src/types/Configuration.d.ts + +# Device trackers +## track android devices (default: true, if was INCLUDE_GOOG enabled in build config) +runGoogTracker: false +## track iOS devices (default: true, if was INCLUDE_APPL enabled in build config) +runApplTracker: false; + +# HTTP[s] servers configuration +server: + - secure: false + port: 8000 + redirectToSecure: + port: 8443 + host: first-mobile-stand.example.com + - secure: true + port: 8443 + options: + certPath: /Users/example/ssl/STAR_example_com.crt + keyPath: /Users/example/ssl/STAR_example_com.key + +# Announce remote device trackers. The server doesn't check their availability. +remoteHostList: + - useProxy: true # optional, default: false + type: android # required, "android" | "ios" | ["android", "ios"] + secure: true # required, false for HTTP, true for HTTPS + hostname: second-mobile-stand.example.com + port: 8443 + - useProxy: true + type: ios + secure: true + hostname: second-mobile-stand.example.com + port: 8443 + - useProxy: true + type: # short variant + - ios + - android + secure: true + hostname: third-mobile-stand.example.com + port: 8443 diff --git a/docs/Devtools.md b/docs/Devtools.md new file mode 100644 index 0000000..a107cc7 --- /dev/null +++ b/docs/Devtools.md @@ -0,0 +1,33 @@ +# Devtools +Forward and proxy a WebKit debug-socket from an android device to your browser + +## How it works + +### Server +1. Find devtools sockets: `adb shell 'grep -a devtools_remote /proc/net/unix'` +2. For each socket request `/json` and `/json/version` +3. Replace websocket address in response with our hostname +4. Combine all data and send to a client + +### Client +Though each debuggable page explicitly specifies `devtoolsFrontendUrl` it is +possible that provided version of devtools frontend will not work in your +browser. To ensure that you will be able to debug webpage/webview, client +creates three links: +- `inspect` - this is a link provided by a remote browser in the answer for +`/json` request (only WebSocket address is changed). When this link points to +a local version of devtools (bundled with debuggable browser) you will not able +to open it, because only WebSocket forwarding is implemented at the moment. +- `bundled` - link to a version of devtools bundled with your (chromium based) +browser without specifying revision or version of the remote target. You will +get same link in the `chrome://inspect` page of Chromium browser. +e.g. `devtools://devtools/bundled/inspector.html?ws=` +- `remote` - link to a bundled devtools but with specified revision and version +of remote target. This link is visible only when original link in +`devtoolsFrontendUrl` contains revision. You will get same link in the + `chrome://inspect` page of Chrome browser. +e.g. `devtools://devtools/remote/serve_rev/@/inspector.html?remoteVersion=&remoteFrontend=true&ws=` + +**You can't open two last links with click or `open link in new tab`.** + +You must copy link and open it manually. This is browser restriction. diff --git a/docs/debug.md b/docs/debug.md new file mode 100644 index 0000000..98b6ef3 --- /dev/null +++ b/docs/debug.md @@ -0,0 +1,46 @@ +### Client + +1. Build dev version (will include source maps): +> npm run dist:dev + +2. Run from `dist` directory: +> npm run start + +3. Use the browser's built-in developer tools or your favorite IDE. + +### Node.js server + +1. `npm run dist:dev` +2. `cd dist` +3. `node --inspect-brk ./index.js` + +__HINT__: you might want to set `DEBUG` environment variable (see [debug](https://github.com/visionmedia/debug)): +> DEBUG=* node --inspect-brk ./index.js + +### Android server (`scrcpy-server.jar`) + +Source code is available [here](https://github.com/NetrisTV/scrcpy/tree/feature/websocket-server) +__HINT__: you might want to build a dev version. + +To debug the server: +1. start node server +2. kill server from UI (click button with cross and PID number). +3. upload server package to a device: +> adb push server/build/outputs/apk/debug/server-debug.apk /data/local/tmp/scrcpy-server.jar + +4. setup port forwarding: +> adb forward tcp:5005 tcp:5005 + +5. connect to device with adb shell: +> adb shell + +6.1. for Android 8 and below run this in adb shell (single line): +> CLASSPATH=/data/local/tmp/scrcpy-server.jar app_process -agentlib:jdwp=transport=dt_socket,suspend=y,server=y,address=5005 / com.genymobile.scrcpy.Server 1.17-ws5 DEBUG web 8886 + +6.2. for Android 9 and above: +> CLASSPATH=/data/local/tmp/scrcpy-server.jar app_process -XjdwpProvider:internal -XjdwpOptions:transport=dt_socket,suspend=y,server=y,address=5005 / com.genymobile.scrcpy.Server 1.17-ws5 web DEBUG 8886 + +7. Open project (scrcpy, not ws-scrcpy) in Android Studio, create `Remote` Debug configuration with: +> Host: localhost, Port: 5005 + +Connect the debugger to the remote server on the device. diff --git a/docs/scheme.md b/docs/scheme.md new file mode 100644 index 0000000..f3b5e75 --- /dev/null +++ b/docs/scheme.md @@ -0,0 +1,30 @@ +``` + +--------------------------+ +------------------------------+ + | Android device | | Server | + | | | | + | +----------------------+ | | +--------------------------+ | + | | adb | | Run scrcpy | | adb (client) | | + | | |<---------------| | | + | | (usb/tcp) | | | | | | + | +----------------------+ | | +--------------------------+ | + | | | | + | +----------------------+ | | +--------------------------+ | + | | scrcpy | | | | nodejs | | + | | | | | | | | +----| (ws://0.0.0.0:8886/) | | | | (http://0.0.0.0:8000/) |---- +| | +----------------------+ | | +--------------------------+ | | +| +--------------------------+ +------------------------------+ | +| | +| | +| | +| | +| | +| HTTP: | +| +------------------------------+ < static (html, js...)| +|Web-socket: | Client | | +|< Input events | | Web-socket: | +|> Video stream | +--------------------------+ | < Device list | +-------------------| Web-browser |----------------------------- + | +--------------------------+ | + +------------------------------+ +``` diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..8fb2e55 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,9411 @@ +{ + "name": "ws-scrcpy", + "version": "0.9.0-dev", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ws-scrcpy", + "version": "0.9.0-dev", + "license": "MIT", + "dependencies": { + "@dead50f7/adbkit": "^2.11.4", + "express": "^4.21.2", + "ios-device-lib": "^0.9.2", + "node-mjpeg-proxy": "^0.3.2", + "node-pty": "^0.10.1", + "portfinder": "^1.0.28", + "python-shell": "^4.0.0", + "tslib": "^2.3.1", + "ws": "^8.18.0", + "xml2js": "^0.6.2", + "yaml": "^2.2.2" + }, + "devDependencies": { + "@dead50f7/generate-package-json-webpack-plugin": "^2.6.1", + "@types/bluebird": "^3.5.36", + "@types/dom-webcodecs": "^0.1.3", + "@types/express": "^4.17.13", + "@types/node": "^12.20.47", + "@types/node-forge": "^0.10.0", + "@types/npmlog": "^4.1.4", + "@types/webpack-node-externals": "^2.5.3", + "@types/ws": "^7.4.7", + "@typescript-eslint/eslint-plugin": "^5.18.0", + "@typescript-eslint/parser": "^5.18.0", + "buffer": "^6.0.3", + "cross-env": "^7.0.3", + "css-loader": "^6.8.1", + "eslint": "^8.12.0", + "eslint-config-prettier": "^8.5.0", + "eslint-plugin-prettier": "^4.0.0", + "eslint-plugin-progress": "0.0.1", + "file-loader": "^6.2.0", + "h264-converter": "^0.1.4", + "html-webpack-plugin": "^5.5.0", + "ifdef-loader": "^2.3.2", + "mini-css-extract-plugin": "^2.6.1", + "mkdirp": "^1.0.4", + "path-browserify": "^1.0.1", + "prettier": "^2.6.2", + "recursive-copy": "^2.0.14", + "rimraf": "^3.0.0", + "svg-inline-loader": "^0.8.2", + "sylvester.js": "^0.1.1", + "tinyh264": "^0.0.7", + "ts-loader": "^9.3.1", + "ts-node": "^10.9.1", + "typescript": "^4.7.4", + "webpack": "^5.94.0", + "webpack-cli": "^4.10.0", + "webpack-node-externals": "^2.5.2", + "worker-loader": "^3.0.8", + "xterm": "^4.5.0", + "xterm-addon-attach": "^0.6.0", + "xterm-addon-fit": "^0.5.0" + }, + "optionalDependencies": { + "appium-xcuitest-driver": "^3.62.0" + } + }, + "node_modules/@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", + "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==", + "optional": true, + "dependencies": { + "@babel/highlight": "^7.23.4", + "chalk": "^2.4.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/code-frame/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "optional": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "optional": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "optional": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/code-frame/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "optional": true + }, + "node_modules/@babel/code-frame/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "optional": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/code-frame/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "optional": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "optional": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "optional": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", + "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", + "optional": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "optional": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "optional": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "optional": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/highlight/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "optional": true + }, + "node_modules/@babel/highlight/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "optional": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/highlight/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "optional": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "optional": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/runtime": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.0.tgz", + "integrity": "sha512-Chk32uHMg6TnQdvw2e9IlqPpFX/6NLuK0Ys2PqLb7/gL5uFn9mXvK715FGLlOLQrcO4qIkNHkvPGktzzXexsFw==", + "optional": true, + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@dabh/colors": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@dabh/colors/-/colors-1.4.0.tgz", + "integrity": "sha512-KmK/gXUdSvELjFUUnfCfkNqGZVlJvkZqXhjicGR4XvQKPgWsXFsRArBCW07Zj2UQUD6lsqru5ai/kk3KCgtxpA==", + "deprecated": "@dabh/colors has been renamed to @colors/colors. Please update your package.json.", + "optional": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@dead50f7/adbkit": { + "version": "2.11.5", + "resolved": "https://registry.npmjs.org/@dead50f7/adbkit/-/adbkit-2.11.5.tgz", + "integrity": "sha512-e2iLJKubqcHlwMXruk5ozAlUM+2F7E5RukCeosarw2247Bprq+dSKzcS6NoBeNdKOVZHSt3CwG9QWNj1NDz23Q==", + "dependencies": { + "@devicefarmer/adbkit-logcat": "^2.1.1", + "@devicefarmer/adbkit-monkey": "^1.2.0", + "@types/bluebird": "^2.0.33", + "@types/node-forge": "^0.9.5", + "bluebird": "~2.9.24", + "commander": "^9.1.0", + "debug": "^4.3.4", + "node-forge": "^1.3.0" + }, + "bin": { + "adbkit": "bin/adbkit" + }, + "engines": { + "node": ">= 0.10.4" + } + }, + "node_modules/@dead50f7/adbkit/node_modules/@types/bluebird": { + "version": "2.0.39", + "resolved": "https://registry.npmjs.org/@types/bluebird/-/bluebird-2.0.39.tgz", + "integrity": "sha512-fgP+wjgroorNtQX3Uai1s4W1pXd6yqv0ulr4KyE8AE3GZnT57yoRrW0MWnhYPfjhk7DBfBYMIzHojmESJOI2IA==" + }, + "node_modules/@dead50f7/adbkit/node_modules/@types/node-forge": { + "version": "0.9.10", + "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-0.9.10.tgz", + "integrity": "sha512-+BbPlhZeYs/WETWftQi2LeRx9VviWSwawNo+Pid5qNrSZHb60loYjpph3OrbwXMMseadu9rE9NeK34r4BHT+QQ==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@dead50f7/generate-package-json-webpack-plugin": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@dead50f7/generate-package-json-webpack-plugin/-/generate-package-json-webpack-plugin-2.6.1.tgz", + "integrity": "sha512-gpf/qchBXGPcQ+ujoo3+0oaqL4ZxPXSldARgqKBNEanxXNJf3lwq+Jhl7tBjemTaZYbLhDNnAJVShmr5c3+L5g==", + "dev": true + }, + "node_modules/@devicefarmer/adbkit-logcat": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@devicefarmer/adbkit-logcat/-/adbkit-logcat-2.1.3.tgz", + "integrity": "sha512-yeaGFjNBc/6+svbDeul1tNHtNChw6h8pSHAt5D+JsedUrMTN7tla7B15WLDyekxsuS2XlZHRxpuC6m92wiwCNw==", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@devicefarmer/adbkit-monkey": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@devicefarmer/adbkit-monkey/-/adbkit-monkey-1.2.1.tgz", + "integrity": "sha512-ZzZY/b66W2Jd6NHbAhLyDWOEIBWC11VizGFk7Wx7M61JZRz7HR9Cq5P+65RKWUU7u6wgsE8Lmh9nE4Mz+U2eTg==", + "engines": { + "node": ">= 0.10.4" + } + }, + "node_modules/@discoveryjs/json-ext": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", + "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", + "dev": true, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", + "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", + "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.14", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", + "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "dev": true, + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.2", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz", + "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==", + "dev": true + }, + "node_modules/@jimp/bmp": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/bmp/-/bmp-0.22.12.tgz", + "integrity": "sha512-aeI64HD0npropd+AR76MCcvvRaa+Qck6loCOS03CkkxGHN5/r336qTM5HPUdHKMDOGzqknuVPA8+kK1t03z12g==", + "optional": true, + "dependencies": { + "@jimp/utils": "^0.22.12", + "bmp-js": "^0.1.0" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/core": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/core/-/core-0.22.12.tgz", + "integrity": "sha512-l0RR0dOPyzMKfjUW1uebzueFEDtCOj9fN6pyTYWWOM/VS4BciXQ1VVrJs8pO3kycGYZxncRKhCoygbNr8eEZQA==", + "optional": true, + "dependencies": { + "@jimp/utils": "^0.22.12", + "any-base": "^1.1.0", + "buffer": "^5.2.0", + "exif-parser": "^0.1.12", + "file-type": "^16.5.4", + "isomorphic-fetch": "^3.0.0", + "pixelmatch": "^4.0.2", + "tinycolor2": "^1.6.0" + } + }, + "node_modules/@jimp/core/node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "optional": true, + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/@jimp/custom": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/custom/-/custom-0.22.12.tgz", + "integrity": "sha512-xcmww1O/JFP2MrlGUMd3Q78S3Qu6W3mYTXYuIqFq33EorgYHV/HqymHfXy9GjiCJ7OI+7lWx6nYFOzU7M4rd1Q==", + "optional": true, + "dependencies": { + "@jimp/core": "^0.22.12" + } + }, + "node_modules/@jimp/gif": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/gif/-/gif-0.22.12.tgz", + "integrity": "sha512-y6BFTJgch9mbor2H234VSjd9iwAhaNf/t3US5qpYIs0TSbAvM02Fbc28IaDETj9+4YB4676sz4RcN/zwhfu1pg==", + "optional": true, + "dependencies": { + "@jimp/utils": "^0.22.12", + "gifwrap": "^0.10.1", + "omggif": "^1.0.9" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/jpeg": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/jpeg/-/jpeg-0.22.12.tgz", + "integrity": "sha512-Rq26XC/uQWaQKyb/5lksCTCxXhtY01NJeBN+dQv5yNYedN0i7iYu+fXEoRsfaJ8xZzjoANH8sns7rVP4GE7d/Q==", + "optional": true, + "dependencies": { + "@jimp/utils": "^0.22.12", + "jpeg-js": "^0.4.4" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-blit": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/plugin-blit/-/plugin-blit-0.22.12.tgz", + "integrity": "sha512-xslz2ZoFZOPLY8EZ4dC29m168BtDx95D6K80TzgUi8gqT7LY6CsajWO0FAxDwHz6h0eomHMfyGX0stspBrTKnQ==", + "optional": true, + "dependencies": { + "@jimp/utils": "^0.22.12" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-blur": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/plugin-blur/-/plugin-blur-0.22.12.tgz", + "integrity": "sha512-S0vJADTuh1Q9F+cXAwFPlrKWzDj2F9t/9JAbUvaaDuivpyWuImEKXVz5PUZw2NbpuSHjwssbTpOZ8F13iJX4uw==", + "optional": true, + "dependencies": { + "@jimp/utils": "^0.22.12" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-circle": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/plugin-circle/-/plugin-circle-0.22.12.tgz", + "integrity": "sha512-SWVXx1yiuj5jZtMijqUfvVOJBwOifFn0918ou4ftoHgegc5aHWW5dZbYPjvC9fLpvz7oSlptNl2Sxr1zwofjTg==", + "optional": true, + "dependencies": { + "@jimp/utils": "^0.22.12" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-color": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/plugin-color/-/plugin-color-0.22.12.tgz", + "integrity": "sha512-xImhTE5BpS8xa+mAN6j4sMRWaUgUDLoaGHhJhpC+r7SKKErYDR0WQV4yCE4gP+N0gozD0F3Ka1LUSaMXrn7ZIA==", + "optional": true, + "dependencies": { + "@jimp/utils": "^0.22.12", + "tinycolor2": "^1.6.0" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-contain": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/plugin-contain/-/plugin-contain-0.22.12.tgz", + "integrity": "sha512-Eo3DmfixJw3N79lWk8q/0SDYbqmKt1xSTJ69yy8XLYQj9svoBbyRpSnHR+n9hOw5pKXytHwUW6nU4u1wegHNoQ==", + "optional": true, + "dependencies": { + "@jimp/utils": "^0.22.12" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5", + "@jimp/plugin-blit": ">=0.3.5", + "@jimp/plugin-resize": ">=0.3.5", + "@jimp/plugin-scale": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-cover": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/plugin-cover/-/plugin-cover-0.22.12.tgz", + "integrity": "sha512-z0w/1xH/v/knZkpTNx+E8a7fnasQ2wHG5ze6y5oL2dhH1UufNua8gLQXlv8/W56+4nJ1brhSd233HBJCo01BXA==", + "optional": true, + "dependencies": { + "@jimp/utils": "^0.22.12" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5", + "@jimp/plugin-crop": ">=0.3.5", + "@jimp/plugin-resize": ">=0.3.5", + "@jimp/plugin-scale": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-crop": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/plugin-crop/-/plugin-crop-0.22.12.tgz", + "integrity": "sha512-FNuUN0OVzRCozx8XSgP9MyLGMxNHHJMFt+LJuFjn1mu3k0VQxrzqbN06yIl46TVejhyAhcq5gLzqmSCHvlcBVw==", + "optional": true, + "dependencies": { + "@jimp/utils": "^0.22.12" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-displace": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/plugin-displace/-/plugin-displace-0.22.12.tgz", + "integrity": "sha512-qpRM8JRicxfK6aPPqKZA6+GzBwUIitiHaZw0QrJ64Ygd3+AsTc7BXr+37k2x7QcyCvmKXY4haUrSIsBug4S3CA==", + "optional": true, + "dependencies": { + "@jimp/utils": "^0.22.12" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-dither": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/plugin-dither/-/plugin-dither-0.22.12.tgz", + "integrity": "sha512-jYgGdSdSKl1UUEanX8A85v4+QUm+PE8vHFwlamaKk89s+PXQe7eVE3eNeSZX4inCq63EHL7cX580dMqkoC3ZLw==", + "optional": true, + "dependencies": { + "@jimp/utils": "^0.22.12" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-fisheye": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/plugin-fisheye/-/plugin-fisheye-0.22.12.tgz", + "integrity": "sha512-LGuUTsFg+fOp6KBKrmLkX4LfyCy8IIsROwoUvsUPKzutSqMJnsm3JGDW2eOmWIS/jJpPaeaishjlxvczjgII+Q==", + "optional": true, + "dependencies": { + "@jimp/utils": "^0.22.12" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-flip": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/plugin-flip/-/plugin-flip-0.22.12.tgz", + "integrity": "sha512-m251Rop7GN8W0Yo/rF9LWk6kNclngyjIJs/VXHToGQ6EGveOSTSQaX2Isi9f9lCDLxt+inBIb7nlaLLxnvHX8Q==", + "optional": true, + "dependencies": { + "@jimp/utils": "^0.22.12" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5", + "@jimp/plugin-rotate": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-gaussian": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/plugin-gaussian/-/plugin-gaussian-0.22.12.tgz", + "integrity": "sha512-sBfbzoOmJ6FczfG2PquiK84NtVGeScw97JsCC3rpQv1PHVWyW+uqWFF53+n3c8Y0P2HWlUjflEla2h/vWShvhg==", + "optional": true, + "dependencies": { + "@jimp/utils": "^0.22.12" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-invert": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/plugin-invert/-/plugin-invert-0.22.12.tgz", + "integrity": "sha512-N+6rwxdB+7OCR6PYijaA/iizXXodpxOGvT/smd/lxeXsZ/empHmFFFJ/FaXcYh19Tm04dGDaXcNF/dN5nm6+xQ==", + "optional": true, + "dependencies": { + "@jimp/utils": "^0.22.12" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-mask": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/plugin-mask/-/plugin-mask-0.22.12.tgz", + "integrity": "sha512-4AWZg+DomtpUA099jRV8IEZUfn1wLv6+nem4NRJC7L/82vxzLCgXKTxvNvBcNmJjT9yS1LAAmiJGdWKXG63/NA==", + "optional": true, + "dependencies": { + "@jimp/utils": "^0.22.12" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-normalize": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/plugin-normalize/-/plugin-normalize-0.22.12.tgz", + "integrity": "sha512-0So0rexQivnWgnhacX4cfkM2223YdExnJTTy6d06WbkfZk5alHUx8MM3yEzwoCN0ErO7oyqEWRnEkGC+As1FtA==", + "optional": true, + "dependencies": { + "@jimp/utils": "^0.22.12" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-print": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/plugin-print/-/plugin-print-0.22.12.tgz", + "integrity": "sha512-c7TnhHlxm87DJeSnwr/XOLjJU/whoiKYY7r21SbuJ5nuH+7a78EW1teOaj5gEr2wYEd7QtkFqGlmyGXY/YclyQ==", + "optional": true, + "dependencies": { + "@jimp/utils": "^0.22.12", + "load-bmfont": "^1.4.1" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5", + "@jimp/plugin-blit": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-resize": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/plugin-resize/-/plugin-resize-0.22.12.tgz", + "integrity": "sha512-3NyTPlPbTnGKDIbaBgQ3HbE6wXbAlFfxHVERmrbqAi8R3r6fQPxpCauA8UVDnieg5eo04D0T8nnnNIX//i/sXg==", + "optional": true, + "dependencies": { + "@jimp/utils": "^0.22.12" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-rotate": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/plugin-rotate/-/plugin-rotate-0.22.12.tgz", + "integrity": "sha512-9YNEt7BPAFfTls2FGfKBVgwwLUuKqy+E8bDGGEsOqHtbuhbshVGxN2WMZaD4gh5IDWvR+emmmPPWGgaYNYt1gA==", + "optional": true, + "dependencies": { + "@jimp/utils": "^0.22.12" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5", + "@jimp/plugin-blit": ">=0.3.5", + "@jimp/plugin-crop": ">=0.3.5", + "@jimp/plugin-resize": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-scale": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/plugin-scale/-/plugin-scale-0.22.12.tgz", + "integrity": "sha512-dghs92qM6MhHj0HrV2qAwKPMklQtjNpoYgAB94ysYpsXslhRTiPisueSIELRwZGEr0J0VUxpUY7HgJwlSIgGZw==", + "optional": true, + "dependencies": { + "@jimp/utils": "^0.22.12" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5", + "@jimp/plugin-resize": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-shadow": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/plugin-shadow/-/plugin-shadow-0.22.12.tgz", + "integrity": "sha512-FX8mTJuCt7/3zXVoeD/qHlm4YH2bVqBuWQHXSuBK054e7wFRnRnbSLPUqAwSeYP3lWqpuQzJtgiiBxV3+WWwTg==", + "optional": true, + "dependencies": { + "@jimp/utils": "^0.22.12" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5", + "@jimp/plugin-blur": ">=0.3.5", + "@jimp/plugin-resize": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-threshold": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/plugin-threshold/-/plugin-threshold-0.22.12.tgz", + "integrity": "sha512-4x5GrQr1a/9L0paBC/MZZJjjgjxLYrqSmWd+e+QfAEPvmRxdRoQ5uKEuNgXnm9/weHQBTnQBQsOY2iFja+XGAw==", + "optional": true, + "dependencies": { + "@jimp/utils": "^0.22.12" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5", + "@jimp/plugin-color": ">=0.8.0", + "@jimp/plugin-resize": ">=0.8.0" + } + }, + "node_modules/@jimp/plugins": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/plugins/-/plugins-0.22.12.tgz", + "integrity": "sha512-yBJ8vQrDkBbTgQZLty9k4+KtUQdRjsIDJSPjuI21YdVeqZxYywifHl4/XWILoTZsjTUASQcGoH0TuC0N7xm3ww==", + "optional": true, + "dependencies": { + "@jimp/plugin-blit": "^0.22.12", + "@jimp/plugin-blur": "^0.22.12", + "@jimp/plugin-circle": "^0.22.12", + "@jimp/plugin-color": "^0.22.12", + "@jimp/plugin-contain": "^0.22.12", + "@jimp/plugin-cover": "^0.22.12", + "@jimp/plugin-crop": "^0.22.12", + "@jimp/plugin-displace": "^0.22.12", + "@jimp/plugin-dither": "^0.22.12", + "@jimp/plugin-fisheye": "^0.22.12", + "@jimp/plugin-flip": "^0.22.12", + "@jimp/plugin-gaussian": "^0.22.12", + "@jimp/plugin-invert": "^0.22.12", + "@jimp/plugin-mask": "^0.22.12", + "@jimp/plugin-normalize": "^0.22.12", + "@jimp/plugin-print": "^0.22.12", + "@jimp/plugin-resize": "^0.22.12", + "@jimp/plugin-rotate": "^0.22.12", + "@jimp/plugin-scale": "^0.22.12", + "@jimp/plugin-shadow": "^0.22.12", + "@jimp/plugin-threshold": "^0.22.12", + "timm": "^1.6.1" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/png": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/png/-/png-0.22.12.tgz", + "integrity": "sha512-Mrp6dr3UTn+aLK8ty/dSKELz+Otdz1v4aAXzV5q53UDD2rbB5joKVJ/ChY310B+eRzNxIovbUF1KVrUsYdE8Hg==", + "optional": true, + "dependencies": { + "@jimp/utils": "^0.22.12", + "pngjs": "^6.0.0" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/tiff": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/tiff/-/tiff-0.22.12.tgz", + "integrity": "sha512-E1LtMh4RyJsoCAfAkBRVSYyZDTtLq9p9LUiiYP0vPtXyxX4BiYBUYihTLSBlCQg5nF2e4OpQg7SPrLdJ66u7jg==", + "optional": true, + "dependencies": { + "utif2": "^4.0.1" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/types": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/types/-/types-0.22.12.tgz", + "integrity": "sha512-wwKYzRdElE1MBXFREvCto5s699izFHNVvALUv79GXNbsOVqlwlOxlWJ8DuyOGIXoLP4JW/m30YyuTtfUJgMRMA==", + "optional": true, + "dependencies": { + "@jimp/bmp": "^0.22.12", + "@jimp/gif": "^0.22.12", + "@jimp/jpeg": "^0.22.12", + "@jimp/png": "^0.22.12", + "@jimp/tiff": "^0.22.12", + "timm": "^1.6.1" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/utils": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/utils/-/utils-0.22.12.tgz", + "integrity": "sha512-yJ5cWUknGnilBq97ZXOyOS0HhsHOyAyjHwYfHxGbSyMTohgQI6sVyE8KPgDwH8HHW/nMKXk8TrSwAE71zt716Q==", + "optional": true, + "dependencies": { + "regenerator-runtime": "^0.13.3" + } + }, + "node_modules/@jimp/utils/node_modules/regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "optional": true + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.5.tgz", + "integrity": "sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@puppeteer/browsers": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.3.1.tgz", + "integrity": "sha512-uK7o3hHkK+naEobMSJ+2ySYyXtQkBxIH8Gn4MK9ciePjNV+Pf+PgY/W7iPzn2MTjl3stcYB5AlcTmPYw7AXDwA==", + "optional": true, + "dependencies": { + "debug": "^4.3.6", + "extract-zip": "^2.0.1", + "progress": "^2.0.3", + "proxy-agent": "^6.4.0", + "semver": "^7.6.3", + "tar-fs": "^3.0.6", + "unbzip2-stream": "^1.4.3", + "yargs": "^17.7.2" + }, + "bin": { + "browsers": "lib/cjs/main-cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@szmarczak/http-timer": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", + "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", + "optional": true, + "dependencies": { + "defer-to-connect": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@tokenizer/token": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", + "optional": true + }, + "node_modules/@tootallnate/quickjs-emscripten": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", + "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", + "optional": true + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", + "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==", + "dev": true + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true + }, + "node_modules/@types/bluebird": { + "version": "3.5.42", + "resolved": "https://registry.npmjs.org/@types/bluebird/-/bluebird-3.5.42.tgz", + "integrity": "sha512-Jhy+MWRlro6UjVi578V/4ZGNfeCOcNCp0YaFNIUGFKlImowqwb1O/22wDVk3FDGMLqxdpOV3qQHD5fPEH4hK6A==", + "devOptional": true + }, + "node_modules/@types/body-parser": { + "version": "1.19.5", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", + "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "dev": true, + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/cacheable-request": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", + "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", + "optional": true, + "dependencies": { + "@types/http-cache-semantics": "*", + "@types/keyv": "^3.1.4", + "@types/node": "*", + "@types/responselike": "^1.0.0" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/dom-webcodecs": { + "version": "0.1.11", + "resolved": "https://registry.npmjs.org/@types/dom-webcodecs/-/dom-webcodecs-0.1.11.tgz", + "integrity": "sha512-yPEZ3z7EohrmOxbk/QTAa0yonMFkNkjnVXqbGb7D4rMr+F1dGQ8ZUFxXkyLLJuiICPejZ0AZE9Rrk9wUCczx4A==", + "dev": true + }, + "node_modules/@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "dev": true + }, + "node_modules/@types/express": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", + "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", + "dev": true, + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.17.43", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.43.tgz", + "integrity": "sha512-oaYtiBirUOPQGSWNGPWnzyAFJ0BP3cwvN4oWZQY+zUBwpVIGsKUkpBpSztp74drYcjavs7SKFZ4DX1V2QeN8rg==", + "dev": true, + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/html-minifier-terser": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", + "integrity": "sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==", + "dev": true + }, + "node_modules/@types/http-cache-semantics": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", + "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==", + "optional": true + }, + "node_modules/@types/http-errors": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", + "dev": true + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true + }, + "node_modules/@types/keyv": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", + "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true + }, + "node_modules/@types/node": { + "version": "12.20.55", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz", + "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==" + }, + "node_modules/@types/node-forge": { + "version": "0.10.10", + "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-0.10.10.tgz", + "integrity": "sha512-iixn5bedlE9fm/5mN7fPpXraXlxCVrnNWHZekys8c5fknridLVWGnNRqlaWpenwaijIuB3bNI0lEOm+JD6hZUA==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/npmlog": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@types/npmlog/-/npmlog-4.1.6.tgz", + "integrity": "sha512-0l3z16vnlJGl2Mi/rgJFrdwfLZ4jfNYgE6ZShEpjqhHuGTqdEzNles03NpYHwUMVYZa+Tj46UxKIEpE78lQ3DQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/puppeteer": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/@types/puppeteer/-/puppeteer-7.0.4.tgz", + "integrity": "sha512-ja78vquZc8y+GM2al07GZqWDKQskQXygCDiu0e3uO0DMRKqE0MjrFBFmTulfPYzLB6WnL7Kl2tFPy0WXSpPomg==", + "deprecated": "This is a stub types definition. puppeteer provides its own type definitions, so you do not need this installed.", + "optional": true, + "dependencies": { + "puppeteer": "*" + } + }, + "node_modules/@types/puppeteer-core": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@types/puppeteer-core/-/puppeteer-core-5.4.0.tgz", + "integrity": "sha512-yqRPuv4EFcSkTyin6Yy17pN6Qz2vwVwTCJIDYMXbE3j8vTPhv0nCQlZOl5xfi0WHUkqvQsjAR8hAfjeMCoetwg==", + "optional": true, + "dependencies": { + "@types/puppeteer": "*" + } + }, + "node_modules/@types/qs": { + "version": "6.9.12", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.12.tgz", + "integrity": "sha512-bZcOkJ6uWrL0Qb2NAWKa7TBU+mJHPzhx9jjLL1KHF+XpzEcR7EXHvjbHlGtR/IsP1vyPrehuS6XqkmaePy//mg==", + "dev": true + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true + }, + "node_modules/@types/responselike": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", + "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/semver": { + "version": "7.5.8", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", + "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==", + "dev": true + }, + "node_modules/@types/send": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "dev": true, + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.5.tgz", + "integrity": "sha512-PDRk21MnK70hja/YF8AHfC7yIsiQHn1rcXx7ijCFBX/k+XQJhQT/gw3xekXKJvx+5SXaMMS8oqQy09Mzvz2TuQ==", + "dev": true, + "dependencies": { + "@types/http-errors": "*", + "@types/mime": "*", + "@types/node": "*" + } + }, + "node_modules/@types/webpack-node-externals": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/@types/webpack-node-externals/-/webpack-node-externals-2.5.3.tgz", + "integrity": "sha512-A9JxaR8QXoYT95egET4AmCFuChyTlP8d18ZAnmSHuIMsFdS7QlCQQ8pmN/+FHgLIkm+ViE/VngltT5avLACY9A==", + "dev": true, + "dependencies": { + "@types/node": "*", + "webpack": "^5" + } + }, + "node_modules/@types/which": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@types/which/-/which-1.3.2.tgz", + "integrity": "sha512-8oDqyLC7eD4HM307boe2QWKyuzdzWBj56xI/imSl2cpL+U3tCMaTAkMJ4ee5JBZ/FsOJlvRGeIShiZDAl1qERA==", + "optional": true + }, + "node_modules/@types/ws": { + "version": "7.4.7", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-7.4.7.tgz", + "integrity": "sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz", + "integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==", + "dev": true, + "dependencies": { + "@eslint-community/regexpp": "^4.4.0", + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/type-utils": "5.62.0", + "@typescript-eslint/utils": "5.62.0", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "natural-compare-lite": "^1.4.0", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^5.0.0", + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz", + "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/typescript-estree": "5.62.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz", + "integrity": "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz", + "integrity": "sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==", + "dev": true, + "dependencies": { + "@typescript-eslint/typescript-estree": "5.62.0", + "@typescript-eslint/utils": "5.62.0", + "debug": "^4.3.4", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz", + "integrity": "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz", + "integrity": "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz", + "integrity": "sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@types/json-schema": "^7.0.9", + "@types/semver": "^7.3.12", + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/typescript-estree": "5.62.0", + "eslint-scope": "^5.1.1", + "semver": "^7.3.7" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz", + "integrity": "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "dev": true + }, + "node_modules/@wdio/config": { + "version": "6.12.1", + "resolved": "https://registry.npmjs.org/@wdio/config/-/config-6.12.1.tgz", + "integrity": "sha512-V5hTIW5FNlZ1W33smHF4Rd5BKjGW2KeYhyXDQfXHjqLCeRiirZ9fABCo9plaVQDnwWSUMWYaAaIAifV82/oJCQ==", + "optional": true, + "dependencies": { + "@wdio/logger": "6.10.10", + "deepmerge": "^4.0.0", + "glob": "^7.1.2" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@wdio/logger": { + "version": "6.10.10", + "resolved": "https://registry.npmjs.org/@wdio/logger/-/logger-6.10.10.tgz", + "integrity": "sha512-2nh0hJz9HeZE0VIEMI+oPgjr/Q37ohrR9iqsl7f7GW5ik+PnKYCT9Eab5mR1GNMG60askwbskgGC1S9ygtvrSw==", + "optional": true, + "dependencies": { + "chalk": "^4.0.0", + "loglevel": "^1.6.0", + "loglevel-plugin-prefix": "^0.8.4", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@wdio/protocols": { + "version": "6.12.0", + "resolved": "https://registry.npmjs.org/@wdio/protocols/-/protocols-6.12.0.tgz", + "integrity": "sha512-UhTBZxClCsM3VjaiDp4DoSCnsa7D1QNmI2kqEBfIpyNkT3GcZhJb7L+nL0fTkzCwi7+/uLastb3/aOwH99gt0A==", + "optional": true, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@wdio/repl": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/@wdio/repl/-/repl-6.11.0.tgz", + "integrity": "sha512-FxrFKiTkFyELNGGVEH1uijyvNY7lUpmff6x+FGskFGZB4uSRs0rxkOMaEjxnxw7QP1zgQKr2xC7GyO03gIGRGg==", + "optional": true, + "dependencies": { + "@wdio/utils": "6.11.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@wdio/utils": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/@wdio/utils/-/utils-6.11.0.tgz", + "integrity": "sha512-vf0sOQzd28WbI26d6/ORrQ4XKWTzSlWLm9W/K/eJO0NASKPEzR+E+Q2kaa+MJ4FKXUpjbt+Lxfo+C26TzBk7tg==", + "optional": true, + "dependencies": { + "@wdio/logger": "6.10.10" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.12.1.tgz", + "integrity": "sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg==", + "dev": true, + "dependencies": { + "@webassemblyjs/helper-numbers": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz", + "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz", + "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.12.1.tgz", + "integrity": "sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz", + "integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==", + "dev": true, + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.11.6", + "@webassemblyjs/helper-api-error": "1.11.6", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz", + "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.12.1.tgz", + "integrity": "sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-buffer": "1.12.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/wasm-gen": "1.12.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz", + "integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==", + "dev": true, + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz", + "integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==", + "dev": true, + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz", + "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==", + "dev": true + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.12.1.tgz", + "integrity": "sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-buffer": "1.12.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/helper-wasm-section": "1.12.1", + "@webassemblyjs/wasm-gen": "1.12.1", + "@webassemblyjs/wasm-opt": "1.12.1", + "@webassemblyjs/wasm-parser": "1.12.1", + "@webassemblyjs/wast-printer": "1.12.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.12.1.tgz", + "integrity": "sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/ieee754": "1.11.6", + "@webassemblyjs/leb128": "1.11.6", + "@webassemblyjs/utf8": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.12.1.tgz", + "integrity": "sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-buffer": "1.12.1", + "@webassemblyjs/wasm-gen": "1.12.1", + "@webassemblyjs/wasm-parser": "1.12.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz", + "integrity": "sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-api-error": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/ieee754": "1.11.6", + "@webassemblyjs/leb128": "1.11.6", + "@webassemblyjs/utf8": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.12.1.tgz", + "integrity": "sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.12.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webpack-cli/configtest": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-1.2.0.tgz", + "integrity": "sha512-4FB8Tj6xyVkyqjj1OaTqCjXYULB9FMkqQ8yGrZjRDrYh0nOE+7Lhs45WioWQQMV+ceFlE368Ukhe6xdvJM9Egg==", + "dev": true, + "peerDependencies": { + "webpack": "4.x.x || 5.x.x", + "webpack-cli": "4.x.x" + } + }, + "node_modules/@webpack-cli/info": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-1.5.0.tgz", + "integrity": "sha512-e8tSXZpw2hPl2uMJY6fsMswaok5FdlGNRTktvFk2sD8RjH0hE2+XistawJx1vmKteh4NmGmNUrp+Tb2w+udPcQ==", + "dev": true, + "dependencies": { + "envinfo": "^7.7.3" + }, + "peerDependencies": { + "webpack-cli": "4.x.x" + } + }, + "node_modules/@webpack-cli/serve": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-1.7.0.tgz", + "integrity": "sha512-oxnCNGj88fL+xzV+dacXs44HcDwf1ovs3AuEzvP7mqXw7fQntqIhQ1BRmynh4qEKQSSSRSWVyXRjmTbZIX9V2Q==", + "dev": true, + "peerDependencies": { + "webpack-cli": "4.x.x" + }, + "peerDependenciesMeta": { + "webpack-dev-server": { + "optional": true + } + } + }, + "node_modules/@xmldom/xmldom": { + "version": "0.8.10", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz", + "integrity": "sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==", + "optional": true, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-attributes": { + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", + "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", + "dev": true, + "peerDependencies": { + "acorn": "^8" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz", + "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "optional": true, + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "devOptional": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-base": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/any-base/-/any-base-1.1.0.tgz", + "integrity": "sha512-uMgjozySS8adZZYePpaWs8cxB9/kdzmpX6SgJZ+wbz1K5eYk5QMYDVJaZKhxyIHUdnnJkfR7SVgStgH7LkGUyg==", + "optional": true + }, + "node_modules/appium-base-driver": { + "version": "7.11.3", + "resolved": "https://registry.npmjs.org/appium-base-driver/-/appium-base-driver-7.11.3.tgz", + "integrity": "sha512-BzB6OYQUV8yfVu0JJB2uTs5tW6umrM7sZJO8sCx/e+dIg8e19F9p4nwwasBfsbfykmIXRLqqwHsZVnh5eCIpJA==", + "engines": [ + "node" + ], + "optional": true, + "dependencies": { + "@babel/runtime": "^7.0.0", + "@dabh/colors": "^1.4.0", + "appium-support": "^2.54.1", + "async-lock": "^1.0.0", + "asyncbox": "^2.9.1", + "axios": "^0.x", + "bluebird": "^3.5.3", + "body-parser": "^1.18.2", + "es6-error": "^4.1.1", + "express": "^4.16.2", + "http-status-codes": "^2.1.1", + "lodash": "^4.0.0", + "lru-cache": "^6.0.0", + "method-override": "^3.0.0", + "morgan": "^1.9.0", + "serve-favicon": "^2.4.5", + "source-map-support": "^0.x", + "validate.js": "^0.13.0", + "webdriverio": "^6.0.2", + "ws": "^8.0.0" + } + }, + "node_modules/appium-base-driver/node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "optional": true + }, + "node_modules/appium-idb": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/appium-idb/-/appium-idb-0.5.1.tgz", + "integrity": "sha512-xijrqWSxJtKtWvXPVVR/FkVJtfQ5DwVCCA5R0nV0jMOtET55CoKKx0TO7KIiuZaHfQv/dbBCR8u7SEe3MF5Q9Q==", + "engines": [ + "node" + ], + "optional": true, + "dependencies": { + "@babel/runtime": "^7.0.0", + "appium-support": "^2.41.0", + "asyncbox": "^2.5.2", + "bluebird": "^3.1.1", + "lodash": "^4.0.0", + "source-map-support": "^0.5.5", + "teen_process": "^1.11.0" + } + }, + "node_modules/appium-idb/node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "optional": true + }, + "node_modules/appium-ios-device": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/appium-ios-device/-/appium-ios-device-1.8.0.tgz", + "integrity": "sha512-l4PVO0RSCsgd9wLiHhpBOlCP2wM0ND3+9+UMKP72c3qaaC2JxEqiKNIoc0r/jW7D5/Aoh/MtTT2yFHvqtZ0r/Q==", + "engines": [ + "node" + ], + "optional": true, + "dependencies": { + "@babel/runtime": "^7.0.0", + "appium-support": "^2.35.0", + "bluebird": "^3.1.1", + "lodash": "^4.17.15", + "semver": "^7.0.0", + "source-map-support": "^0.5.5" + } + }, + "node_modules/appium-ios-device/node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "optional": true + }, + "node_modules/appium-ios-simulator": { + "version": "3.29.0", + "resolved": "https://registry.npmjs.org/appium-ios-simulator/-/appium-ios-simulator-3.29.0.tgz", + "integrity": "sha512-7Y6KJJI3K4V35xbJeEwyedPg0DACYmv7cyyyoMbyz7PcLmoaKcufziNYwhlIiiKJIlPc+U/MJfeWUBSQ6W5/Vg==", + "engines": [ + "node" + ], + "optional": true, + "dependencies": { + "@babel/runtime": "^7.0.0", + "@xmldom/xmldom": "^0.x", + "appium-support": "^2.44.0", + "appium-xcode": "^3.1.0", + "async-lock": "^1.0.0", + "asyncbox": "^2.3.1", + "bluebird": "^3.5.1", + "lodash": "^4.2.1", + "node-simctl": "^6.6.0", + "semver": "^7.0.0", + "source-map-support": "^0.5.3", + "teen_process": "^1.3.0" + } + }, + "node_modules/appium-ios-simulator/node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "optional": true + }, + "node_modules/appium-remote-debugger": { + "version": "8.13.2", + "resolved": "https://registry.npmjs.org/appium-remote-debugger/-/appium-remote-debugger-8.13.2.tgz", + "integrity": "sha512-ke418dyTWUYt9tsMnrhq0DpDfsVUofmPR0lkq3OWY3Jov46Myype2Hl8PwjJFMBn9wFcxNv1sghcLhoWYenSzA==", + "engines": [ + "node" + ], + "optional": true, + "dependencies": { + "@babel/runtime": "^7.0.0", + "appium-base-driver": "^7.0.0", + "appium-ios-device": "^1.7.0", + "appium-support": "^2.41.0", + "async-lock": "^1.2.2", + "asyncbox": "^2.6.0", + "bluebird": "^3.4.7", + "lodash": "^4.17.11", + "source-map-support": "^0.5.5" + } + }, + "node_modules/appium-remote-debugger/node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "optional": true + }, + "node_modules/appium-support": { + "version": "2.55.0", + "resolved": "https://registry.npmjs.org/appium-support/-/appium-support-2.55.0.tgz", + "integrity": "sha512-QA8lcJr9fagZ9votEGRInjGxHAIzqYr2WVo7WOe0zYwIVZNK5r2oBTHwjwtRm6mXKf3q5z3A6/hdiYeNBlaBpA==", + "engines": [ + "node" + ], + "optional": true, + "dependencies": { + "@babel/runtime": "^7.0.0", + "archiver": "^5.0.0", + "axios": "^0.x", + "base64-stream": "^1.0.0", + "bluebird": "^3.5.1", + "bplist-creator": "^0", + "bplist-parser": "^0.x", + "form-data": "^4.0.0", + "get-stream": "^6.0.0", + "glob": "^7.1.2", + "jimp": "^0.x", + "jsftp": "^2.1.2", + "klaw": "^3.0.0", + "lockfile": "^1.0.4", + "lodash": "^4.2.1", + "mkdirp": "^1.0.0", + "moment": "^2.24.0", + "mv": "^2.1.1", + "ncp": "^2.0.0", + "npmlog": "^6.0.0", + "plist": "^3.0.1", + "pluralize": "^8.0.0", + "pngjs": "^6.0.0", + "rimraf": "^3.0.0", + "sanitize-filename": "^1.6.1", + "semver": "^7.0.0", + "shell-quote": "^1.7.2", + "source-map-support": "^0.5.5", + "teen_process": "^1.5.1", + "uuid": "^8.0.0", + "which": "^2.0.0", + "yauzl": "^2.7.0" + } + }, + "node_modules/appium-support/node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "optional": true + }, + "node_modules/appium-webdriveragent": { + "version": "3.17.0", + "resolved": "https://registry.npmjs.org/appium-webdriveragent/-/appium-webdriveragent-3.17.0.tgz", + "integrity": "sha512-gHtx1uPCQlJn4k2cnMyyOPf4dCaNy0eFt6Oc1bPXkxrYm0X04ZVLFUSIdqBL9n5YKv8Igg3/mkqt4TzWyZxrFw==", + "optional": true, + "dependencies": { + "@babel/runtime": "^7.0.0", + "appium-base-driver": "^7.0.0", + "appium-ios-simulator": "^3.14.0", + "appium-support": "^2.46.0", + "async-lock": "^1.0.0", + "asyncbox": "^2.5.3", + "axios": "^0.x", + "bluebird": "^3.5.5", + "lodash": "^4.17.11", + "node-simctl": "^6.0.2", + "source-map-support": "^0.5.12", + "teen_process": "^1.14.1" + }, + "bin": { + "appium-wda-bootstrap": "build/index.js" + } + }, + "node_modules/appium-webdriveragent/node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "optional": true + }, + "node_modules/appium-xcode": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/appium-xcode/-/appium-xcode-3.11.0.tgz", + "integrity": "sha512-kT1du+HYBrd8X+cLfpp0mLCbqv7tgPRI9lCmrSeL2t9cIGdb+p9m4MOlfaKxrsqFurQlmMVUHmKDZLU4kvZWBg==", + "engines": [ + "node" + ], + "optional": true, + "dependencies": { + "@babel/runtime": "^7.0.0", + "appium-support": "^2.4.0", + "asyncbox": "^2.3.0", + "lodash": "^4.17.4", + "plist": "^3.0.1", + "semver": "^7.0.0", + "source-map-support": "^0.5.5", + "teen_process": "^1.3.0" + } + }, + "node_modules/appium-xcuitest-driver": { + "version": "3.62.0", + "resolved": "https://registry.npmjs.org/appium-xcuitest-driver/-/appium-xcuitest-driver-3.62.0.tgz", + "integrity": "sha512-ABF1bIZfLA3ufO84LJlWeNVMHnooBuVh02scGIyEJwEqN5LDDqcJmg33CRlQTTQMu3jbEII6hiwd1VUAdDf+4w==", + "engines": [ + "node" + ], + "optional": true, + "dependencies": { + "@babel/runtime": "^7.0.0", + "@xmldom/xmldom": "^0.x", + "appium-base-driver": "^7.0.0", + "appium-idb": "^0.x", + "appium-ios-device": "^1.8.0", + "appium-ios-simulator": "^3.28.0", + "appium-remote-debugger": "^8.13.2", + "appium-support": "^2.47.1", + "appium-webdriveragent": "^3.16.0", + "appium-xcode": "^3.8.0", + "async-lock": "^1.0.0", + "asyncbox": "^2.3.1", + "bluebird": "^3.5.1", + "css-selector-parser": "^1.4.1", + "js2xmlparser2": "^0.x", + "lodash": "^4.17.10", + "lru-cache": "^6.0.0", + "moment": "^2.24.0", + "moment-timezone": "^0.x", + "node-simctl": "^6.4.0", + "portscanner": "2.2.0", + "semver": "^7.0.0", + "source-map-support": "^0.x", + "teen_process": "^1.14.0", + "ws": "^8.0.0" + } + }, + "node_modules/appium-xcuitest-driver/node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "optional": true + }, + "node_modules/aproba": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", + "optional": true + }, + "node_modules/archiver": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.2.tgz", + "integrity": "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==", + "optional": true, + "dependencies": { + "archiver-utils": "^2.1.0", + "async": "^3.2.4", + "buffer-crc32": "^0.2.1", + "readable-stream": "^3.6.0", + "readdir-glob": "^1.1.2", + "tar-stream": "^2.2.0", + "zip-stream": "^4.1.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/archiver-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz", + "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==", + "optional": true, + "dependencies": { + "glob": "^7.1.4", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^2.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/archiver-utils/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "optional": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/archiver-utils/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "optional": true + }, + "node_modules/archiver-utils/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "optional": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/are-we-there-yet": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", + "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", + "optional": true, + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "devOptional": true + }, + "node_modules/array-differ": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-differ/-/array-differ-1.0.0.tgz", + "integrity": "sha512-LeZY+DZDRnvP7eMuQ6LHfCzUGxAAIViUBliK24P3hWXL6y4SortgR6Nim6xrkfSLlmH0+k+9NYNwVC2s53ZrYQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/array-uniq": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", + "integrity": "sha512-MNha4BWQ6JbwhFhj03YK552f7cb3AzoE8SzeljgChvL1dl3IcvggXVz1DilzySZkCja+CXuZbdW7yATchWn8/Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true + }, + "node_modules/ast-types": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "optional": true, + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/async": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", + "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==", + "optional": true + }, + "node_modules/async-lock": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/async-lock/-/async-lock-1.4.1.tgz", + "integrity": "sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ==", + "optional": true + }, + "node_modules/asyncbox": { + "version": "2.9.4", + "resolved": "https://registry.npmjs.org/asyncbox/-/asyncbox-2.9.4.tgz", + "integrity": "sha512-TCuA73K6Gvn+5tFGsWf4jc+PsR9RmYXw/AF0mv+CRB3VhHLjqHh/w9gPvYILnV0RcRFfjADHtzZexpxWlsP3Tg==", + "optional": true, + "dependencies": { + "@babel/runtime": "^7.0.0", + "@types/bluebird": "^3.5.37", + "bluebird": "^3.5.1", + "lodash": "^4.17.4", + "source-map-support": "^0.5.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/asyncbox/node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "optional": true + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "optional": true + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "optional": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/atob": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", + "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", + "optional": true, + "bin": { + "atob": "bin/atob.js" + }, + "engines": { + "node": ">= 4.5.0" + } + }, + "node_modules/axios": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.28.0.tgz", + "integrity": "sha512-Tu7NYoGY4Yoc7I+Npf9HhUMtEEpV7ZiLH9yndTCoNhcpBH0kwcvFbzYN9/u5QKI5A6uefjsNNWaz5olJVYS62Q==", + "optional": true, + "dependencies": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/b4a": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.6.tgz", + "integrity": "sha512-5Tk1HLk6b6ctmjIkAcU/Ujv/1WqiDl0F0JdRCR80VsOcUlHcu7pWeWRlOqQLHfDEsVx9YH/aif5AG4ehoCtTmg==", + "optional": true + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "devOptional": true + }, + "node_modules/bare-events": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.4.2.tgz", + "integrity": "sha512-qMKFd2qG/36aA4GwvKq8MxnPgCQAmBWmSyLWsJcbn8v03wvIPQ/hG1Ms8bPzndZxMDoHpxez5VOS+gC9Yi24/Q==", + "optional": true + }, + "node_modules/bare-fs": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-2.3.1.tgz", + "integrity": "sha512-W/Hfxc/6VehXlsgFtbB5B4xFcsCl+pAh30cYhoFyXErf6oGrwjh8SwiPAdHgpmWonKuYpZgGywN0SXt7dgsADA==", + "optional": true, + "dependencies": { + "bare-events": "^2.0.0", + "bare-path": "^2.0.0", + "bare-stream": "^2.0.0" + } + }, + "node_modules/bare-os": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-2.4.0.tgz", + "integrity": "sha512-v8DTT08AS/G0F9xrhyLtepoo9EJBJ85FRSMbu1pQUlAf6A8T0tEEQGMVObWeqpjhSPXsE0VGlluFBJu2fdoTNg==", + "optional": true + }, + "node_modules/bare-path": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-2.1.3.tgz", + "integrity": "sha512-lh/eITfU8hrj9Ru5quUp0Io1kJWIk1bTjzo7JH1P5dWmQ2EL4hFUlfI8FonAhSlgIfhn63p84CDY/x+PisgcXA==", + "optional": true, + "dependencies": { + "bare-os": "^2.1.0" + } + }, + "node_modules/bare-stream": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.1.3.tgz", + "integrity": "sha512-tiDAH9H/kP+tvNO5sczyn9ZAA7utrSMobyDchsnyyXBuUe2FSQWbxhtuHB8jwpHYYevVo2UJpcmvvjrbHboUUQ==", + "optional": true, + "dependencies": { + "streamx": "^2.18.0" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "devOptional": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/base64-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/base64-stream/-/base64-stream-1.0.0.tgz", + "integrity": "sha512-BQQZftaO48FcE1Kof9CmXMFaAdqkcNorgc8CxesZv9nMbbTF1EFyQe89UOuh//QMmdtfUDXyO8rgUalemL5ODA==", + "optional": true + }, + "node_modules/basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "optional": true, + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/basic-auth/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "optional": true + }, + "node_modules/basic-ftp": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz", + "integrity": "sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==", + "optional": true, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/big-integer": { + "version": "1.6.52", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", + "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==", + "optional": true, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "optional": true, + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bl/node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "optional": true, + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/bluebird": { + "version": "2.9.34", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-2.9.34.tgz", + "integrity": "sha512-ZDzCb87X7/IP1uzQ5eJZB+WoQRGTnKL5DHWvPw6kkMbQseouiQIrEi3P1UGE0D1k0N5/+aP/5GMCyHZ1xYJyHQ==" + }, + "node_modules/bmp-js": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/bmp-js/-/bmp-js-0.1.0.tgz", + "integrity": "sha512-vHdS19CnY3hwiNdkaqk93DvjVLfbEcI8mys4UjuWrlX1haDmroo8o4xCzh4wD6DGV6HxRCyauwhHRqMTfERtjw==", + "optional": true + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true + }, + "node_modules/bplist-creator": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/bplist-creator/-/bplist-creator-0.1.1.tgz", + "integrity": "sha512-Ese7052fdWrxp/vqSJkydgx/1MdBnNOCV2XVfbmdGWD2H6EYza+Q4pyYSuVSnCUD22hfI/BFI4jHaC3NLXLlJQ==", + "optional": true, + "dependencies": { + "stream-buffers": "2.2.x" + } + }, + "node_modules/bplist-parser": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.3.2.tgz", + "integrity": "sha512-apC2+fspHGI3mMKj+dGevkGo/tCqVB8jMb6i+OX+E29p0Iposz07fABkRIfVUPNd5A5VbuOz1bZbnmkKLYF+wQ==", + "optional": true, + "dependencies": { + "big-integer": "1.6.x" + }, + "engines": { + "node": ">= 5.10.0" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "devOptional": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz", + "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001587", + "electron-to-chromium": "^1.4.668", + "node-releases": "^2.0.14", + "update-browserslist-db": "^1.0.13" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "optional": true, + "engines": { + "node": "*" + } + }, + "node_modules/buffer-equal": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal/-/buffer-equal-0.0.1.tgz", + "integrity": "sha512-RgSV6InVQ9ODPdLWJ5UAqBqJBOg370Nz6ZQtRzpt6nUjc8v0St97uJ4PYC6NztqIScrAXafKM3mZPMygSe1ggA==", + "optional": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "devOptional": true + }, + "node_modules/bufferpack": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/bufferpack/-/bufferpack-0.0.6.tgz", + "integrity": "sha512-MTWvLHElqczrIVhge9qHtqgNigJFyh0+tCDId5yCbFAfuekHWIG+uAgvoHVflwrDPuY/e47JE1ki5qcM7w4uLg==", + "engines": { + "node": "*" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cacheable-lookup": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", + "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", + "optional": true, + "engines": { + "node": ">=10.6.0" + } + }, + "node_modules/cacheable-request": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", + "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", + "optional": true, + "dependencies": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^6.0.1", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cacheable-request/node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "optional": true, + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "devOptional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/camel-case": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", + "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", + "dev": true, + "dependencies": { + "pascal-case": "^3.1.2", + "tslib": "^2.0.3" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001596", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001596.tgz", + "integrity": "sha512-zpkZ+kEr6We7w63ORkoJ2pOfBwBkY/bJrG/UZ90qNb45Isblu8wzDgevEOrRL1r9dWayHjYiiyCMEXPn4DweGQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/centra": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/centra/-/centra-2.7.0.tgz", + "integrity": "sha512-PbFMgMSrmgx6uxCdm57RUos9Tc3fclMvhLSATYN39XsDV29B89zZ3KA89jmY0vwSGazyU+uerqwa6t+KaodPcg==", + "optional": true, + "dependencies": { + "follow-redirects": "^1.15.6" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "devOptional": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "optional": true + }, + "node_modules/chrome-launcher": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/chrome-launcher/-/chrome-launcher-0.13.4.tgz", + "integrity": "sha512-nnzXiDbGKjDSK6t2I+35OAPBy5Pw/39bgkb/ZAFwMhwJbdYBp6aH+vW28ZgtjdU890Q7D+3wN/tB8N66q5Gi2A==", + "optional": true, + "dependencies": { + "@types/node": "*", + "escape-string-regexp": "^1.0.5", + "is-wsl": "^2.2.0", + "lighthouse-logger": "^1.0.0", + "mkdirp": "^0.5.3", + "rimraf": "^3.0.2" + } + }, + "node_modules/chrome-launcher/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "optional": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/chrome-launcher/node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "optional": true, + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", + "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", + "dev": true, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/chromium-bidi": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.6.4.tgz", + "integrity": "sha512-8zoq6ogmhQQkAKZVKO2ObFTl4uOkqoX1PlKQX3hZQ5E9cbUotcAb7h4pTNVAGGv8Z36PF3CtdOriEp/Rz82JqQ==", + "optional": true, + "dependencies": { + "mitt": "3.0.1", + "urlpattern-polyfill": "10.0.0", + "zod": "3.23.8" + }, + "peerDependencies": { + "devtools-protocol": "*" + } + }, + "node_modules/clean-css": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz", + "integrity": "sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==", + "dev": true, + "dependencies": { + "source-map": "~0.6.0" + }, + "engines": { + "node": ">= 10.0" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "optional": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dev": true, + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/clone-response": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", + "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", + "optional": true, + "dependencies": { + "mimic-response": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "devOptional": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "devOptional": true + }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "optional": true, + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "optional": true, + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "engines": { + "node": "^12.20.0 || >=14" + } + }, + "node_modules/compress-commons": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.2.tgz", + "integrity": "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==", + "optional": true, + "dependencies": { + "buffer-crc32": "^0.2.13", + "crc32-stream": "^4.0.2", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "devOptional": true + }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "optional": true + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "optional": true + }, + "node_modules/cosmiconfig": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", + "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", + "optional": true, + "dependencies": { + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "optional": true, + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/crc32-stream": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.3.tgz", + "integrity": "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==", + "optional": true, + "dependencies": { + "crc-32": "^1.2.0", + "readable-stream": "^3.4.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, + "node_modules/cross-env": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", + "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "bin": { + "cross-env": "src/bin/cross-env.js", + "cross-env-shell": "src/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=10.14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-loader": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.10.0.tgz", + "integrity": "sha512-LTSA/jWbwdMlk+rhmElbDR2vbtQoTBPr7fkJE+mxrHj+7ru0hUmHafDRzWIjIHTwpitWVaqY2/UWGRca3yUgRw==", + "dev": true, + "dependencies": { + "icss-utils": "^5.1.0", + "postcss": "^8.4.33", + "postcss-modules-extract-imports": "^3.0.0", + "postcss-modules-local-by-default": "^4.0.4", + "postcss-modules-scope": "^3.1.1", + "postcss-modules-values": "^4.0.0", + "postcss-value-parser": "^4.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/css-select": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", + "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.0.1", + "domhandler": "^4.3.1", + "domutils": "^2.8.0", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-selector-parser": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/css-selector-parser/-/css-selector-parser-1.4.1.tgz", + "integrity": "sha512-HYPSb7y/Z7BNDCOrakL4raGO2zltZkbeXyAd6Tg9obzix6QhzxCotdBl6VT0Dv4vZfJGVz3WL/xaEI9Ly3ul0g==", + "optional": true + }, + "node_modules/css-shorthand-properties": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/css-shorthand-properties/-/css-shorthand-properties-1.1.1.tgz", + "integrity": "sha512-Md+Juc7M3uOdbAFwOYlTrccIZ7oCFuzrhKYQjdeUEW/sE1hv17Jp/Bws+ReOPpGVBTYCBoYo+G17V5Qo8QQ75A==", + "optional": true + }, + "node_modules/css-value": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/css-value/-/css-value-0.0.1.tgz", + "integrity": "sha512-FUV3xaJ63buRLgHrLQVlVgQnQdR4yqdLGaDu7g8CQcWjInDfM9plBTPI9FRfpahju1UBSaMckeb2/46ApS/V1Q==", + "optional": true + }, + "node_modules/css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "dev": true, + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/data-uri-to-buffer": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", + "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", + "optional": true, + "engines": { + "node": ">= 14" + } + }, + "node_modules/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "optional": true, + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decompress-response/node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "optional": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/degenerator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", + "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", + "optional": true, + "dependencies": { + "ast-types": "^0.13.4", + "escodegen": "^2.1.0", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "optional": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "optional": true + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/devtools": { + "version": "6.12.1", + "resolved": "https://registry.npmjs.org/devtools/-/devtools-6.12.1.tgz", + "integrity": "sha512-JyG46suEiZmld7/UVeogkCWM0zYGt+2ML/TI+SkEp+bTv9cs46cDb0pKF3glYZJA7wVVL2gC07Ic0iCxyJEnCQ==", + "optional": true, + "dependencies": { + "@wdio/config": "6.12.1", + "@wdio/logger": "6.10.10", + "@wdio/protocols": "6.12.0", + "@wdio/utils": "6.11.0", + "chrome-launcher": "^0.13.1", + "edge-paths": "^2.1.0", + "puppeteer-core": "^5.1.0", + "ua-parser-js": "^0.7.21", + "uuid": "^8.0.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/devtools-protocol": { + "version": "0.0.818844", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.818844.tgz", + "integrity": "sha512-AD1hi7iVJ8OD0aMLQU5VK0XH9LDlA1+BcPIgrAxPfaibx2DbWucuyOhc4oyQCbnvDDO68nN6/LcKfqTP343Jjg==", + "optional": true + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dom-converter": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz", + "integrity": "sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==", + "dev": true, + "dependencies": { + "utila": "~0.4" + } + }, + "node_modules/dom-serializer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", + "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", + "dev": true, + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/dom-walk": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.2.tgz", + "integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==", + "optional": true + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ] + }, + "node_modules/domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "dev": true, + "dependencies": { + "domelementtype": "^2.2.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "dev": true, + "dependencies": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dot-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", + "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", + "dev": true, + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/duplexer": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", + "optional": true + }, + "node_modules/edge-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/edge-paths/-/edge-paths-2.2.1.tgz", + "integrity": "sha512-AI5fC7dfDmCdKo3m5y7PkYE8m6bMqR6pvVpgtrZkkhcJXFLelUgkjrhk3kXXx8Kbw2cRaTT4LkOR7hqf39KJdw==", + "optional": true, + "dependencies": { + "@types/which": "^1.3.2", + "which": "^2.0.2" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "node_modules/electron-to-chromium": { + "version": "1.4.698", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.698.tgz", + "integrity": "sha512-f9iZD1t3CLy1AS6vzM5EKGa6p9pRcOeEFXRFbaG2Ta+Oe7MkfRQ3fsvPYidzHe1h4i0JvIvpcY55C+B6BZNGtQ==", + "dev": true + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "optional": true + }, + "node_modules/emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "optional": true, + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.17.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz", + "integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "dev": true, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/envinfo": { + "version": "7.11.1", + "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.11.1.tgz", + "integrity": "sha512-8PiZgZNIB4q/Lw4AhOvAfB/ityHAd2bli3lESSWmWSzSsl5dKpy5N1d1Rfkd2teq/g9xN90lc6o98DOjMeYHpg==", + "dev": true, + "bin": { + "envinfo": "dist/cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/errno": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", + "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", + "dev": true, + "dependencies": { + "prr": "~1.0.1" + }, + "bin": { + "errno": "cli.js" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "optional": true, + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.4.1.tgz", + "integrity": "sha512-cXLGjP0c4T3flZJKQSuziYoq7MlT+rnvfZjfp7h+I7K9BNX54kP9nyWvdbwjQ4u1iWbOL4u96fgeZLToQlZC7w==", + "dev": true + }, + "node_modules/es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "optional": true + }, + "node_modules/escalade": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "devOptional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "optional": true, + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/escodegen/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "optional": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/eslint": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", + "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.0", + "@humanwhocodes/config-array": "^0.11.14", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-prettier": { + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.10.0.tgz", + "integrity": "sha512-SM8AMJdeQqRYT9O9zguiruQZaN7+z+E4eAP9oiLNGKMtomwaB1E9dcgUD6ZAn/eQAb52USbvezbiljfZUhbJcg==", + "dev": true, + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-4.2.1.tgz", + "integrity": "sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ==", + "dev": true, + "dependencies": { + "prettier-linter-helpers": "^1.0.0" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "eslint": ">=7.28.0", + "prettier": ">=2.0.0" + }, + "peerDependenciesMeta": { + "eslint-config-prettier": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-progress": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-progress/-/eslint-plugin-progress-0.0.1.tgz", + "integrity": "sha512-MAa+Nbw3uAHYKrt5ML2asiXCHdJ4ticANZC/KlfGO5Rck9oB+KhAXc+Zj4bLohci+EAE/3LbbcWyYm3kQuwJiQ==", + "dev": true + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "optional": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esquery/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "devOptional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/exif-parser": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/exif-parser/-/exif-parser-0.1.12.tgz", + "integrity": "sha512-c2bQfLNbMzLPmzQuOr8fy0csy84WmwnER81W88DzTp9CYNPJ6yzOj2EZAh9pywYpqHnshVLHQJ8WzldAyfY+Iw==", + "optional": true + }, + "node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "optional": true, + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/extract-zip/node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "optional": true, + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true + }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "optional": true + }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/fastest-levenshtein": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", + "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", + "dev": true, + "engines": { + "node": ">= 4.9.1" + } + }, + "node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "optional": true, + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/file-loader": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz", + "integrity": "sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==", + "dev": true, + "dependencies": { + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/file-type": { + "version": "16.5.4", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-16.5.4.tgz", + "integrity": "sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw==", + "optional": true, + "dependencies": { + "readable-web-to-node-stream": "^3.0.0", + "strtok3": "^6.2.4", + "token-types": "^4.1.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "bin": { + "flat": "cli.js" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", + "dev": true + }, + "node_modules/follow-redirects": { + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "optional": true, + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "optional": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "optional": true + }, + "node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "optional": true, + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "devOptional": true + }, + "node_modules/ftp-response-parser": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ftp-response-parser/-/ftp-response-parser-1.0.1.tgz", + "integrity": "sha512-++Ahlo2hs/IC7UVQzjcSAfeUpCwTTzs4uvG5XfGnsinIFkWUYF4xWwPd5qZuK8MJrmUIxFMuHcfqaosCDjvIWw==", + "optional": true, + "dependencies": { + "readable-stream": "^1.0.31" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/ftp-response-parser/node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", + "optional": true + }, + "node_modules/ftp-response-parser/node_modules/readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ==", + "optional": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "node_modules/ftp-response-parser/node_modules/string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==", + "optional": true + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gauge": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", + "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", + "optional": true, + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.3", + "console-control-strings": "^1.1.0", + "has-unicode": "^2.0.1", + "signal-exit": "^3.0.7", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "optional": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-port": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/get-port/-/get-port-5.1.1.tgz", + "integrity": "sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==", + "optional": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-uri": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.3.tgz", + "integrity": "sha512-BzUrJBS9EcUb4cFol8r4W3v1cPsSyajLSthNkz5BxbpDcHN5tIrM10E2eNvfnvBn3DaT3DUgx0OpsBKkaOpanw==", + "optional": true, + "dependencies": { + "basic-ftp": "^5.0.2", + "data-uri-to-buffer": "^6.0.2", + "debug": "^4.3.4", + "fs-extra": "^11.2.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/get-uri/node_modules/fs-extra": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", + "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", + "optional": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/gifwrap": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/gifwrap/-/gifwrap-0.10.1.tgz", + "integrity": "sha512-2760b1vpJHNmLzZ/ubTtNnEx5WApN/PYWJvXvgS+tL1egTTthayFYIQQNi136FLEDcN/IyEY2EcGpIITD6eYUw==", + "optional": true, + "dependencies": { + "image-q": "^4.0.0", + "omggif": "^1.0.10" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "devOptional": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true + }, + "node_modules/global": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/global/-/global-4.4.0.tgz", + "integrity": "sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==", + "optional": true, + "dependencies": { + "min-document": "^2.19.0", + "process": "^0.11.10" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/got": { + "version": "11.8.6", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", + "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", + "optional": true, + "dependencies": { + "@sindresorhus/is": "^4.0.0", + "@szmarczak/http-timer": "^4.0.5", + "@types/cacheable-request": "^6.0.1", + "@types/responselike": "^1.0.0", + "cacheable-lookup": "^5.0.3", + "cacheable-request": "^7.0.2", + "decompress-response": "^6.0.0", + "http2-wrapper": "^1.0.0-beta.5.2", + "lowercase-keys": "^2.0.0", + "p-cancelable": "^2.0.0", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=10.19.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "devOptional": true + }, + "node_modules/grapheme-splitter": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", + "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", + "optional": true + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, + "node_modules/h264-converter": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/h264-converter/-/h264-converter-0.1.4.tgz", + "integrity": "sha512-+rBCqYTyRuJYBjpbN/T9tShvhMsITrYeEKnLrhFmGaqCEjsMT/xT9y2nUsLYfq0X4thuP+2vkCJAQZZIqRYMnQ==", + "dev": true, + "dependencies": { + "tslib": "^1.14.1" + } + }, + "node_modules/h264-converter/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "optional": true + }, + "node_modules/hasown": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.1.tgz", + "integrity": "sha512-1/th4MHjnwncwXsIW6QMzlvYL9kG5e/CpVvLRZe4XPa8TOUNbCELqmvhDmnkNsAjwaG4+I8gJJL0JBvTTLO9qA==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "bin": { + "he": "bin/he" + } + }, + "node_modules/html-minifier-terser": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", + "integrity": "sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw==", + "dev": true, + "dependencies": { + "camel-case": "^4.1.2", + "clean-css": "^5.2.2", + "commander": "^8.3.0", + "he": "^1.2.0", + "param-case": "^3.0.4", + "relateurl": "^0.2.7", + "terser": "^5.10.0" + }, + "bin": { + "html-minifier-terser": "cli.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/html-minifier-terser/node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "dev": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/html-webpack-plugin": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.6.0.tgz", + "integrity": "sha512-iwaY4wzbe48AfKLZ/Cc8k0L+FKG6oSNRaZ8x5A/T/IVDGyXcbHncM9TdDa93wn0FsSm82FhTKW7f3vS61thXAw==", + "dev": true, + "dependencies": { + "@types/html-minifier-terser": "^6.0.0", + "html-minifier-terser": "^6.0.2", + "lodash": "^4.17.21", + "pretty-error": "^4.0.0", + "tapable": "^2.0.0" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/html-webpack-plugin" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "webpack": "^5.20.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/htmlparser2": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", + "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.0.0", + "domutils": "^2.5.2", + "entities": "^2.0.0" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", + "optional": true + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "optional": true, + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/http-status-codes": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/http-status-codes/-/http-status-codes-2.3.0.tgz", + "integrity": "sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA==", + "optional": true + }, + "node_modules/http2-wrapper": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", + "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", + "optional": true, + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", + "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", + "optional": true, + "dependencies": { + "agent-base": "^7.0.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/icss-utils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", + "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", + "dev": true, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "devOptional": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/ifdef-loader": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/ifdef-loader/-/ifdef-loader-2.3.2.tgz", + "integrity": "sha512-kH9bHPrfIFxLpq3XEruJqSlHXch2nOljKIDRS/6MU5LDZTyHeaSWVf04wNYX+8RT+NDmeS8Vm5HwZ7akkXo8ig==", + "dev": true, + "dependencies": { + "loader-utils": "^1.1.0" + } + }, + "node_modules/ifdef-loader/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/ifdef-loader/node_modules/loader-utils": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz", + "integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==", + "dev": true, + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^1.0.1" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/ignore": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", + "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/image-q": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/image-q/-/image-q-4.0.0.tgz", + "integrity": "sha512-PfJGVgIfKQJuq3s0tTDOKtztksibuUEbJQIYT3by6wctQo+Rdlh7ef4evJ5NCdxY4CfMbvFkocEwbl4BF8RlJw==", + "optional": true, + "dependencies": { + "@types/node": "16.9.1" + } + }, + "node_modules/image-q/node_modules/@types/node": { + "version": "16.9.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.9.1.tgz", + "integrity": "sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g==", + "optional": true + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "devOptional": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-local": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", + "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", + "dev": true, + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "devOptional": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/interpret": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-2.2.0.tgz", + "integrity": "sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/ios-device-lib": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/ios-device-lib/-/ios-device-lib-0.9.3.tgz", + "integrity": "sha512-LwkC7O6S0XwslkHLtEcNLwNdBuDq2Aeit60ZO+mrOyMUKPpCJHZs2decTJdsx5uIKwLZkxd5mmcUdxlMAwsiLQ==", + "dependencies": { + "bufferpack": "0.0.6", + "uuid": "8.3.2" + } + }, + "node_modules/ip-address": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", + "optional": true, + "dependencies": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "optional": true + }, + "node_modules/is-core-module": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", + "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "dev": true, + "dependencies": { + "hasown": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "optional": true, + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-function": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-function/-/is-function-1.0.2.tgz", + "integrity": "sha512-lw7DUp0aWXYg+CBCN+JKkcE0Q2RayZnSvnZBlwgxHBQhqt5pZNVy4Ri7H9GmmXkdu7LUthszM+Tor1u/2iBcpQ==", + "optional": true + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-like": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/is-number-like/-/is-number-like-1.0.8.tgz", + "integrity": "sha512-6rZi3ezCyFcn5L71ywzz2bS5b2Igl1En3eTlZlvKjpz1n3IZLAYMbKYAIQgFmEu0GENg92ziU/faEOA/aixjbA==", + "optional": true, + "dependencies": { + "lodash.isfinite": "^3.3.2" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "optional": true, + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "optional": true + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "devOptional": true + }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isomorphic-fetch": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz", + "integrity": "sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==", + "optional": true, + "dependencies": { + "node-fetch": "^2.6.1", + "whatwg-fetch": "^3.4.1" + } + }, + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/jimp": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/jimp/-/jimp-0.22.12.tgz", + "integrity": "sha512-R5jZaYDnfkxKJy1dwLpj/7cvyjxiclxU3F4TrI/J4j2rS0niq6YDUMoPn5hs8GDpO+OZGo7Ky057CRtWesyhfg==", + "optional": true, + "dependencies": { + "@jimp/custom": "^0.22.12", + "@jimp/plugins": "^0.22.12", + "@jimp/types": "^0.22.12", + "regenerator-runtime": "^0.13.3" + } + }, + "node_modules/jimp/node_modules/regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "optional": true + }, + "node_modules/jpeg-js": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.4.tgz", + "integrity": "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==", + "optional": true + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "optional": true + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "devOptional": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/js2xmlparser2": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/js2xmlparser2/-/js2xmlparser2-0.2.0.tgz", + "integrity": "sha512-SzFGc1hQqzpDcalKmrM5gobSMGRSRg2lgaZrHGIfowrmd8+uaI+PWW62jcCGIqI+b4wdyYK0VKMhvVtJfkD0cg==", + "optional": true + }, + "node_modules/jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", + "optional": true + }, + "node_modules/jsftp": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/jsftp/-/jsftp-2.1.3.tgz", + "integrity": "sha512-r79EVB8jaNAZbq8hvanL8e8JGu2ZNr2bXdHC4ZdQhRImpSPpnWwm5DYVzQ5QxJmtGtKhNNuvqGgbNaFl604fEQ==", + "optional": true, + "dependencies": { + "debug": "^3.1.0", + "ftp-response-parser": "^1.0.1", + "once": "^1.4.0", + "parse-listing": "^1.1.3", + "stream-combiner": "^0.2.2", + "unorm": "^1.4.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsftp/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "optional": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "devOptional": true + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "devOptional": true + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "optional": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/junk": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/junk/-/junk-1.0.3.tgz", + "integrity": "sha512-3KF80UaaSSxo8jVnRYtMKNGFOoVPBdkkVPsw+Ad0y4oxKXPduS6G6iHkrf69yJVff/VAaYXkV42rtZ7daJxU3w==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "devOptional": true, + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/klaw": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/klaw/-/klaw-3.0.0.tgz", + "integrity": "sha512-0Fo5oir+O9jnXu5EefYbVK+mHMBeEVEy2cmctR1O1NECcCkPRreJKrS6Qt/j3KC2C148Dfo9i3pCmCMsdqGr0g==", + "optional": true, + "dependencies": { + "graceful-fs": "^4.1.9" + } + }, + "node_modules/lazystream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "optional": true, + "dependencies": { + "readable-stream": "^2.0.5" + }, + "engines": { + "node": ">= 0.6.3" + } + }, + "node_modules/lazystream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "optional": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/lazystream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "optional": true + }, + "node_modules/lazystream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "optional": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lighthouse-logger": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/lighthouse-logger/-/lighthouse-logger-1.4.2.tgz", + "integrity": "sha512-gPWxznF6TKmUHrOQjlVo2UbaL2EJ71mb2CCeRs/2qBpi4L/g4LUVc9+3lKQ6DTUZwJswfM7ainGrLO1+fOqa2g==", + "optional": true, + "dependencies": { + "debug": "^2.6.9", + "marky": "^1.2.2" + } + }, + "node_modules/lighthouse-logger/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "optional": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/lighthouse-logger/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "optional": true + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "optional": true + }, + "node_modules/load-bmfont": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/load-bmfont/-/load-bmfont-1.4.2.tgz", + "integrity": "sha512-qElWkmjW9Oq1F9EI5Gt7aD9zcdHb9spJCW1L/dmPf7KzCCEJxq8nhHz5eCgI9aMf7vrG/wyaCqdsI+Iy9ZTlog==", + "optional": true, + "dependencies": { + "buffer-equal": "0.0.1", + "mime": "^1.3.4", + "parse-bmfont-ascii": "^1.0.3", + "parse-bmfont-binary": "^1.0.5", + "parse-bmfont-xml": "^1.1.4", + "phin": "^3.7.1", + "xhr": "^2.0.1", + "xtend": "^4.0.0" + } + }, + "node_modules/loader-runner": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", + "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "dev": true, + "engines": { + "node": ">=6.11.5" + } + }, + "node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dev": true, + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lockfile": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/lockfile/-/lockfile-1.0.4.tgz", + "integrity": "sha512-cvbTwETRfsFh4nHsL1eGWapU1XFi5Ot9E85sWAwia7Y7EgB7vfqcZhTKZ+l7hCGxSPoushMv5GKhT5PdLv03WA==", + "optional": true, + "dependencies": { + "signal-exit": "^3.0.2" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "node_modules/lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==", + "optional": true + }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", + "optional": true + }, + "node_modules/lodash.difference": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", + "integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==", + "optional": true + }, + "node_modules/lodash.flatten": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", + "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==", + "optional": true + }, + "node_modules/lodash.isfinite": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/lodash.isfinite/-/lodash.isfinite-3.3.2.tgz", + "integrity": "sha512-7FGG40uhC8Mm633uKW1r58aElFlBlxCrg9JfSi3P6aYiWmfiWF0PgMd86ZUsxE5GwWPdHoS2+48bwTh2VPkIQA==", + "optional": true + }, + "node_modules/lodash.isobject": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/lodash.isobject/-/lodash.isobject-3.0.2.tgz", + "integrity": "sha512-3/Qptq2vr7WeJbB4KHUSKlq8Pl7ASXi3UG6CMbBm8WRtXi8+GHm7mKaU3urfpSEzWe2wCIChs6/sdocUsTKJiA==", + "optional": true + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "optional": true + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "devOptional": true + }, + "node_modules/lodash.union": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", + "integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==", + "optional": true + }, + "node_modules/lodash.zip": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.zip/-/lodash.zip-4.2.0.tgz", + "integrity": "sha512-C7IOaBBK/0gMORRBd8OETNx3kmOkgIWIPvyDpZSCTwUrpYmgZwJkjZeOD8ww4xbOUOs4/attY+pciKvadNfFbg==", + "optional": true + }, + "node_modules/loglevel": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.9.1.tgz", + "integrity": "sha512-hP3I3kCrDIMuRwAwHltphhDM1r8i55H33GgqjXbrisuJhF4kRhW1dNuxsRklp4bXl8DSdLaNLuiL4A/LWRfxvg==", + "optional": true, + "engines": { + "node": ">= 0.6.0" + }, + "funding": { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/loglevel" + } + }, + "node_modules/loglevel-plugin-prefix": { + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/loglevel-plugin-prefix/-/loglevel-plugin-prefix-0.8.4.tgz", + "integrity": "sha512-WpG9CcFAOjz/FtNht+QJeGpvVl/cdR6P0z6OcXSkr8wFJOsV2GRj2j10JLfjuA4aYkcKCNIEqRGCyTife9R8/g==", + "optional": true + }, + "node_modules/lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "dev": true, + "dependencies": { + "tslib": "^2.0.3" + } + }, + "node_modules/lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, + "node_modules/marky": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/marky/-/marky-1.2.5.tgz", + "integrity": "sha512-q9JtQJKjpsVxCRVgQ+WapguSbKC3SQ5HEzFGPAJMStgh3QjCawp00UKv3MTTAArTmGmmPUvllHZoNbZ3gs0I+Q==", + "optional": true + }, + "node_modules/maximatch": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/maximatch/-/maximatch-0.1.0.tgz", + "integrity": "sha512-9ORVtDUFk4u/NFfo0vG/ND/z7UQCVZBL539YW0+U1I7H1BkZwizcPx5foFv7LCPcBnm2U6RjFnQOsIvN4/Vm2A==", + "dev": true, + "dependencies": { + "array-differ": "^1.0.0", + "array-union": "^1.0.1", + "arrify": "^1.0.0", + "minimatch": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/maximatch/node_modules/array-union": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", + "integrity": "sha512-Dxr6QJj/RdU/hCaBjOfxW+q6lyuVE6JFWIrAUpuOOhoJJoQ99cUn3igRaHVB5P9WrgFVN0FfArM3x0cueOU8ng==", + "dev": true, + "dependencies": { + "array-uniq": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/method-override": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/method-override/-/method-override-3.0.0.tgz", + "integrity": "sha512-IJ2NNN/mSl9w3kzWB92rcdHpz+HjkxhDJWNDBqSlas+zQdP8wBiJzITPg08M/k2uVvMow7Sk41atndNtt/PHSA==", + "optional": true, + "dependencies": { + "debug": "3.1.0", + "methods": "~1.1.2", + "parseurl": "~1.3.2", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/method-override/node_modules/debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "optional": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/method-override/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "optional": true + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "optional": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/min-document": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.0.tgz", + "integrity": "sha512-9Wy1B3m3f66bPPmU5hdA4DR4PB2OfDU/+GS3yAB7IQozE3tqXaVv2zOjgla7MEGSRv95+ILmOuvhLkOK6wJtCQ==", + "optional": true, + "dependencies": { + "dom-walk": "^0.1.0" + } + }, + "node_modules/mini-css-extract-plugin": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.8.1.tgz", + "integrity": "sha512-/1HDlyFRxWIZPI1ZpgqlZ8jMw/1Dp/dl3P0L1jtZ+zVcHqwPhGwaJwKL00WVgfnBy6PWCde9W65or7IIETImuA==", + "dev": true, + "dependencies": { + "schema-utils": "^4.0.0", + "tapable": "^2.2.1" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/mini-css-extract-plugin/node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/mini-css-extract-plugin/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/mini-css-extract-plugin/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "node_modules/mini-css-extract-plugin/node_modules/schema-utils": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz", + "integrity": "sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "devOptional": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "optional": true + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "devOptional": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "optional": true + }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "optional": true, + "engines": { + "node": "*" + } + }, + "node_modules/moment-timezone": { + "version": "0.5.45", + "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.45.tgz", + "integrity": "sha512-HIWmqA86KcmCAhnMAN0wuDOARV/525R2+lOLotuGFzn4HO+FH+/645z2wx0Dt3iDv6/p61SIvKnDstISainhLQ==", + "optional": true, + "dependencies": { + "moment": "^2.29.4" + }, + "engines": { + "node": "*" + } + }, + "node_modules/morgan": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz", + "integrity": "sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==", + "optional": true, + "dependencies": { + "basic-auth": "~2.0.1", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-finished": "~2.3.0", + "on-headers": "~1.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/morgan/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "optional": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/morgan/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "optional": true + }, + "node_modules/morgan/node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "optional": true, + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/mv": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz", + "integrity": "sha512-at/ZndSy3xEGJ8i0ygALh8ru9qy7gWW1cmkaqBN29JmMlIvM//MEO9y1sk/avxuwnPcfhkejkLsuPxH81BrkSg==", + "optional": true, + "dependencies": { + "mkdirp": "~0.5.1", + "ncp": "~2.0.0", + "rimraf": "~2.4.0" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/mv/node_modules/glob": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz", + "integrity": "sha512-MKZeRNyYZAVVVG1oZeLaWie1uweH40m9AZwIwxyPbTSX4hHrVYSzLg0Ro5Z5R7XKkIX+Cc6oD1rqeDJnwsB8/A==", + "optional": true, + "dependencies": { + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "2 || 3", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + } + }, + "node_modules/mv/node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "optional": true, + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/mv/node_modules/rimraf": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.4.5.tgz", + "integrity": "sha512-J5xnxTyqaiw06JjMftq7L9ouA448dw/E7dKghkP9WpKNuwmARNNg+Gk8/u5ryb9N/Yo2+z3MCwuqFK/+qPOPfQ==", + "optional": true, + "dependencies": { + "glob": "^6.0.1" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/nan": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.19.0.tgz", + "integrity": "sha512-nO1xXxfh/RWNxfd/XPfbIfFk5vgLsAxUR9y5O0cHMJu/AW9U95JLXqthYHjEp+8gQ5p96K9jUp8nbVOxCdRbtw==" + }, + "node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/natural-compare-lite": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz", + "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", + "dev": true + }, + "node_modules/ncp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz", + "integrity": "sha512-zIdGUrPRFTUELUvr3Gmc7KZ2Sw/h1PiVM0Af/oHB6zgnV1ikqSfRk+TOufi79aHYCW3NiOXmr1BP5nWbzojLaA==", + "optional": true, + "bin": { + "ncp": "bin/ncp" + } + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true + }, + "node_modules/netmask": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", + "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==", + "optional": true, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "dev": true, + "dependencies": { + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "optional": true, + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-forge": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "engines": { + "node": ">= 6.13.0" + } + }, + "node_modules/node-mjpeg-proxy": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/node-mjpeg-proxy/-/node-mjpeg-proxy-0.3.2.tgz", + "integrity": "sha512-xZAHAF1LK1DSSgvEQnWJQ3/la+oqaVkEVWqYtGNvte0/Dt8B8trcqjjdJulYliCM+TLdkjwxu+uG4CNmpzZ2Tg==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/node-pty": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/node-pty/-/node-pty-0.10.1.tgz", + "integrity": "sha512-JTdtUS0Im/yRsWJSx7yiW9rtpfmxqxolrtnyKwPLI+6XqTAPW/O2MjS8FYL4I5TsMbH2lVgDb2VMjp+9LoQGNg==", + "hasInstallScript": true, + "dependencies": { + "nan": "^2.14.0" + } + }, + "node_modules/node-releases": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", + "dev": true + }, + "node_modules/node-simctl": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/node-simctl/-/node-simctl-6.6.0.tgz", + "integrity": "sha512-157a3XqQFatcPT8BijH3IQml/GW8qByVjhe04reG86SawyJGfosM3s+qugd1kaar3nsKo+ad6KSS4GB7e9fxig==", + "engines": [ + "node" + ], + "optional": true, + "dependencies": { + "@babel/runtime": "^7.0.0", + "asyncbox": "^2.3.1", + "bluebird": "^3.5.1", + "lodash": "^4.2.1", + "npmlog": "^5.0.0", + "rimraf": "^3.0.0", + "semver": "^7.0.0", + "source-map-support": "^0.5.5", + "teen_process": "^1.5.1", + "uuid": "^8.0.0", + "which": "^2.0.0" + } + }, + "node_modules/node-simctl/node_modules/are-we-there-yet": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "optional": true, + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-simctl/node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "optional": true + }, + "node_modules/node-simctl/node_modules/gauge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "optional": true, + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-simctl/node_modules/npmlog": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "optional": true, + "dependencies": { + "are-we-there-yet": "^2.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^3.0.0", + "set-blocking": "^2.0.0" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npmlog": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", + "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", + "optional": true, + "dependencies": { + "are-we-there-yet": "^3.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^4.0.3", + "set-blocking": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "devOptional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", + "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/omggif": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/omggif/-/omggif-1.0.10.tgz", + "integrity": "sha512-LMJTtvgc/nugXj0Vcrrs68Mn2D1r0zf630VNtqtpI1FEO7e+O9FP4gqs9AcnBaSEeoHIPm28u6qgPR0oyEpGSw==", + "optional": true + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "optional": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "devOptional": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", + "dev": true, + "dependencies": { + "@aashutoshrathi/word-wrap": "^1.2.3", + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-cancelable": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", + "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "devOptional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/pac-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.0.2.tgz", + "integrity": "sha512-BFi3vZnO9X5Qt6NRz7ZOaPja3ic0PhlsmCRYLOpN11+mWBCR6XJDqW5RF3j8jm4WGGQZtBA+bTfxYzeKW73eHg==", + "optional": true, + "dependencies": { + "@tootallnate/quickjs-emscripten": "^0.23.0", + "agent-base": "^7.0.2", + "debug": "^4.3.4", + "get-uri": "^6.0.1", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.5", + "pac-resolver": "^7.0.1", + "socks-proxy-agent": "^8.0.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-resolver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", + "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", + "optional": true, + "dependencies": { + "degenerator": "^5.0.0", + "netmask": "^2.0.2" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "optional": true + }, + "node_modules/param-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", + "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==", + "dev": true, + "dependencies": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "devOptional": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-bmfont-ascii": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/parse-bmfont-ascii/-/parse-bmfont-ascii-1.0.6.tgz", + "integrity": "sha512-U4RrVsUFCleIOBsIGYOMKjn9PavsGOXxbvYGtMOEfnId0SVNsgehXh1DxUdVPLoxd5mvcEtvmKs2Mmf0Mpa1ZA==", + "optional": true + }, + "node_modules/parse-bmfont-binary": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/parse-bmfont-binary/-/parse-bmfont-binary-1.0.6.tgz", + "integrity": "sha512-GxmsRea0wdGdYthjuUeWTMWPqm2+FAd4GI8vCvhgJsFnoGhTrLhXDDupwTo7rXVAgaLIGoVHDZS9p/5XbSqeWA==", + "optional": true + }, + "node_modules/parse-bmfont-xml": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/parse-bmfont-xml/-/parse-bmfont-xml-1.1.6.tgz", + "integrity": "sha512-0cEliVMZEhrFDwMh4SxIyVJpqYoOWDJ9P895tFuS+XuNzI5UBmBk5U5O4KuJdTnZpSBI4LFA2+ZiJaiwfSwlMA==", + "optional": true, + "dependencies": { + "xml-parse-from-string": "^1.0.0", + "xml2js": "^0.5.0" + } + }, + "node_modules/parse-bmfont-xml/node_modules/xml2js": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", + "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", + "optional": true, + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/parse-bmfont-xml/node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "optional": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/parse-headers": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/parse-headers/-/parse-headers-2.0.5.tgz", + "integrity": "sha512-ft3iAoLOB/MlwbNXgzy43SWGP6sQki2jQvAyBg/zDFAgr9bfNWZIUj42Kw2eJIl8kEi4PbgE6U1Zau/HwI75HA==", + "optional": true + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "optional": true, + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-listing": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/parse-listing/-/parse-listing-1.1.3.tgz", + "integrity": "sha512-a1p1i+9Qyc8pJNwdrSvW1g5TPxRH0sywVi6OzVvYHRo6xwF9bDWBxtH0KkxeOOvhUE8vAMtiSfsYQFOuK901eA==", + "optional": true, + "engines": { + "node": ">=0.6.21" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/pascal-case": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", + "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", + "dev": true, + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "devOptional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/peek-readable": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-4.1.0.tgz", + "integrity": "sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg==", + "optional": true, + "engines": { + "node": ">=8" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "optional": true + }, + "node_modules/phin": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/phin/-/phin-3.7.1.tgz", + "integrity": "sha512-GEazpTWwTZaEQ9RhL7Nyz0WwqilbqgLahDM3D0hxWwmVDI52nXEybHqiN6/elwpkJBhcuj+WbBu+QfT0uhPGfQ==", + "optional": true, + "dependencies": { + "centra": "^2.7.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pixelmatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/pixelmatch/-/pixelmatch-4.0.2.tgz", + "integrity": "sha512-J8B6xqiO37sU/gkcMglv6h5Jbd9xNER7aHzpfRdNmV4IbQBzBpe4l9XmbG+xPF/znacgu2jfEw+wHffaq/YkXA==", + "optional": true, + "dependencies": { + "pngjs": "^3.0.0" + }, + "bin": { + "pixelmatch": "bin/pixelmatch" + } + }, + "node_modules/pixelmatch/node_modules/pngjs": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-3.4.0.tgz", + "integrity": "sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==", + "optional": true, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "devOptional": true, + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "devOptional": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "devOptional": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "devOptional": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "devOptional": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/plist": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", + "integrity": "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==", + "optional": true, + "dependencies": { + "@xmldom/xmldom": "^0.8.8", + "base64-js": "^1.5.1", + "xmlbuilder": "^15.1.1" + }, + "engines": { + "node": ">=10.4.0" + } + }, + "node_modules/pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", + "optional": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/pngjs": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-6.0.0.tgz", + "integrity": "sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg==", + "optional": true, + "engines": { + "node": ">=12.13.0" + } + }, + "node_modules/portfinder": { + "version": "1.0.32", + "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.32.tgz", + "integrity": "sha512-on2ZJVVDXRADWE6jnQaX0ioEylzgBpQk8r55NE4wjXW1ZxO+BgDlY6DXwj20i0V8eB4SenDQ00WEaxfiIQPcxg==", + "dependencies": { + "async": "^2.6.4", + "debug": "^3.2.7", + "mkdirp": "^0.5.6" + }, + "engines": { + "node": ">= 0.12.0" + } + }, + "node_modules/portfinder/node_modules/async": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", + "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", + "dependencies": { + "lodash": "^4.17.14" + } + }, + "node_modules/portfinder/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/portfinder/node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/portscanner": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/portscanner/-/portscanner-2.2.0.tgz", + "integrity": "sha512-IFroCz/59Lqa2uBvzK3bKDbDDIEaAY8XJ1jFxcLWTqosrsc32//P4VuSB2vZXoHiHqOmx8B5L5hnKOxL/7FlPw==", + "optional": true, + "dependencies": { + "async": "^2.6.0", + "is-number-like": "^1.0.3" + }, + "engines": { + "node": ">=0.4", + "npm": ">=1.0.0" + } + }, + "node_modules/portscanner/node_modules/async": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", + "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", + "optional": true, + "dependencies": { + "lodash": "^4.17.14" + } + }, + "node_modules/postcss": { + "version": "8.4.35", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.35.tgz", + "integrity": "sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-modules-extract-imports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz", + "integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==", + "dev": true, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-local-by-default": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.4.tgz", + "integrity": "sha512-L4QzMnOdVwRm1Qb8m4x8jsZzKAaPAgrUF1r/hjDR2Xj7R+8Zsf97jAlSQzWtKx5YNiNGN8QxmPFIc/sh+RQl+Q==", + "dev": true, + "dependencies": { + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^6.0.2", + "postcss-value-parser": "^4.1.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-scope": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.1.1.tgz", + "integrity": "sha512-uZgqzdTleelWjzJY+Fhti6F3C9iF1JR/dODLs/JDefozYcKTBCdD8BIl6nNPbTbcLnGrk56hzwZC2DaGNvYjzA==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.4" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-values": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", + "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", + "dev": true, + "dependencies": { + "icss-utils": "^5.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.0.15", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.15.tgz", + "integrity": "sha512-rEYkQOMUCEMhsKbK66tbEU9QVIxbhN18YiniAwA7XQYTVBqrBy+P2p5JcdqsHgKM2zWylp8d7J6eszocfds5Sw==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", + "dev": true, + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/pretty-error": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-4.0.0.tgz", + "integrity": "sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw==", + "dev": true, + "dependencies": { + "lodash": "^4.17.20", + "renderkid": "^3.0.0" + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "optional": true, + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "optional": true + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "optional": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/promise": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", + "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", + "dev": true, + "dependencies": { + "asap": "~2.0.3" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-agent": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.4.0.tgz", + "integrity": "sha512-u0piLU+nCOHMgGjRbimiXmA9kM/L9EHh3zL81xCdp7m+Y2pHIsnmbdDoEDoAz5geaonNR6q6+yOPQs6n4T6sBQ==", + "optional": true, + "dependencies": { + "agent-base": "^7.0.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.3", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.0.1", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.2" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-agent/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "optional": true + }, + "node_modules/prr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==", + "dev": true + }, + "node_modules/pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "optional": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/puppeteer": { + "version": "23.1.0", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-23.1.0.tgz", + "integrity": "sha512-m+CyicDlGN1AVUeOsCa6/+KQydJzxfsPowL7fQy+VGNeaWafB0m8G5aGfXdfZztKMxzCsdz7KNNzbJPeG9wwFw==", + "hasInstallScript": true, + "optional": true, + "dependencies": { + "@puppeteer/browsers": "2.3.1", + "chromium-bidi": "0.6.4", + "cosmiconfig": "^9.0.0", + "devtools-protocol": "0.0.1312386", + "puppeteer-core": "23.1.0", + "typed-query-selector": "^2.12.0" + }, + "bin": { + "puppeteer": "lib/cjs/puppeteer/node/cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/puppeteer-core": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-5.5.0.tgz", + "integrity": "sha512-tlA+1n+ziW/Db03hVV+bAecDKse8ihFRXYiEypBe9IlLRvOCzYFG6qrCMBYK34HO/Q/Ecjc+tvkHRAfLVH+NgQ==", + "optional": true, + "dependencies": { + "debug": "^4.1.0", + "devtools-protocol": "0.0.818844", + "extract-zip": "^2.0.0", + "https-proxy-agent": "^4.0.0", + "node-fetch": "^2.6.1", + "pkg-dir": "^4.2.0", + "progress": "^2.0.1", + "proxy-from-env": "^1.0.0", + "rimraf": "^3.0.2", + "tar-fs": "^2.0.0", + "unbzip2-stream": "^1.3.3", + "ws": "^7.2.3" + }, + "engines": { + "node": ">=10.18.1" + } + }, + "node_modules/puppeteer-core/node_modules/agent-base": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-5.1.1.tgz", + "integrity": "sha512-TMeqbNl2fMW0nMjTEPOwe3J/PRFP4vqeoNuQMG0HlMrtm5QxKqdvAkZ1pRBQ/ulIyDD5Yq0nJ7YbdD8ey0TO3g==", + "optional": true, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/puppeteer-core/node_modules/https-proxy-agent": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-4.0.0.tgz", + "integrity": "sha512-zoDhWrkR3of1l9QAL8/scJZyLu8j/gBkcwcaQOZh7Gyh/+uJQzGVETdgT30akuwkpL8HTRfssqI3BZuV18teDg==", + "optional": true, + "dependencies": { + "agent-base": "5", + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/puppeteer-core/node_modules/tar-fs": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", + "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", + "optional": true, + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/puppeteer-core/node_modules/ws": { + "version": "7.5.9", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", + "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", + "optional": true, + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/puppeteer/node_modules/devtools-protocol": { + "version": "0.0.1312386", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1312386.tgz", + "integrity": "sha512-DPnhUXvmvKT2dFA/j7B+riVLUt9Q6RKJlcppojL5CoRywJJKLDYnRlw0gTFKfgDPHP5E04UoB71SxoJlVZy8FA==", + "optional": true + }, + "node_modules/puppeteer/node_modules/puppeteer-core": { + "version": "23.1.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-23.1.0.tgz", + "integrity": "sha512-SvAsu+xnLN2FMXE/59bp3s3WXp8ewqUGzVV4AQtml/2xmsciZnU/bXcCW+eETHPWQ6Agg2vTI7QzWXPpEARK2g==", + "optional": true, + "dependencies": { + "@puppeteer/browsers": "2.3.1", + "chromium-bidi": "0.6.4", + "debug": "^4.3.6", + "devtools-protocol": "0.0.1312386", + "typed-query-selector": "^2.12.0", + "ws": "^8.18.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/python-shell": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/python-shell/-/python-shell-4.0.0.tgz", + "integrity": "sha512-yuucmp/YrsyLA80gbw+u3f8Db+qcjnr27MJ7uKdlCOOhsLXmKxDcTRhxV3Euvm9F73HO0C8UOVoFakAWVAte9w==", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/queue-tick": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz", + "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==", + "optional": true + }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "optional": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readable-web-to-node-stream": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.2.tgz", + "integrity": "sha512-ePeK6cc1EcKLEhJFt/AebMCLL+GgSKhuygrZ/GLaKZYEecIgIECf4UaUuaByiGtzckwR4ain9VzUh95T1exYGw==", + "optional": true, + "dependencies": { + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/readdir-glob": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", + "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", + "optional": true, + "dependencies": { + "minimatch": "^5.1.0" + } + }, + "node_modules/readdir-glob/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "optional": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/readdir-glob/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "optional": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/rechoir": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.7.1.tgz", + "integrity": "sha512-/njmZ8s1wVeR6pjTZ+0nCnv8SpZNRMT2D1RLOJQESlYFDBvwpTA4KWJpZ+sBJ4+vhjILRcK7JIFdGCdxEAAitg==", + "dev": true, + "dependencies": { + "resolve": "^1.9.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/recursive-copy": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/recursive-copy/-/recursive-copy-2.0.14.tgz", + "integrity": "sha512-K8WNY8f8naTpfbA+RaXmkaQuD1IeW9EgNEfyGxSqqTQukpVtoOKros9jUqbpEsSw59YOmpd8nCBgtqJZy5nvog==", + "dev": true, + "dependencies": { + "errno": "^0.1.2", + "graceful-fs": "^4.1.4", + "junk": "^1.0.1", + "maximatch": "^0.1.0", + "mkdirp": "^0.5.1", + "pify": "^2.3.0", + "promise": "^7.0.1", + "rimraf": "^2.7.1", + "slash": "^1.0.0" + } + }, + "node_modules/recursive-copy/node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/recursive-copy/node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/recursive-copy/node_modules/slash": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz", + "integrity": "sha512-3TYDR7xWt4dIqV2JauJr+EJeW356RXijHeUlO+8djJ+uBXPn8/2dpzBc8yQhh583sVvc9CvFAeQVgijsH+PNNg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", + "optional": true + }, + "node_modules/relateurl": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", + "integrity": "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/renderkid": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-3.0.0.tgz", + "integrity": "sha512-q/7VIQA8lmM1hF+jn+sFSPWGlMkSAeNYcPLmDQx2zzuiDfaLrOmumR8iaUKlenFgh0XRPIUeSPlH3A+AW3Z5pg==", + "dev": true, + "dependencies": { + "css-select": "^4.1.3", + "dom-converter": "^0.2.0", + "htmlparser2": "^6.1.0", + "lodash": "^4.17.21", + "strip-ansi": "^6.0.1" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", + "optional": true + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-cwd/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "devOptional": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/responselike": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", + "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", + "optional": true, + "dependencies": { + "lowercase-keys": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/resq": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/resq/-/resq-1.11.0.tgz", + "integrity": "sha512-G10EBz+zAAy3zUd/CDoBbXRL6ia9kOo3xRHrMDsHljI0GDkhYlyjwoCx5+3eCC4swi1uCoZQhskuJkj7Gp57Bw==", + "optional": true, + "dependencies": { + "fast-deep-equal": "^2.0.1" + } + }, + "node_modules/resq/node_modules/fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w==", + "optional": true + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rgb2hex": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/rgb2hex/-/rgb2hex-0.2.3.tgz", + "integrity": "sha512-clEe0m1xv+Tva1B/TOepuIcvLAxP0U+sCDfgt1SX1HmI2Ahr5/Cd/nzJM1e78NKVtWdoo0s33YehpFA8UfIShQ==", + "optional": true + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "devOptional": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/sanitize-filename": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.3.tgz", + "integrity": "sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==", + "optional": true, + "dependencies": { + "truncate-utf8-bytes": "^1.0.0" + } + }, + "node_modules/sax": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.3.0.tgz", + "integrity": "sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==" + }, + "node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "devOptional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/serialize-error": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-8.1.0.tgz", + "integrity": "sha512-3NnuWfM6vBYoy5gZFvHiYsVbafvI9vZv/+jlIigFn4oP4zjNPK3LhcY0xSCgeb1a5L8jO71Mit9LlNoi2UfDDQ==", + "optional": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/serve-favicon": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/serve-favicon/-/serve-favicon-2.5.0.tgz", + "integrity": "sha512-FMW2RvqNr03x+C0WxTyu6sOv21oOjkq5j8tjquWccwa6ScNyGFOGJVpuS1NmTVGBAHS07xnSKotgf2ehQmf9iA==", + "optional": true, + "dependencies": { + "etag": "~1.8.1", + "fresh": "0.5.2", + "ms": "2.1.1", + "parseurl": "~1.3.2", + "safe-buffer": "5.1.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/serve-favicon/node_modules/ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", + "optional": true + }, + "node_modules/serve-favicon/node_modules/safe-buffer": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", + "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==", + "optional": true + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "optional": true + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "node_modules/shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "dev": true, + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/shell-quote": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz", + "integrity": "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==", + "optional": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "optional": true + }, + "node_modules/simple-html-tokenizer": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/simple-html-tokenizer/-/simple-html-tokenizer-0.1.1.tgz", + "integrity": "sha512-Mc/gH3RvlKvB/gkp9XwgDKEWrSYyefIJPGG8Jk1suZms/rISdUuVEMx5O1WBnTWaScvxXDvGJrZQWblUmQHjkQ==", + "dev": true + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "optional": true, + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz", + "integrity": "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==", + "optional": true, + "dependencies": { + "ip-address": "^9.0.5", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.4.tgz", + "integrity": "sha512-GNAq/eg8Udq2x0eNiFkr9gRg5bA7PXEWagQdeRX4cPSG+X/8V38v637gim9bjFptMk1QWsCTr0ttrJEiXbNnRw==", + "optional": true, + "dependencies": { + "agent-base": "^7.1.1", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "devOptional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "devOptional": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "optional": true + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/stream-buffers": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/stream-buffers/-/stream-buffers-2.2.0.tgz", + "integrity": "sha512-uyQK/mx5QjHun80FLJTfaWE7JtwfRMKBLkMne6udYOmvH0CawotVa7TfgYHzAnpphn4+TweIx1QKMnRIbipmUg==", + "optional": true, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/stream-combiner": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.2.2.tgz", + "integrity": "sha512-6yHMqgLYDzQDcAkL+tjJDC5nSNuNIx0vZtRZeiPh7Saef7VHX9H5Ijn9l2VIol2zaNYlYEX6KyuT/237A58qEQ==", + "optional": true, + "dependencies": { + "duplexer": "~0.1.1", + "through": "~2.3.4" + } + }, + "node_modules/streamx": { + "version": "2.18.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.18.0.tgz", + "integrity": "sha512-LLUC1TWdjVdn1weXGcSxyTR3T4+acB6tVGXT95y0nGbca4t4o/ng1wKAGTljm9VicuCVLvRlqFYXYy5GwgM7sQ==", + "optional": true, + "dependencies": { + "fast-fifo": "^1.3.2", + "queue-tick": "^1.0.1", + "text-decoder": "^1.1.0" + }, + "optionalDependencies": { + "bare-events": "^2.2.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "optional": true, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "optional": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "devOptional": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strtok3": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-6.3.0.tgz", + "integrity": "sha512-fZtbhtvI9I48xDSywd/somNqgUHl2L2cstmXCCif0itOf96jeW18MBSyrLuNicYQVkvpOxkZtkzujiTJ9LW5Jw==", + "optional": true, + "dependencies": { + "@tokenizer/token": "^0.3.0", + "peek-readable": "^4.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "devOptional": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/svg-inline-loader": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/svg-inline-loader/-/svg-inline-loader-0.8.2.tgz", + "integrity": "sha512-kbrcEh5n5JkypaSC152eGfGcnT4lkR0eSfvefaUJkLqgGjRQJyKDvvEE/CCv5aTSdfXuc+N98w16iAojhShI3g==", + "dev": true, + "dependencies": { + "loader-utils": "^1.1.0", + "object-assign": "^4.0.1", + "simple-html-tokenizer": "^0.1.1" + } + }, + "node_modules/svg-inline-loader/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/svg-inline-loader/node_modules/loader-utils": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz", + "integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==", + "dev": true, + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^1.0.1" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/sylvester.js": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/sylvester.js/-/sylvester.js-0.1.1.tgz", + "integrity": "sha512-BDJneP4tUmzFR4jrgANtuozDWHBnVyYW7aMTZsnp1zLUhv2xsk4K3sEE0YlJguSCz2lST7KLq3GYLNFacA9SmQ==", + "dev": true, + "engines": { + "node": ">=0.2.6" + } + }, + "node_modules/tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/tar-fs": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.6.tgz", + "integrity": "sha512-iokBDQQkUyeXhgPYaZxmczGPhnhXZ0CmrqI+MOb/WFGS9DW5wnfrLgtjUJBvz50vQ3qfRwJ62QVoCFu8mPVu5w==", + "optional": true, + "dependencies": { + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + }, + "optionalDependencies": { + "bare-fs": "^2.1.1", + "bare-path": "^2.1.0" + } + }, + "node_modules/tar-fs/node_modules/tar-stream": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", + "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", + "optional": true, + "dependencies": { + "b4a": "^1.6.4", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "optional": true, + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/teen_process": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/teen_process/-/teen_process-1.16.0.tgz", + "integrity": "sha512-RnW7HHZD1XuhSTzD3djYOdIl1adE3oNEprE3HOFFxWs5m4FZsqYRhKJ4mDU2udtNGMLUS7jV7l8vVRLWAvmPDw==", + "engines": [ + "node" + ], + "optional": true, + "dependencies": { + "@babel/runtime": "^7.0.0", + "bluebird": "^3.5.1", + "lodash": "^4.17.4", + "shell-quote": "^1.4.3", + "source-map-support": "^0.5.3", + "which": "^2.0.2" + } + }, + "node_modules/teen_process/node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "optional": true + }, + "node_modules/terser": { + "version": "5.29.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.29.1.tgz", + "integrity": "sha512-lZQ/fyaIGxsbGxApKmoPTODIzELy3++mXhS5hOqaAWZjQtpq/hFHAc+rm29NND1rYRxRWKcjuARNwULNXa5RtQ==", + "dev": true, + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.3.10", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz", + "integrity": "sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.20", + "jest-worker": "^27.4.5", + "schema-utils": "^3.1.1", + "serialize-javascript": "^6.0.1", + "terser": "^5.26.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, + "node_modules/text-decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.1.1.tgz", + "integrity": "sha512-8zll7REEv4GDD3x4/0pW+ppIxSNs7H1J10IKFZsuOMscumCdM2a+toDGLPA3T+1+fLBql4zbt5z83GEQGGV5VA==", + "optional": true, + "dependencies": { + "b4a": "^1.6.4" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "optional": true + }, + "node_modules/timm": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/timm/-/timm-1.7.1.tgz", + "integrity": "sha512-IjZc9KIotudix8bMaBW6QvMuq64BrJWFs1+4V0lXwWGQZwH+LnX87doAYhem4caOEusRP9/g6jVDQmZ8XOk1nw==", + "optional": true + }, + "node_modules/tinycolor2": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz", + "integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==", + "optional": true + }, + "node_modules/tinyh264": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/tinyh264/-/tinyh264-0.0.7.tgz", + "integrity": "sha512-etkBRgYkSFBdAi2Cqk4sZgi+xWs/vhzNgvjO3z2i4WILeEmORiNqxuQ4URJatrWQ9LPNV3WPWAtzsh/LA/XL/g==", + "dev": true + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/token-types": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-4.2.1.tgz", + "integrity": "sha512-6udB24Q737UD/SDsKAHI9FCRP7Bqc9D/MQUV02ORQg5iskjtLJlZJNdN4kKtcdtwCeWIwIHDGaUsTsCCAa8sFQ==", + "optional": true, + "dependencies": { + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "optional": true + }, + "node_modules/truncate-utf8-bytes": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", + "integrity": "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==", + "optional": true, + "dependencies": { + "utf8-byte-length": "^1.0.1" + } + }, + "node_modules/ts-loader": { + "version": "9.5.1", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.1.tgz", + "integrity": "sha512-rNH3sK9kGZcH9dYzC7CewQm4NtxJTjSEVRJ2DyBZR7f8/wcta+iV44UPCXc5+nzDzivKtlzV6c9P4e+oFhDLYg==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "enhanced-resolve": "^5.0.0", + "micromatch": "^4.0.0", + "semver": "^7.3.4", + "source-map": "^0.7.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "typescript": "*", + "webpack": "^5.0.0" + } + }, + "node_modules/ts-loader/node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + }, + "node_modules/tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "dependencies": { + "tslib": "^1.8.1" + }, + "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" + } + }, + "node_modules/tsutils/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "devOptional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typed-query-selector": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.0.tgz", + "integrity": "sha512-SbklCd1F0EiZOyPiW192rrHZzZ5sBijB6xM+cpmrwDqObvdtunOHHIk9fCGsoK5JVIYXoyEp4iEdE3upFH3PAg==", + "optional": true + }, + "node_modules/typescript": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "devOptional": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/ua-parser-js": { + "version": "0.7.37", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.37.tgz", + "integrity": "sha512-xV8kqRKM+jhMvcHWUKthV9fNebIzrNy//2O9ZwWcfiBFR5f25XVZPLlEajk/sf3Ra15V92isyQqnIEXRDaZWEA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + }, + { + "type": "github", + "url": "https://github.com/sponsors/faisalman" + } + ], + "optional": true, + "engines": { + "node": "*" + } + }, + "node_modules/unbzip2-stream": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", + "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==", + "optional": true, + "dependencies": { + "buffer": "^5.2.1", + "through": "^2.3.8" + } + }, + "node_modules/unbzip2-stream/node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "optional": true, + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "optional": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unorm": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/unorm/-/unorm-1.6.0.tgz", + "integrity": "sha512-b2/KCUlYZUeA7JFUuRJZPUtr4gZvBh7tavtv4fvk4+KV9pfGiR6CQAQAWl49ZpR3ts2dk4FYkP7EIgDJoiOLDA==", + "optional": true, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", + "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/urlpattern-polyfill": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-10.0.0.tgz", + "integrity": "sha512-H/A06tKD7sS1O1X2SshBVeA5FLycRpjqiBeqGKmBwBDBy28EnRjORxTNe269KSSr5un5qyWi1iL61wLxpd+ZOg==", + "optional": true + }, + "node_modules/utf8-byte-length": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.4.tgz", + "integrity": "sha512-4+wkEYLBbWxqTahEsWrhxepcoVOJ+1z5PGIjPZxRkytcdSUaNjIjBM7Xn8E+pdSuV7SzvWovBFA54FO0JSoqhA==", + "optional": true + }, + "node_modules/utif2": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/utif2/-/utif2-4.1.0.tgz", + "integrity": "sha512-+oknB9FHrJ7oW7A2WZYajOcv4FcDR4CfoGB0dPNfxbi4GO05RRnFmt5oa23+9w32EanrYcSJWspUiJkLMs+37w==", + "optional": true, + "dependencies": { + "pako": "^1.0.11" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "devOptional": true + }, + "node_modules/utila": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz", + "integrity": "sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==", + "dev": true + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true + }, + "node_modules/validate.js": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/validate.js/-/validate.js-0.13.1.tgz", + "integrity": "sha512-PnFM3xiZ+kYmLyTiMgTYmU7ZHkjBZz2/+F0DaALc/uUtVzdCt1wAosvYJ5hFQi/hz8O4zb52FQhHZRC+uVkJ+g==", + "optional": true + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/watchpack": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz", + "integrity": "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==", + "dev": true, + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webdriver": { + "version": "6.12.1", + "resolved": "https://registry.npmjs.org/webdriver/-/webdriver-6.12.1.tgz", + "integrity": "sha512-3rZgAj9o2XHp16FDTzvUYaHelPMSPbO1TpLIMUT06DfdZjNYIzZiItpIb/NbQDTPmNhzd9cuGmdI56WFBGY2BA==", + "optional": true, + "dependencies": { + "@wdio/config": "6.12.1", + "@wdio/logger": "6.10.10", + "@wdio/protocols": "6.12.0", + "@wdio/utils": "6.11.0", + "got": "^11.0.2", + "lodash.merge": "^4.6.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/webdriverio": { + "version": "6.12.1", + "resolved": "https://registry.npmjs.org/webdriverio/-/webdriverio-6.12.1.tgz", + "integrity": "sha512-Nx7ge0vTWHVIRUbZCT+IuMwB5Q0Q5nLlYdgnmmJviUKLuc3XtaEBkYPTbhHWHgSBXsPZMIc023vZKNkn+6iyeQ==", + "optional": true, + "dependencies": { + "@types/puppeteer-core": "^5.4.0", + "@wdio/config": "6.12.1", + "@wdio/logger": "6.10.10", + "@wdio/repl": "6.11.0", + "@wdio/utils": "6.11.0", + "archiver": "^5.0.0", + "atob": "^2.1.2", + "css-shorthand-properties": "^1.1.1", + "css-value": "^0.0.1", + "devtools": "6.12.1", + "fs-extra": "^9.0.1", + "get-port": "^5.1.1", + "grapheme-splitter": "^1.0.2", + "lodash.clonedeep": "^4.5.0", + "lodash.isobject": "^3.0.2", + "lodash.isplainobject": "^4.0.6", + "lodash.zip": "^4.2.0", + "minimatch": "^3.0.4", + "puppeteer-core": "^5.1.0", + "resq": "^1.9.1", + "rgb2hex": "0.2.3", + "serialize-error": "^8.0.0", + "webdriver": "6.12.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "optional": true + }, + "node_modules/webpack": { + "version": "5.94.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.94.0.tgz", + "integrity": "sha512-KcsGn50VT+06JH/iunZJedYGUJS5FGjow8wb9c0v5n1Om8O1g4L6LjtfxwlXIATopoQu+vOXXa7gYisWxCoPyg==", + "dev": true, + "dependencies": { + "@types/estree": "^1.0.5", + "@webassemblyjs/ast": "^1.12.1", + "@webassemblyjs/wasm-edit": "^1.12.1", + "@webassemblyjs/wasm-parser": "^1.12.1", + "acorn": "^8.7.1", + "acorn-import-attributes": "^1.9.5", + "browserslist": "^4.21.10", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.17.1", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^3.2.0", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.3.10", + "watchpack": "^2.4.1", + "webpack-sources": "^3.2.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-cli": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-4.10.0.tgz", + "integrity": "sha512-NLhDfH/h4O6UOy+0LSso42xvYypClINuMNBVVzX4vX98TmTaTUxwRbXdhucbFMd2qLaCTcLq/PdYrvi8onw90w==", + "dev": true, + "dependencies": { + "@discoveryjs/json-ext": "^0.5.0", + "@webpack-cli/configtest": "^1.2.0", + "@webpack-cli/info": "^1.5.0", + "@webpack-cli/serve": "^1.7.0", + "colorette": "^2.0.14", + "commander": "^7.0.0", + "cross-spawn": "^7.0.3", + "fastest-levenshtein": "^1.0.12", + "import-local": "^3.0.2", + "interpret": "^2.2.0", + "rechoir": "^0.7.0", + "webpack-merge": "^5.7.3" + }, + "bin": { + "webpack-cli": "bin/cli.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "4.x.x || 5.x.x" + }, + "peerDependenciesMeta": { + "@webpack-cli/generators": { + "optional": true + }, + "@webpack-cli/migrate": { + "optional": true + }, + "webpack-bundle-analyzer": { + "optional": true + }, + "webpack-dev-server": { + "optional": true + } + } + }, + "node_modules/webpack-cli/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/webpack-merge": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", + "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==", + "dev": true, + "dependencies": { + "clone-deep": "^4.0.1", + "flat": "^5.0.2", + "wildcard": "^2.0.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/webpack-node-externals": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/webpack-node-externals/-/webpack-node-externals-2.5.2.tgz", + "integrity": "sha512-aHdl/y2N7PW2Sx7K+r3AxpJO+aDMcYzMQd60Qxefq3+EwhewSbTBqNumOsCE1JsCUNoyfGj5465N0sSf6hc/5w==", + "dev": true + }, + "node_modules/webpack-sources": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", + "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "dev": true, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/whatwg-fetch": { + "version": "3.6.20", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz", + "integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==", + "optional": true + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "optional": true, + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "devOptional": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "optional": true, + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/wildcard": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", + "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", + "dev": true + }, + "node_modules/worker-loader": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/worker-loader/-/worker-loader-3.0.8.tgz", + "integrity": "sha512-XQyQkIFeRVC7f7uRhFdNMe/iJOdO6zxAaR3EWbDp45v3mDhrTi+++oswKNxShUNjPC/1xUp5DB29YKLhFo129g==", + "dev": true, + "dependencies": { + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "optional": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "devOptional": true + }, + "node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xhr": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/xhr/-/xhr-2.6.0.tgz", + "integrity": "sha512-/eCGLb5rxjx5e3mF1A7s+pLlR6CGyqWN91fv1JgER5mVWg1MZmlhBvy9kjcsOdRk8RrIujotWyJamfyrp+WIcA==", + "optional": true, + "dependencies": { + "global": "~4.4.0", + "is-function": "^1.0.1", + "parse-headers": "^2.0.0", + "xtend": "^4.0.0" + } + }, + "node_modules/xml-parse-from-string": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/xml-parse-from-string/-/xml-parse-from-string-1.0.1.tgz", + "integrity": "sha512-ErcKwJTF54uRzzNMXq2X5sMIy88zJvfN2DmdoQvy7PAFJ+tPRU6ydWuOKNMyfmOjdyBQTFREi60s0Y0SyI0G0g==", + "optional": true + }, + "node_modules/xml2js": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", + "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xml2js/node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/xmlbuilder": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", + "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", + "optional": true, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "optional": true, + "engines": { + "node": ">=0.4" + } + }, + "node_modules/xterm": { + "version": "4.19.0", + "resolved": "https://registry.npmjs.org/xterm/-/xterm-4.19.0.tgz", + "integrity": "sha512-c3Cp4eOVsYY5Q839dR5IejghRPpxciGmLWWaP9g+ppfMeBChMeLa1DCA+pmX/jyDZ+zxFOmlJL/82qVdayVoGQ==", + "dev": true + }, + "node_modules/xterm-addon-attach": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/xterm-addon-attach/-/xterm-addon-attach-0.6.0.tgz", + "integrity": "sha512-Mo8r3HTjI/EZfczVCwRU6jh438B4WLXxdFO86OB7bx0jGhwh2GdF4ifx/rP+OB+Cb2vmLhhVIZ00/7x3YSP3dg==", + "dev": true, + "peerDependencies": { + "xterm": "^4.0.0" + } + }, + "node_modules/xterm-addon-fit": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/xterm-addon-fit/-/xterm-addon-fit-0.5.0.tgz", + "integrity": "sha512-DsS9fqhXHacEmsPxBJZvfj2la30Iz9xk+UKjhQgnYNkrUIN5CYLbw7WEfz117c7+S86S/tpHPfvNxJsF5/G8wQ==", + "dev": true, + "peerDependencies": { + "xterm": "^4.0.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "optional": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "optional": true + }, + "node_modules/yaml": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.1.tgz", + "integrity": "sha512-pIXzoImaqmfOrL7teGUBt/T7ZDnyeGBWyXQBvOVhLkWLN37GXv8NMLK406UY6dS51JfcQHsmcW5cJ441bHg6Lg==", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "optional": true, + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "optional": true, + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zip-stream": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.1.tgz", + "integrity": "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==", + "optional": true, + "dependencies": { + "archiver-utils": "^3.0.4", + "compress-commons": "^4.1.2", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/zip-stream/node_modules/archiver-utils": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-3.0.4.tgz", + "integrity": "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==", + "optional": true, + "dependencies": { + "glob": "^7.2.3", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/zod": { + "version": "3.23.8", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", + "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", + "optional": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..5f8b36f --- /dev/null +++ b/package.json @@ -0,0 +1,77 @@ +{ + "name": "ws-scrcpy", + "version": "0.9.0-dev", + "description": "Web client for scrcpy and more", + "scripts": { + "clean": "npx rimraf dist", + "dist:dev": "webpack --config webpack/ws-scrcpy.dev.ts --stats-error-details", + "dist:prod": "webpack --config webpack/ws-scrcpy.prod.ts --stats-error-details", + "dist": "npm run dist:prod", + "start": "npm run dist && cd dist && npm start", + "script:dist:start": "node ./index.js", + "lint": "eslint src/ --ext .ts", + "format": "eslint src/ --fix --ext .ts", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "Sergey Volkov ", + "license": "MIT", + "dependencies": { + "@dead50f7/adbkit": "^2.11.4", + "express": "^4.21.2", + "ios-device-lib": "^0.9.2", + "node-mjpeg-proxy": "^0.3.2", + "node-pty": "^0.10.1", + "portfinder": "^1.0.28", + "python-shell": "^4.0.0", + "tslib": "^2.3.1", + "ws": "^8.18.0", + "xml2js": "^0.6.2", + "yaml": "^2.2.2" + }, + "devDependencies": { + "@dead50f7/generate-package-json-webpack-plugin": "^2.6.1", + "@types/bluebird": "^3.5.36", + "@types/dom-webcodecs": "^0.1.3", + "@types/express": "^4.17.13", + "@types/node": "^12.20.47", + "@types/node-forge": "^0.10.0", + "@types/npmlog": "^4.1.4", + "@types/webpack-node-externals": "^2.5.3", + "@types/ws": "^7.4.7", + "@typescript-eslint/eslint-plugin": "^5.18.0", + "@typescript-eslint/parser": "^5.18.0", + "buffer": "^6.0.3", + "cross-env": "^7.0.3", + "css-loader": "^6.8.1", + "eslint": "^8.12.0", + "eslint-config-prettier": "^8.5.0", + "eslint-plugin-prettier": "^4.0.0", + "eslint-plugin-progress": "0.0.1", + "file-loader": "^6.2.0", + "h264-converter": "^0.1.4", + "html-webpack-plugin": "^5.5.0", + "ifdef-loader": "^2.3.2", + "mini-css-extract-plugin": "^2.6.1", + "mkdirp": "^1.0.4", + "path-browserify": "^1.0.1", + "prettier": "^2.6.2", + "recursive-copy": "^2.0.14", + "rimraf": "^3.0.0", + "svg-inline-loader": "^0.8.2", + "sylvester.js": "^0.1.1", + "tinyh264": "^0.0.7", + "ts-loader": "^9.3.1", + "ts-node": "^10.9.1", + "typescript": "^4.7.4", + "webpack": "^5.94.0", + "webpack-cli": "^4.10.0", + "webpack-node-externals": "^2.5.2", + "worker-loader": "^3.0.8", + "xterm": "^4.5.0", + "xterm-addon-attach": "^0.6.0", + "xterm-addon-fit": "^0.5.0" + }, + "optionalDependencies": { + "appium-xcuitest-driver": "^3.62.0" + } +} diff --git a/src/app/Attribute.ts b/src/app/Attribute.ts new file mode 100644 index 0000000..866b968 --- /dev/null +++ b/src/app/Attribute.ts @@ -0,0 +1,14 @@ +export const Attribute = { + COMMAND: 'data-command', + FULL_NAME: 'data-full-name', + NAME: 'data-name', + PID: 'data-pid', + UDID: 'data-udid', + URL: 'data-url', + USE_PROXY: 'data-use-proxy', + SECURE: 'data-secure', + HOSTNAME: 'data-hostname', + PORT: 'data-port', + PATHNAME: 'data-pathname', + VALUE: 'data-value', +}; diff --git a/src/app/DisplayInfo.ts b/src/app/DisplayInfo.ts new file mode 100644 index 0000000..ea3c011 --- /dev/null +++ b/src/app/DisplayInfo.ts @@ -0,0 +1,61 @@ +import Size from './Size'; + +export class DisplayInfo { + public static readonly DEFAULT_DISPLAY = 0x00000000; + public static readonly FLAG_ROUND = 0b10000; + public static readonly FLAG_PRESENTATION = 0b1000; + public static readonly FLAG_PRIVATE = 0b100; + public static readonly FLAG_SECURE = 0b10; + public static readonly FLAG_SUPPORTS_PROTECTED_BUFFERS = 0b1; + public static readonly INVALID_DISPLAY = -1; + public static readonly BUFFER_LENGTH = 24; + + constructor( + public readonly displayId: number, + public readonly size: Size, + public readonly rotation: number, + public readonly layerStack: number, + public readonly flags: number, + ) {} + + public toBuffer(): Buffer { + const temp = Buffer.alloc(DisplayInfo.BUFFER_LENGTH); + let offset = 0; + offset = temp.writeInt32BE(this.displayId, offset); + offset = temp.writeInt32BE(this.size.width, offset); + offset = temp.writeInt32BE(this.size.height, offset); + offset = temp.writeInt32BE(this.rotation, offset); + offset = temp.writeInt32BE(this.layerStack, offset); + temp.writeInt32BE(this.flags, offset); + return temp; + } + + public toString(): string { + // prettier-ignore + return `DisplayInfo{displayId=${ + this.displayId}, size=${ + this.size}, rotation=${ + this.rotation}, layerStack=${ + this.layerStack}, flags=${ + this.flags}}`; + } + + public static fromBuffer(buffer: Buffer): DisplayInfo { + if (buffer.length !== DisplayInfo.BUFFER_LENGTH) { + throw Error(`Incorrect buffer length. Expected: ${DisplayInfo.BUFFER_LENGTH}, received: ${buffer.length}`); + } + let offset = 0; + const displayId = buffer.readInt32BE(offset); + offset += 4; + const width = buffer.readInt32BE(offset); + offset += 4; + const height = buffer.readInt32BE(offset); + offset += 4; + const rotation = buffer.readInt32BE(offset); + offset += 4; + const layerStack = buffer.readInt32BE(offset); + offset += 4; + const flags = buffer.readInt32BE(offset); + return new DisplayInfo(displayId, new Size(width, height), rotation, layerStack, flags); + } +} diff --git a/src/app/ErrorHandler.ts b/src/app/ErrorHandler.ts new file mode 100644 index 0000000..eae1327 --- /dev/null +++ b/src/app/ErrorHandler.ts @@ -0,0 +1,3 @@ +export default class ErrorHandler { + constructor(readonly OnError: (ev: string | Event) => void) {} +} diff --git a/src/app/MotionEvent.ts b/src/app/MotionEvent.ts new file mode 100644 index 0000000..8caddc0 --- /dev/null +++ b/src/app/MotionEvent.ts @@ -0,0 +1,19 @@ +export default class MotionEvent { + public static ACTION_DOWN = 0; + public static ACTION_UP = 1; + public static ACTION_MOVE = 2; + /** + * Button constant: Primary button (left mouse button). + */ + public static BUTTON_PRIMARY: number = 1 << 0; + + /** + * Button constant: Secondary button (right mouse button). + */ + public static BUTTON_SECONDARY: number = 1 << 1; + + /** + * Button constant: Tertiary button (middle mouse button). + */ + public static BUTTON_TERTIARY: number = 1 << 2; +} diff --git a/src/app/Point.ts b/src/app/Point.ts new file mode 100644 index 0000000..dc9a66c --- /dev/null +++ b/src/app/Point.ts @@ -0,0 +1,40 @@ +export interface PointInterface { + x: number; + y: number; +} + +export default class Point { + readonly x: number; + readonly y: number; + constructor(x: number, y: number) { + this.x = Math.round(x); + this.y = Math.round(y); + } + + public equals(o: Point): boolean { + if (this === o) { + return true; + } + if (o === null) { + return false; + } + return this.x === o.x && this.y === o.y; + } + + public distance(to: Point): number { + const x = this.x - to.x; + const y = this.y - to.y; + return Math.sqrt(x * x + y * y); + } + + public toString(): string { + return `Point{x=${this.x}, y=${this.y}}`; + } + + public toJSON(): PointInterface { + return { + x: this.x, + y: this.y, + }; + } +} diff --git a/src/app/Position.ts b/src/app/Position.ts new file mode 100644 index 0000000..38459cf --- /dev/null +++ b/src/app/Position.ts @@ -0,0 +1,55 @@ +import Point, { PointInterface } from './Point'; +import Size, { SizeInterface } from './Size'; + +export interface PositionInterface { + point: PointInterface; + screenSize: SizeInterface; +} + +export default class Position { + public constructor(readonly point: Point, readonly screenSize: Size) {} + + public equals(o: Position): boolean { + if (this === o) { + return true; + } + if (o === null) { + return false; + } + + return this.point.equals(o.point) && this.screenSize.equals(o.screenSize); + } + + public rotate(rotation: number): Position { + switch (rotation) { + case 1: + return new Position( + new Point(this.screenSize.height - this.point.y, this.point.x), + this.screenSize.rotate(), + ); + case 2: + return new Position( + new Point(this.screenSize.width - this.point.x, this.screenSize.height - this.point.y), + this.screenSize, + ); + case 3: + return new Position( + new Point(this.point.y, this.screenSize.width - this.point.x), + this.screenSize.rotate(), + ); + default: + return this; + } + } + + public toString(): string { + return `Position{point=${this.point}, screenSize=${this.screenSize}}`; + } + + public toJSON(): PositionInterface { + return { + point: this.point.toJSON(), + screenSize: this.screenSize.toJSON(), + }; + } +} diff --git a/src/app/Rect.ts b/src/app/Rect.ts new file mode 100644 index 0000000..9bcbd21 --- /dev/null +++ b/src/app/Rect.ts @@ -0,0 +1,62 @@ +interface RectInterface { + left: number; + top: number; + right: number; + bottom: number; +} + +export default class Rect { + constructor(readonly left: number, readonly top: number, readonly right: number, readonly bottom: number) { + this.left = left; + this.top = top; + this.right = right; + this.bottom = bottom; + } + public static equals(a?: Rect | null, b?: Rect | null): boolean { + if (!a && !b) { + return true; + } + return !!a && !!b && a.equals(b); + } + public static copy(a?: Rect | null): Rect | null { + if (!a) { + return null; + } + return new Rect(a.left, a.top, a.right, a.bottom); + } + public equals(o: Rect | null): boolean { + if (this === o) { + return true; + } + if (!o) { + return false; + } + return this.left === o.left && this.top === o.top && this.right === o.right && this.bottom === o.bottom; + } + + public getWidth(): number { + return this.right - this.left; + } + + public getHeight(): number { + return this.bottom - this.top; + } + + public toString(): string { + // prettier-ignore + return `Rect{left=${ + this.left}, top=${ + this.top}, right=${ + this.right}, bottom=${ + this.bottom}}`; + } + + public toJSON(): RectInterface { + return { + left: this.left, + right: this.right, + top: this.top, + bottom: this.bottom, + }; + } +} diff --git a/src/app/ScreenInfo.ts b/src/app/ScreenInfo.ts new file mode 100644 index 0000000..d89eeef --- /dev/null +++ b/src/app/ScreenInfo.ts @@ -0,0 +1,33 @@ +import Rect from './Rect'; +import Size from './Size'; + +export default class ScreenInfo { + public static readonly BUFFER_LENGTH: number = 25; + constructor(readonly contentRect: Rect, readonly videoSize: Size, readonly deviceRotation: number) {} + + public static fromBuffer(buffer: Buffer): ScreenInfo { + const left = buffer.readInt32BE(0); + const top = buffer.readInt32BE(4); + const right = buffer.readInt32BE(8); + const bottom = buffer.readInt32BE(12); + const width = buffer.readInt32BE(16); + const height = buffer.readInt32BE(20); + const deviceRotation = buffer.readUInt8(24); + return new ScreenInfo(new Rect(left, top, right, bottom), new Size(width, height), deviceRotation); + } + + public equals(o?: ScreenInfo | null): boolean { + if (!o) { + return false; + } + return ( + this.contentRect.equals(o.contentRect) && + this.videoSize.equals(o.videoSize) && + this.deviceRotation === o.deviceRotation + ); + } + + public toString(): string { + return `ScreenInfo{contentRect=${this.contentRect}, videoSize=${this.videoSize}, deviceRotation=${this.deviceRotation}}`; + } +} diff --git a/src/app/Size.ts b/src/app/Size.ts new file mode 100644 index 0000000..37ba77a --- /dev/null +++ b/src/app/Size.ts @@ -0,0 +1,70 @@ +export interface SizeInterface { + width: number; + height: number; +} + +export default class Size { + public readonly w: number; + public readonly h: number; + + constructor(readonly width: number, readonly height: number) { + this.w = width; + this.h = height; + } + + public static equals(a?: Size | null, b?: Size | null): boolean { + if (!a && !b) { + return true; + } + return !!a && !!b && a.equals(b); + } + + public static copy(a?: Size | null): Size | null { + if (!a) { + return null; + } + return new Size(a.width, a.height); + } + + length(): number { + return this.w * this.h; + } + + public rotate(): Size { + return new Size(this.height, this.width); + } + + public equals(o: Size | null | undefined): boolean { + if (this === o) { + return true; + } + if (!o) { + return false; + } + return this.width === o.width && this.height === o.height; + } + + public intersect(o: Size | undefined | null): Size { + if (!o) { + return this; + } + const minH = Math.min(this.height, o.height); + const minW = Math.min(this.width, o.width); + return new Size(minW, minH); + } + + public getHalfSize(): Size { + return new Size(this.width >>> 1, this.height >>> 1); + } + + public toString(): string { + return `Size{width=${this.width}, height=${this.height}}`; + } + + public toJSON(): SizeInterface { + return { + width: this.width, + height: this.height, + }; + } +} diff --git a/src/app/UIEventsCode.ts b/src/app/UIEventsCode.ts new file mode 100644 index 0000000..504ef48 --- /dev/null +++ b/src/app/UIEventsCode.ts @@ -0,0 +1,191 @@ +// https://w3c.github.io/uievents-code/ + +export default class UIEventsCode { + // 3.1.1.1. Writing System Keys + public static readonly Backquote: string = 'Backquote'; + public static readonly Backslash: string = 'Backslash'; + public static readonly BracketLeft: string = 'BracketLeft'; + public static readonly BracketRight: string = 'BracketRight'; + public static readonly Comma: string = 'Comma'; + public static readonly Digit0: string = 'Digit0'; + public static readonly Digit1: string = 'Digit1'; + public static readonly Digit2: string = 'Digit2'; + public static readonly Digit3: string = 'Digit3'; + public static readonly Digit4: string = 'Digit4'; + public static readonly Digit5: string = 'Digit5'; + public static readonly Digit6: string = 'Digit6'; + public static readonly Digit7: string = 'Digit7'; + public static readonly Digit8: string = 'Digit8'; + public static readonly Digit9: string = 'Digit9'; + public static readonly Equal: string = 'Equal'; + public static readonly IntlBackslash: string = 'IntlBackslash'; + public static readonly IntlRo: string = 'IntlRo'; + public static readonly IntlYen: string = 'IntlYen'; + public static readonly KeyA: string = 'KeyA'; + public static readonly KeyB: string = 'KeyB'; + public static readonly KeyC: string = 'KeyC'; + public static readonly KeyD: string = 'KeyD'; + public static readonly KeyE: string = 'KeyE'; + public static readonly KeyF: string = 'KeyF'; + public static readonly KeyG: string = 'KeyG'; + public static readonly KeyH: string = 'KeyH'; + public static readonly KeyI: string = 'KeyI'; + public static readonly KeyJ: string = 'KeyJ'; + public static readonly KeyK: string = 'KeyK'; + public static readonly KeyL: string = 'KeyL'; + public static readonly KeyM: string = 'KeyM'; + public static readonly KeyN: string = 'KeyN'; + public static readonly KeyO: string = 'KeyO'; + public static readonly KeyP: string = 'KeyP'; + public static readonly KeyQ: string = 'KeyQ'; + public static readonly KeyR: string = 'KeyR'; + public static readonly KeyS: string = 'KeyS'; + public static readonly KeyT: string = 'KeyT'; + public static readonly KeyU: string = 'KeyU'; + public static readonly KeyV: string = 'KeyV'; + public static readonly KeyW: string = 'KeyW'; + public static readonly KeyX: string = 'KeyX'; + public static readonly KeyY: string = 'KeyY'; + public static readonly KeyZ: string = 'KeyZ'; + public static readonly Minus: string = 'Minus'; + public static readonly Period: string = 'Period'; + public static readonly Quote: string = 'Quote'; + public static readonly Semicolon: string = 'Semicolon'; + public static readonly Slash: string = 'Slash'; + + // 3.1.1.2. Functional Keys + public static readonly AltLeft: string = 'AltLeft'; + public static readonly AltRight: string = 'AltRight'; + public static readonly Backspace: string = 'Backspace'; + public static readonly CapsLock: string = 'CapsLock'; + public static readonly ContextMenu: string = 'ContextMenu'; + public static readonly ControlLeft: string = 'ControlLeft'; + public static readonly ControlRight: string = 'ControlRight'; + public static readonly Enter: string = 'Enter'; + public static readonly MetaLeft: string = 'MetaLeft'; + public static readonly MetaRight: string = 'MetaRight'; + public static readonly ShiftLeft: string = 'ShiftLeft'; + public static readonly ShiftRight: string = 'ShiftRight'; + public static readonly Space: string = 'Space'; + public static readonly Tab: string = 'Tab'; + public static readonly Convert: string = 'Convert'; + public static readonly KanaMode: string = 'KanaMode'; + public static readonly Lang1: string = 'Lang1'; + public static readonly Lang2: string = 'Lang2'; + public static readonly Lang3: string = 'Lang3'; + public static readonly Lang4: string = 'Lang4'; + public static readonly Lang5: string = 'Lang5'; + public static readonly NonConvert: string = 'NonConvert'; + + // 3.1.2. Control Pad Section + public static readonly Delete: string = 'Delete'; + public static readonly End: string = 'End'; + public static readonly Help: string = 'Help'; + public static readonly Home: string = 'Home'; + public static readonly Insert: string = 'Insert'; + public static readonly PageDown: string = 'PageDown'; + public static readonly PageUp: string = 'PageUp'; + + // 3.1.3. Arrow Pad Section + public static readonly ArrowDown: string = 'ArrowDown'; + public static readonly ArrowLeft: string = 'ArrowLeft'; + public static readonly ArrowRight: string = 'ArrowRight'; + public static readonly ArrowUp: string = 'ArrowUp'; + + // 3.1.4. Numpad Section + public static readonly NumLock: string = 'NumLock'; + public static readonly Numpad0: string = 'Numpad0'; + public static readonly Numpad1: string = 'Numpad1'; + public static readonly Numpad2: string = 'Numpad2'; + public static readonly Numpad3: string = 'Numpad3'; + public static readonly Numpad4: string = 'Numpad4'; + public static readonly Numpad5: string = 'Numpad5'; + public static readonly Numpad6: string = 'Numpad6'; + public static readonly Numpad7: string = 'Numpad7'; + public static readonly Numpad8: string = 'Numpad8'; + public static readonly Numpad9: string = 'Numpad9'; + public static readonly NumpadAdd: string = 'NumpadAdd'; + public static readonly NumpadBackspace: string = 'NumpadBackspace'; + public static readonly NumpadClear: string = 'NumpadClear'; + public static readonly NumpadClearEntry: string = 'NumpadClearEntry'; + public static readonly NumpadComma: string = 'NumpadComma'; + public static readonly NumpadDecimal: string = 'NumpadDecimal'; + public static readonly NumpadDivide: string = 'NumpadDivide'; + public static readonly NumpadEnter: string = 'NumpadEnter'; + public static readonly NumpadEqual: string = 'NumpadEqual'; + public static readonly NumpadHash: string = 'NumpadHash'; + public static readonly NumpadMemoryAdd: string = 'NumpadMemoryAdd'; + public static readonly NumpadMemoryClear: string = 'NumpadMemoryClear'; + public static readonly NumpadMemoryRecall: string = 'NumpadMemoryRecall'; + public static readonly NumpadMemoryStore: string = 'NumpadMemoryStore'; + public static readonly NumpadMemorySubtract: string = 'NumpadMemorySubtract'; + public static readonly NumpadMultiply: string = 'NumpadMultiply'; + public static readonly NumpadParenLeft: string = 'NumpadParenLeft'; + public static readonly NumpadParenRight: string = 'NumpadParenRight'; + public static readonly NumpadStar: string = 'NumpadStar'; + public static readonly NumpadSubtract: string = 'NumpadSubtract'; + + // 3.1.5. Function Section + public static readonly Escape: string = 'Escape'; + public static readonly F1: string = 'F1'; + public static readonly F2: string = 'F2'; + public static readonly F3: string = 'F3'; + public static readonly F4: string = 'F4'; + public static readonly F5: string = 'F5'; + public static readonly F6: string = 'F6'; + public static readonly F7: string = 'F7'; + public static readonly F8: string = 'F8'; + public static readonly F9: string = 'F9'; + public static readonly F10: string = 'F10'; + public static readonly F11: string = 'F11'; + public static readonly F12: string = 'F12'; + public static readonly Fn: string = 'Fn'; + public static readonly FnLock: string = 'FnLock'; + public static readonly PrintScreen: string = 'PrintScreen'; + public static readonly ScrollLock: string = 'ScrollLock'; + public static readonly Pause: string = 'Pause'; + + // 3.1.6. Media Keys + public static readonly BrowserBack: string = 'BrowserBack'; + public static readonly BrowserFavorites: string = 'BrowserFavorites'; + public static readonly BrowserForward: string = 'BrowserForward'; + public static readonly BrowserHome: string = 'BrowserHome'; + public static readonly BrowserRefresh: string = 'BrowserRefresh'; + public static readonly BrowserSearch: string = 'BrowserSearch'; + public static readonly BrowserStop: string = 'BrowserStop'; + public static readonly Eject: string = 'Eject'; + public static readonly LaunchApp1: string = 'LaunchApp1'; + public static readonly LaunchApp2: string = 'LaunchApp2'; + public static readonly LaunchMail: string = 'LaunchMail'; + public static readonly MediaPlayPause: string = 'MediaPlayPause'; + public static readonly MediaSelect: string = 'MediaSelect'; + public static readonly MediaStop: string = 'MediaStop'; + public static readonly MediaTrackNext: string = 'MediaTrackNext'; + public static readonly MediaTrackPrevious: string = 'MediaTrackPrevious'; + public static readonly Power: string = 'Power'; + public static readonly Sleep: string = 'Sleep'; + public static readonly AudioVolumeDown: string = 'AudioVolumeDown'; + public static readonly AudioVolumeMute: string = 'AudioVolumeMute'; + public static readonly AudioVolumeUp: string = 'AudioVolumeUp'; + public static readonly WakeUp: string = 'WakeUp'; + + // 3.1.7. Legacy, Non-Standard and Special Keys + public static readonly Hyper: string = 'Hyper'; + public static readonly Super: string = 'Super'; + public static readonly Turbo: string = 'Turbo'; + public static readonly Abort: string = 'Abort'; + public static readonly Resume: string = 'Resume'; + public static readonly Suspend: string = 'Suspend'; + public static readonly Again: string = 'Again'; + public static readonly Copy: string = 'Copy'; + public static readonly Cut: string = 'Cut'; + public static readonly Find: string = 'Find'; + public static readonly Open: string = 'Open'; + public static readonly Paste: string = 'Paste'; + public static readonly Props: string = 'Props'; + public static readonly Select: string = 'Select'; + public static readonly Undo: string = 'Undo'; + public static readonly Hiragana: string = 'Hiragana'; + public static readonly Katakana: string = 'Katakana'; + public static readonly Unidentified: string = 'Unidentified'; +} diff --git a/src/app/Util.ts b/src/app/Util.ts new file mode 100644 index 0000000..80de261 --- /dev/null +++ b/src/app/Util.ts @@ -0,0 +1,219 @@ +export default class Util { + private static SUFFIX: Record = { + 0: 'B', + 1: 'KiB', + 2: 'MiB', + 3: 'GiB', + 4: 'TiB', + }; + private static supportsPassiveValue: boolean | undefined; + + public static filterTrailingZeroes(bytes: Uint8Array): Uint8Array { + let b = 0; + return bytes + .reverse() + .filter((i) => b || (b = i)) + .reverse(); + } + + public static prettyBytes(value: number): string { + let suffix = 0; + while (value >= 512) { + suffix++; + value /= 1024; + } + return `${value.toFixed(suffix ? 1 : 0)}${Util.SUFFIX[suffix]}`; + } + + public static escapeUdid(udid: string): string { + return 'udid_' + udid.replace(/[. :]/g, '_'); + } + + public static parse(params: URLSearchParams, name: string, required?: boolean): string | null { + const value = params.get(name); + if (required && value === null) { + throw TypeError(`Missing required parameter "${name}"`); + } + return value; + } + + public static parseString(params: URLSearchParams, name: string, required?: boolean): string { + const value = params.get(name); + if (required && value === null) { + throw TypeError(`Missing required parameter "${name}"`); + } + return value || ''; + } + + public static parseBoolean(params: URLSearchParams, name: string, required?: boolean): boolean { + const value = this.parse(params, name, required); + return value === '1' || (!!value && value.toString() === 'true'); + } + + public static parseInt(params: URLSearchParams, name: string, required?: boolean): number { + const value = this.parse(params, name, required); + if (value === null) { + return 0; + } + const int = parseInt(value, 10); + if (isNaN(int)) { + return 0; + } + return int; + } + + public static parseBooleanEnv(input: string | string[] | boolean | undefined | null): boolean | undefined { + if (typeof input === 'boolean') { + return input; + } + if (typeof input === 'undefined' || input === null) { + return undefined; + } + if (Array.isArray(input)) { + input = input[input.length - 1]; + } + return input === '1' || input.toLowerCase() === 'true'; + } + + public static parseStringEnv(input: string | string[] | undefined | null): string | undefined { + if (typeof input === 'undefined' || input === null) { + return undefined; + } + if (Array.isArray(input)) { + input = input[input.length - 1]; + } + return input; + } + public static parseIntEnv(input: string | string[] | number | undefined | null): number | undefined { + if (typeof input === 'number') { + return input; + } + if (typeof input === 'undefined' || input === null) { + return undefined; + } + if (Array.isArray(input)) { + input = input[input.length - 1]; + } + const int = parseInt(input, 10); + if (isNaN(int)) { + return undefined; + } + return int; + } + + + // https://github.com/google/closure-library/blob/51e5a5ac373aefa354a991816ec418d730e29a7e/closure/goog/crypt/crypt.js#L117 +/* + Copyright 2008 The Closure Library Authors. All Rights Reserved. + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS-IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + /* tslint:disable */ + /** + * Converts a JS string to a UTF-8 "byte" array. + * @param {string} str 16-bit unicode string. + * @return {!Array} UTF-8 byte array. + */ + static stringToUtf8ByteArray = function(str: string) { + // TODO(user): Use native implementations if/when available + var out = [], p = 0; + for (var i = 0; i < str.length; i++) { + var c = str.charCodeAt(i); + if (c < 128) { + out[p++] = c; + } else if (c < 2048) { + out[p++] = (c >> 6) | 192; + out[p++] = (c & 63) | 128; + } else if ( + ((c & 0xFC00) == 0xD800) && (i + 1) < str.length && + ((str.charCodeAt(i + 1) & 0xFC00) == 0xDC00)) { + // Surrogate Pair + c = 0x10000 + ((c & 0x03FF) << 10) + (str.charCodeAt(++i) & 0x03FF); + out[p++] = (c >> 18) | 240; + out[p++] = ((c >> 12) & 63) | 128; + out[p++] = ((c >> 6) & 63) | 128; + out[p++] = (c & 63) | 128; + } else { + out[p++] = (c >> 12) | 224; + out[p++] = ((c >> 6) & 63) | 128; + out[p++] = (c & 63) | 128; + } + } + return Uint8Array.from(out); + }; + + /** + * Converts a UTF-8 byte array to JavaScript's 16-bit Unicode. + * @param {Uint8Array|Array} bytes UTF-8 byte array. + * @return {string} 16-bit Unicode string. + */ + static utf8ByteArrayToString(bytes: Uint8Array): string { + // TODO(user): Use native implementations if/when available + var out = [], pos = 0, c = 0; + while (pos < bytes.length) { + var c1 = bytes[pos++]; + if (c1 < 128) { + out[c++] = String.fromCharCode(c1); + } else if (c1 > 191 && c1 < 224) { + var c2 = bytes[pos++]; + out[c++] = String.fromCharCode((c1 & 31) << 6 | c2 & 63); + } else if (c1 > 239 && c1 < 365) { + // Surrogate Pair + var c2 = bytes[pos++]; + var c3 = bytes[pos++]; + var c4 = bytes[pos++]; + var u = ((c1 & 7) << 18 | (c2 & 63) << 12 | (c3 & 63) << 6 | c4 & 63) - + 0x10000; + out[c++] = String.fromCharCode(0xD800 + (u >> 10)); + out[c++] = String.fromCharCode(0xDC00 + (u & 1023)); + } else { + var c2 = bytes[pos++]; + var c3 = bytes[pos++]; + out[c++] = + String.fromCharCode((c1 & 15) << 12 | (c2 & 63) << 6 | c3 & 63); + } + } + return out.join(''); + }; + /* tslint:enable */ + + // https://github.com/WICG/EventListenerOptions/blob/gh-pages/explainer.md + static supportsPassive(): boolean { + if (typeof Util.supportsPassiveValue === 'boolean') { + return Util.supportsPassiveValue; + } + + // Test via a getter in the options object to see if the passive property is accessed + let supportsPassive = false; + try { + const opts = Object.defineProperty({}, 'passive', { + get: function() { + supportsPassive = true; + } + }); + + // @ts-ignore + window.addEventListener('testPassive', null, opts); + // @ts-ignore + window.removeEventListener('testPassive', null, opts); + } catch (error: any) {} + + return Util.supportsPassiveValue = supportsPassive; + + // Use our detect's results. passive applied if supported, capture will be false either way. + // elem.addEventListener('touchstart', fn, supportsPassive ? { passive: true } : false); + } + + static setImmediate(fn: () => any): void { + Promise.resolve().then(fn); + } +} diff --git a/src/app/VideoSettings.ts b/src/app/VideoSettings.ts new file mode 100644 index 0000000..5ca9785 --- /dev/null +++ b/src/app/VideoSettings.ts @@ -0,0 +1,228 @@ +import Rect from './Rect'; +import Size from './Size'; +import Util from './Util'; + +interface Settings { + crop?: Rect | null; + bitrate: number; + bounds?: Size | null; + maxFps: number; + iFrameInterval: number; + sendFrameMeta?: boolean; + lockedVideoOrientation?: number; + displayId?: number; + codecOptions?: string; + encoderName?: string; +} + +export default class VideoSettings { + public static readonly BASE_BUFFER_LENGTH: number = 35; + public readonly crop?: Rect | null = null; + public readonly bitrate: number = 0; + public readonly bounds?: Size | null = null; + public readonly maxFps: number = 0; + public readonly iFrameInterval: number = 0; + public readonly sendFrameMeta: boolean = false; + public readonly lockedVideoOrientation: number = -1; + public readonly displayId: number = 0; + public readonly codecOptions?: string; + public readonly encoderName?: string; + + constructor(data?: Settings, public readonly bytesLength: number = VideoSettings.BASE_BUFFER_LENGTH) { + if (data) { + this.crop = data.crop; + this.bitrate = data.bitrate; + this.bounds = data.bounds; + this.maxFps = data.maxFps; + this.iFrameInterval = data.iFrameInterval; + this.sendFrameMeta = data.sendFrameMeta || false; + this.lockedVideoOrientation = data.lockedVideoOrientation || -1; + if (typeof data.displayId === 'number' && !isNaN(data.displayId) && data.displayId >= 0) { + this.displayId = data.displayId; + } + if (data.codecOptions) { + this.codecOptions = data.codecOptions.trim(); + } + if (data.encoderName) { + this.encoderName = data.encoderName.trim(); + } + } + } + + public static fromBuffer(buffer: Buffer): VideoSettings { + let offset = 0; + const bitrate = buffer.readInt32BE(offset); + offset += 4; + const maxFps = buffer.readInt32BE(offset); + offset += 4; + const iFrameInterval = buffer.readInt8(offset); + offset += 1; + const width = buffer.readInt16BE(offset); + offset += 2; + const height = buffer.readInt16BE(offset); + offset += 2; + const left = buffer.readInt16BE(offset); + offset += 2; + const top = buffer.readInt16BE(offset); + offset += 2; + const right = buffer.readInt16BE(offset); + offset += 2; + const bottom = buffer.readInt16BE(offset); + offset += 2; + const sendFrameMeta = !!buffer.readInt8(offset); + offset += 1; + const lockedVideoOrientation = buffer.readInt8(offset); + offset += 1; + const displayId = buffer.readInt32BE(offset); + offset += 4; + let bounds: Size | null = null; + let crop: Rect | null = null; + if (width !== 0 && height !== 0) { + bounds = new Size(width, height); + } + if (left || top || right || bottom) { + crop = new Rect(left, top, right, bottom); + } + let codecOptions; + let encoderName; + const codecOptionsLength = buffer.readInt32BE(offset); + offset += 4; + if (codecOptionsLength) { + const codecOptionsBytes = buffer.slice(offset, offset + codecOptionsLength); + offset += codecOptionsLength; + codecOptions = Util.utf8ByteArrayToString(codecOptionsBytes); + } + const encoderNameLength = buffer.readInt32BE(offset); + offset += 4; + if (encoderNameLength) { + const encoderNameBytes = buffer.slice(offset, offset + encoderNameLength); + offset += encoderNameLength; + encoderName = Util.utf8ByteArrayToString(encoderNameBytes); + } + return new VideoSettings( + { + crop, + bitrate, + bounds, + maxFps, + iFrameInterval, + lockedVideoOrientation, + displayId, + sendFrameMeta, + codecOptions, + encoderName, + }, + offset, + ); + } + + public static copy(a: VideoSettings): VideoSettings { + return new VideoSettings( + { + bitrate: a.bitrate, + crop: Rect.copy(a.crop), + bounds: Size.copy(a.bounds), + maxFps: a.maxFps, + iFrameInterval: a.iFrameInterval, + lockedVideoOrientation: a.lockedVideoOrientation, + displayId: a.displayId, + sendFrameMeta: a.sendFrameMeta, + codecOptions: a.codecOptions, + encoderName: a.encoderName, + }, + a.bytesLength, + ); + } + + public equals(o?: VideoSettings | null): boolean { + if (!o) { + return false; + } + return ( + this.encoderName === o.encoderName && + this.codecOptions === o.codecOptions && + Rect.equals(this.crop, o.crop) && + this.lockedVideoOrientation === o.lockedVideoOrientation && + this.displayId === o.displayId && + Size.equals(this.bounds, o.bounds) && + this.bitrate === o.bitrate && + this.maxFps === o.maxFps && + this.iFrameInterval === o.iFrameInterval + ); + } + + public toBuffer(): Buffer { + let additionalLength = 0; + let codecOptionsBytes; + let encoderNameBytes; + if (this.codecOptions) { + codecOptionsBytes = Util.stringToUtf8ByteArray(this.codecOptions); + additionalLength += codecOptionsBytes.length; + } + if (this.encoderName) { + encoderNameBytes = Util.stringToUtf8ByteArray(this.encoderName); + additionalLength += encoderNameBytes.length; + } + const buffer = Buffer.alloc(VideoSettings.BASE_BUFFER_LENGTH + additionalLength); + const { width = 0, height = 0 } = this.bounds || {}; + const { left = 0, top = 0, right = 0, bottom = 0 } = this.crop || {}; + let offset = 0; + offset = buffer.writeInt32BE(this.bitrate, offset); + offset = buffer.writeInt32BE(this.maxFps, offset); + offset = buffer.writeInt8(this.iFrameInterval, offset); + offset = buffer.writeInt16BE(width, offset); + offset = buffer.writeInt16BE(height, offset); + offset = buffer.writeInt16BE(left, offset); + offset = buffer.writeInt16BE(top, offset); + offset = buffer.writeInt16BE(right, offset); + offset = buffer.writeInt16BE(bottom, offset); + offset = buffer.writeInt8(this.sendFrameMeta ? 1 : 0, offset); + offset = buffer.writeInt8(this.lockedVideoOrientation, offset); + offset = buffer.writeInt32BE(this.displayId, offset); + if (codecOptionsBytes) { + offset = buffer.writeInt32BE(codecOptionsBytes.length, offset); + buffer.fill(codecOptionsBytes, offset); + offset += codecOptionsBytes.length; + } else { + offset = buffer.writeInt32BE(0, offset); + } + if (encoderNameBytes) { + offset = buffer.writeInt32BE(encoderNameBytes.length, offset); + buffer.fill(encoderNameBytes, offset); + offset += encoderNameBytes.length; + } else { + buffer.writeInt32BE(0, offset); + } + return buffer; + } + + public toString(): string { + // prettier-ignore + return `VideoSettings{bitrate=${ + this.bitrate}, maxFps=${ + this.maxFps}, iFrameInterval=${ + this.iFrameInterval}, bounds=${ + this.bounds}, crop=${ + this.crop}, metaFrame=${ + this.sendFrameMeta}, lockedVideoOrientation=${ + this.lockedVideoOrientation}, displayId=${ + this.displayId}, codecOptions=${ + this.codecOptions}, encoderName=${ + this.encoderName}}`; + } + + public toJSON(): Settings { + return { + bitrate: this.bitrate, + maxFps: this.maxFps, + iFrameInterval: this.iFrameInterval, + bounds: this.bounds, + crop: this.crop, + sendFrameMeta: this.sendFrameMeta, + lockedVideoOrientation: this.lockedVideoOrientation, + displayId: this.displayId, + codecOptions: this.codecOptions, + encoderName: this.encoderName, + }; + } +} diff --git a/src/app/applDevice/client/DeviceTracker.ts b/src/app/applDevice/client/DeviceTracker.ts new file mode 100644 index 0000000..1025efe --- /dev/null +++ b/src/app/applDevice/client/DeviceTracker.ts @@ -0,0 +1,80 @@ +import { BaseDeviceTracker } from '../../client/BaseDeviceTracker'; +import { ACTION } from '../../../common/Action'; +import ApplDeviceDescriptor from '../../../types/ApplDeviceDescriptor'; +import Util from '../../Util'; +import { html } from '../../ui/HtmlTag'; +import { DeviceState } from '../../../common/DeviceState'; +import { HostItem } from '../../../types/Configuration'; +import { ChannelCode } from '../../../common/ChannelCode'; +import { Tool } from '../../client/Tool'; + +export class DeviceTracker extends BaseDeviceTracker { + public static ACTION = ACTION.APPL_DEVICE_LIST; + protected static tools: Set = new Set(); + private static instancesByUrl: Map = new Map(); + + public static start(hostItem: HostItem): DeviceTracker { + const url = this.buildUrlForTracker(hostItem).toString(); + let instance = this.instancesByUrl.get(url); + if (!instance) { + instance = new DeviceTracker(hostItem, url); + } + return instance; + } + + public static getInstance(hostItem: HostItem): DeviceTracker { + return this.start(hostItem); + } + protected tableId = 'appl_devices_list'; + constructor(params: HostItem, directUrl: string) { + super({ ...params, action: DeviceTracker.ACTION }, directUrl); + DeviceTracker.instancesByUrl.set(directUrl, this); + this.buildDeviceTable(); + this.openNewConnection(); + } + + protected onSocketOpen(): void { + // do nothing; + } + + protected buildDeviceRow(tbody: Element, device: ApplDeviceDescriptor): void { + const blockClass = 'desc-block'; + const fullName = `${this.id}_${Util.escapeUdid(device.udid)}`; + const isActive = device.state === DeviceState.CONNECTED; + const servicesId = `device_services_${fullName}`; + const row = html`
+
+
"${device.name}"
+
${device.model}
+
${device.udid}
+
+
${device.version}
+
+
+
+
+
`.content; + const services = row.getElementById(servicesId); + if (!services) { + return; + } + + DeviceTracker.tools.forEach((tool) => { + const entry = tool.createEntryForDeviceList(device, blockClass, this.params); + if (entry) { + if (Array.isArray(entry)) { + entry.forEach((item) => { + item && services.appendChild(item); + }); + } else { + services.appendChild(entry); + } + } + }); + tbody.appendChild(row); + } + + protected getChannelCode(): string { + return ChannelCode.ATRC; + } +} diff --git a/src/app/applDevice/client/StreamClient.ts b/src/app/applDevice/client/StreamClient.ts new file mode 100644 index 0000000..ed229f4 --- /dev/null +++ b/src/app/applDevice/client/StreamClient.ts @@ -0,0 +1,247 @@ +import { BaseClient } from '../../client/BaseClient'; +import { ParamsStream } from '../../../types/ParamsStream'; +import { SimpleInteractionHandler } from '../../interactionHandler/SimpleInteractionHandler'; +import { BasePlayer, PlayerClass } from '../../player/BasePlayer'; +import ScreenInfo from '../../ScreenInfo'; +import { WdaProxyClient } from './WdaProxyClient'; +import { ACTION } from '../../../common/Action'; +import { ApplMoreBox } from '../toolbox/ApplMoreBox'; +import { ApplToolBox } from '../toolbox/ApplToolBox'; +import Size from '../../Size'; +import Util from '../../Util'; +import ApplDeviceDescriptor from '../../../types/ApplDeviceDescriptor'; +import { ParamsDeviceTracker } from '../../../types/ParamsDeviceTracker'; +import { DeviceTracker } from './DeviceTracker'; +import { WdaStatus } from '../../../common/WdaStatus'; +import { MessageRunWdaResponse } from '../../../types/MessageRunWdaResponse'; + +const WAIT_CLASS = 'wait'; +const TAG = 'StreamClient'; + +export interface StreamClientEvents { + 'wda:status': WdaStatus; +} + +export abstract class StreamClient extends BaseClient { + public static ACTION = 'MUST_OVERRIDE'; + protected static players: Map = new Map(); + + public static registerPlayer(playerClass: PlayerClass): void { + if (playerClass.isSupported()) { + this.players.set(playerClass.playerFullName, playerClass); + } + } + + public static getPlayers(): PlayerClass[] { + return Array.from(this.players.values()); + } + + private static getPlayerClass(playerName?: string): PlayerClass | undefined { + let playerClass: PlayerClass | undefined; + for (const value of this.players.values()) { + if (value.playerFullName === playerName || value.playerCodeName === playerName) { + playerClass = value; + } + } + return playerClass; + } + + public static createPlayer(udid: string, playerName?: string): BasePlayer { + if (!playerName) { + throw Error('Must provide player name'); + } + const playerClass = this.getPlayerClass(playerName); + if (!playerClass) { + throw Error(`Unsupported player "${playerName}"`); + } + return new playerClass(udid); + } + + public static createEntryForDeviceList( + descriptor: ApplDeviceDescriptor, + blockClass: string, + params: ParamsDeviceTracker, + ): Array { + const entries: Array = []; + const players = this.getPlayers(); + players.forEach((playerClass) => { + const { playerCodeName, playerFullName } = playerClass; + const playerTd = document.createElement('div'); + playerTd.classList.add(blockClass); + playerTd.setAttribute(DeviceTracker.AttributePlayerFullName, encodeURIComponent(playerFullName)); + playerTd.setAttribute(DeviceTracker.AttributePlayerCodeName, encodeURIComponent(playerCodeName)); + const q: any = { + action: this.ACTION, + player: playerCodeName, + udid: descriptor.udid, + }; + const link = DeviceTracker.buildLink(q, `Stream (${playerFullName})`, params); + playerTd.appendChild(link); + entries.push(playerTd); + }); + return entries; + } + + protected static getMaxSize(controlButtons: HTMLElement): Size | undefined { + if (!controlButtons) { + return; + } + const body = document.body; + const width = (body.clientWidth - controlButtons.clientWidth) & ~15; + const height = body.clientHeight & ~15; + return new Size(width, height); + } + + private waitForWda?: Promise; + protected touchHandler?: SimpleInteractionHandler; + protected readonly wdaProxy: WdaProxyClient; + protected name: string; + protected udid: string; + protected deviceName = ''; + protected videoWrapper: HTMLElement; + protected deviceView?: HTMLDivElement; + protected moreBox?: HTMLElement; + protected player?: BasePlayer; + + protected constructor(params: T) { + super(params); + this.udid = this.params.udid; + this.wdaProxy = new WdaProxyClient({ ...this.params, action: ACTION.PROXY_WDA }); + this.name = `[${TAG}:${this.udid}]`; + this.videoWrapper = document.createElement('div'); + this.videoWrapper.className = `video`; + this.setWdaStatusNotification(WdaStatus.STARTING); + } + + public static get action(): string { + return StreamClient.ACTION; + } + + public static parseParameters(params: URLSearchParams): ParamsStream { + const typedParams = super.parseParameters(params); + const { action } = typedParams; + if (action !== this.action) { + throw Error('Incorrect action'); + } + return { + ...typedParams, + action, + udid: Util.parseString(params, 'udid', true), + player: Util.parseString(params, 'player', true), + }; + } + + public createPlayer(udid: string, playerName?: string): BasePlayer { + return StreamClient.createPlayer(udid, playerName); + } + + public getMaxSize(controlButtons: HTMLElement): Size | undefined { + return StreamClient.getMaxSize(controlButtons); + } + + protected async runWebDriverAgent(): Promise { + if (!this.waitForWda) { + this.wdaProxy.on('wda-status', this.handleWdaStatus); + this.waitForWda = this.wdaProxy.runWebDriverAgent().then(this.handleWdaStatus); + } + return this.waitForWda; + } + + protected handleWdaStatus = (message: MessageRunWdaResponse): void => { + const data = message.data; + this.setWdaStatusNotification(data.status); + switch (data.status) { + case WdaStatus.STARTING: + case WdaStatus.STARTED: + case WdaStatus.STOPPED: + this.emit('wda:status', data.status); + break; + default: + throw Error(`Unknown WDA status: '${status}'`); + } + }; + + protected setTouchListeners(player: BasePlayer): void { + if (this.touchHandler) { + return; + } + this.touchHandler = new SimpleInteractionHandler(player, this.wdaProxy); + } + + protected onInputVideoResize = (screenInfo: ScreenInfo): void => { + this.wdaProxy.setScreenInfo(screenInfo); + }; + + public onStop(ev?: string | Event): void { + if (ev && ev instanceof Event && ev.type === 'error') { + console.error(TAG, ev); + } + if (this.deviceView) { + const parent = this.deviceView.parentElement; + if (parent) { + parent.removeChild(this.deviceView); + } + } + if (this.moreBox) { + const parent = this.moreBox.parentElement; + if (parent) { + parent.removeChild(this.moreBox); + } + } + this.wdaProxy.stop(); + this.player?.stop(); + } + + public setWdaStatusNotification(status: WdaStatus): void { + // TODO: use proper notification instead of `cursor: wait` + if (status === WdaStatus.STARTED || status === WdaStatus.STOPPED) { + this.videoWrapper.classList.remove(WAIT_CLASS); + } else { + this.videoWrapper.classList.add(WAIT_CLASS); + } + } + + protected createMoreBox(udid: string, player: BasePlayer): ApplMoreBox { + return new ApplMoreBox(udid, player, this.wdaProxy); + } + + protected startStream(inputPlayer?: BasePlayer): void { + const { udid, player: playerName } = this.params; + if (!udid) { + throw Error(`Invalid udid value: "${udid}"`); + } + let player: BasePlayer; + if (inputPlayer) { + player = inputPlayer; + } else { + player = this.createPlayer(udid, playerName); + } + this.setTouchListeners(player); + player.pause(); + + const deviceView = document.createElement('div'); + deviceView.className = 'device-view'; + + const applMoreBox = this.createMoreBox(udid, player); + applMoreBox.setOnStop(this); + const moreBox: HTMLElement = applMoreBox.getHolderElement(); + const applToolBox = ApplToolBox.createToolBox(udid, player, this, this.wdaProxy, moreBox); + const controlButtons = applToolBox.getHolderElement(); + deviceView.appendChild(controlButtons); + deviceView.appendChild(this.videoWrapper); + deviceView.appendChild(moreBox); + player.setParent(this.videoWrapper); + player.on('input-video-resize', this.onInputVideoResize); + + document.body.appendChild(deviceView); + const bounds = this.getMaxSize(controlButtons); + if (bounds) { + player.setBounds(bounds); + } + this.player = player; + } + + public getDeviceName(): string { + return this.deviceName; + } +} diff --git a/src/app/applDevice/client/StreamClientMJPEG.ts b/src/app/applDevice/client/StreamClientMJPEG.ts new file mode 100644 index 0000000..8bba36c --- /dev/null +++ b/src/app/applDevice/client/StreamClientMJPEG.ts @@ -0,0 +1,50 @@ +import { ParamsStream } from '../../../types/ParamsStream'; +import { ACTION } from '../../../common/Action'; +import { StreamClient } from './StreamClient'; +import { BasePlayer, PlayerClass } from '../../player/BasePlayer'; +import { WdaStatus } from '../../../common/WdaStatus'; +import { ApplMjpegMoreBox } from '../toolbox/ApplMjpegMoreBox'; + +const TAG = '[StreamClientMJPEG]'; + +export class StreamClientMJPEG extends StreamClient { + public static ACTION = ACTION.STREAM_MJPEG; + protected static players: Map = new Map(); + + public static start(params: ParamsStream): StreamClientMJPEG { + return new StreamClientMJPEG(params); + } + + constructor(params: ParamsStream) { + super(params); + this.name = `[${TAG}:${this.udid}]`; + this.udid = this.params.udid; + this.runWebDriverAgent().then(() => { + this.startStream(); + this.player?.play(); + }); + this.on('wda:status', (status) => { + if (status === WdaStatus.STOPPED) { + this.player?.stop(); + } else if (status === WdaStatus.STARTED) { + this.player?.play(); + } + }); + } + + public static get action(): string { + return StreamClientMJPEG.ACTION; + } + + public createPlayer(udid: string, playerName?: string): BasePlayer { + return StreamClientMJPEG.createPlayer(udid, playerName); + } + + public getDeviceName(): string { + return this.deviceName; + } + + protected createMoreBox(udid: string, player: BasePlayer): ApplMjpegMoreBox { + return new ApplMjpegMoreBox(udid, player, this.wdaProxy); + } +} diff --git a/src/app/applDevice/client/StreamClientQVHack.ts b/src/app/applDevice/client/StreamClientQVHack.ts new file mode 100644 index 0000000..f4117c3 --- /dev/null +++ b/src/app/applDevice/client/StreamClientQVHack.ts @@ -0,0 +1,72 @@ +import { StreamReceiver } from '../../client/StreamReceiver'; +import { BasePlayer, PlayerClass } from '../../player/BasePlayer'; +import { ACTION } from '../../../common/Action'; +import { StreamReceiverQVHack } from './StreamReceiverQVHack'; +import { StreamClient } from './StreamClient'; +import { ParamsStream } from '../../../types/ParamsStream'; + +const TAG = '[StreamClientQVHack]'; + +export class StreamClientQVHack extends StreamClient { + public static ACTION = ACTION.STREAM_WS_QVH; + protected static players: Map = new Map(); + + public static start(params: ParamsStream): StreamClientQVHack { + return new StreamClientQVHack(params); + } + + private readonly streamReceiver: StreamReceiver; + + constructor(params: ParamsStream) { + super(params); + + this.name = `[${TAG}:${this.udid}]`; + this.udid = this.params.udid; + let udid = this.udid; + // Workaround for qvh v0.5-beta + if (udid.indexOf('-') !== -1) { + udid = udid.replace('-', ''); + udid = udid + '\0'.repeat(16); + } + this.streamReceiver = new StreamReceiverQVHack({ ...this.params, udid }); + this.startStream(); + this.setTitle(`${this.udid} stream`); + this.setBodyClass('stream'); + } + + public static get action(): string { + return StreamClientQVHack.ACTION; + } + + public createPlayer(udid: string, playerName?: string): BasePlayer { + return StreamClientQVHack.createPlayer(udid, playerName); + } + + protected onViewVideoResize = (): void => { + this.runWebDriverAgent(); + }; + + public onStop(ev?: string | Event): void { + super.onStop(ev); + this.streamReceiver.stop(); + } + + protected startStream(inputPlayer?: BasePlayer): void { + + console.log("标识", inputPlayer); + super.startStream(inputPlayer); + const player = this.player; + if (player) { + player.on('video-view-resize', this.onViewVideoResize); + this.streamReceiver.on('video', (data) => { + const STATE = BasePlayer.STATE; + if (player.getState() === STATE.PAUSED) { + player.play(); + } + if (player.getState() === STATE.PLAYING) { + player.pushFrame(new Uint8Array(data)); + } + }); + } + } +} diff --git a/src/app/applDevice/client/StreamReceiverQVHack.ts b/src/app/applDevice/client/StreamReceiverQVHack.ts new file mode 100644 index 0000000..010144f --- /dev/null +++ b/src/app/applDevice/client/StreamReceiverQVHack.ts @@ -0,0 +1,34 @@ +import { StreamReceiver } from '../../client/StreamReceiver'; +import { ACTION } from '../../../common/Action'; +import Util from '../../Util'; +import { ParamsStream } from '../../../types/ParamsStream'; +import { ChannelCode } from '../../../common/ChannelCode'; + +export class StreamReceiverQVHack extends StreamReceiver { + public static parseParameters(params: URLSearchParams): ParamsStream { + const typedParams = super.parseParameters(params); + const { action } = typedParams; + if (action !== ACTION.STREAM_WS_QVH) { + throw Error('Incorrect action'); + } + return { + ...typedParams, + action, + player: Util.parseString(params, 'player', true), + udid: Util.parseString(params, 'udid', true), + }; + } + + protected supportMultiplexing(): boolean { + return true; + } + + protected getChannelInitData(): Buffer { + const udid = Util.stringToUtf8ByteArray(this.params.udid); + const buffer = Buffer.alloc(4 + 4 + udid.byteLength); + buffer.write(ChannelCode.QVHS, 'ascii'); + buffer.writeUInt32LE(udid.length, 4); + buffer.set(udid, 8); + return buffer; + } +} diff --git a/src/app/applDevice/client/WdaProxyClient.ts b/src/app/applDevice/client/WdaProxyClient.ts new file mode 100644 index 0000000..f0ebf7a --- /dev/null +++ b/src/app/applDevice/client/WdaProxyClient.ts @@ -0,0 +1,309 @@ +import { ManagerClient } from '../../client/ManagerClient'; +import { MessageRunWdaResponse } from '../../../types/MessageRunWdaResponse'; +import { Message } from '../../../types/Message'; +import { ControlCenterCommand } from '../../../common/ControlCenterCommand'; +import { ParamsWdaProxy } from '../../../types/ParamsWdaProxy'; +import { ACTION } from '../../../common/Action'; +import Util from '../../Util'; +import { ChannelCode } from '../../../common/ChannelCode'; +import { WDAMethod } from '../../../common/WDAMethod'; +import ScreenInfo from '../../ScreenInfo'; +import Position from '../../Position'; +import Point from '../../Point'; +import { TouchHandlerListener } from '../../interactionHandler/SimpleInteractionHandler'; + +export type WdaProxyClientEvents = { + 'wda-status': MessageRunWdaResponse; + connected: boolean; +}; + +export type MjpegServerOptions = { + // The maximum count of screenshots per second taken by the MJPEG screenshots broadcaster. + // Must be in range 1..60. 10 by default + mjpegServerFramerate?: number; + + // The percentage value used to apply downscaling on the screenshots generated by the MJPEG screenshots + // broadcaster. Must be in range 1..100. 100 is by default, which means that screenshots are not downscaled. + mjpegScalingFactor?: number; + + // The percentage value used to apply lossy JPEG compression on the screenshots generated by the MJPEG + // screenshots broadcaster. Must be in range 1..100. 25 is by default, which means that screenshots are + // compressed to the quarter of their original quality. + mjpegServerScreenshotQuality?: number; +}; + +export const DefaultMjpegServerOption: MjpegServerOptions = { + mjpegServerFramerate: 10, + mjpegScalingFactor: 100, + mjpegServerScreenshotQuality: 25, +}; + +const TAG = '[WdaProxyClient]'; + +export class WdaProxyClient + extends ManagerClient + implements TouchHandlerListener { + public static calculatePhysicalPoint( + screenInfo: ScreenInfo, + screenWidth: number, + position: Position, + ): Point | undefined { + // ignore the locked video orientation, the events will apply in coordinates considered in the physical device orientation + const { videoSize, deviceRotation, contentRect } = screenInfo; + const { right, left, bottom, top } = contentRect; + let shortSide: number; + if (videoSize.width >= videoSize.height) { + shortSide = bottom - top; + } else { + shortSide = right - left; + } + const scale = shortSide / screenWidth; + + // reverse the video rotation to apply the events + const devicePosition = position.rotate(deviceRotation); + + if (!videoSize.equals(devicePosition.screenSize)) { + // The client sends a click relative to a video with wrong dimensions, + // the device may have been rotated since the event was generated, so ignore the event + return; + } + const { point } = devicePosition; + const convertedX = contentRect.left + (point.x * contentRect.getWidth()) / videoSize.width; + const convertedY = contentRect.top + (point.y * contentRect.getHeight()) / videoSize.height; + + const scaledX = Math.round(convertedX / scale); + const scaledY = Math.round(convertedY / scale); + + return new Point(scaledX, scaledY); + } + + private screenInfo?: ScreenInfo; + private screenWidth = 0; + private udid: string; + private stopped = false; + private commands: string[] = []; + private hasSession = false; + private messageId = 0; + private wait: Map void; reject: (error: any) => void }> = new Map(); + + constructor(params: ParamsWdaProxy) { + super(params); + this.openNewConnection(); + this.udid = params.udid; + } + + public static parseParameters(params: URLSearchParams): ParamsWdaProxy { + const typedParams = super.parseParameters(params); + const { action } = typedParams; + if (action !== ACTION.PROXY_WDA) { + throw Error('Incorrect action'); + } + return { ...typedParams, action, udid: Util.parseString(params, 'udid', true) }; + } + + protected onSocketClose(event: CloseEvent): void { + this.emit('connected', false); + console.log(TAG, `Connection closed: ${event.reason}`); + if (!this.stopped) { + setTimeout(() => { + this.openNewConnection(); + }, 2000); + } + } + + protected onSocketMessage(event: MessageEvent): void { + console.log("接收到的参数03", event.data) + + new Response(event.data) + .text() + .then((text: string) => { + const json = JSON.parse(text) as Message; + const id = json['id']; + const p = this.wait.get(id); + if (p) { + this.wait.delete(id); + p.resolve(json); + return; + } + switch (json['type']) { + case ControlCenterCommand.RUN_WDA: + this.emit('wda-status', json as MessageRunWdaResponse); + return; + default: + throw Error('Unsupported message'); + } + }) + .catch((error: Error) => { + console.error(TAG, error.message); + console.log(TAG, event.data); + }); + } + + protected onSocketOpen(): void { + this.emit('connected', true); + while (this.commands.length) { + const str = this.commands.shift(); + if (str) { + this.sendCommand(str); + } + } + } + + private sendCommand(str: string): void { + if (this.ws && this.ws.readyState === this.ws.OPEN) { + console.log('发送的参数1'); + this.ws.send(str); + } else { + this.commands.push(str); + } + } + + private getNextId(): number { + return ++this.messageId; + } + + public async sendMessage(message: Message): Promise { + this.sendCommand(JSON.stringify(message)); + return new Promise((resolve, reject) => { + this.wait.set(message.id, { resolve, reject }); + }); + } + + public setScreenInfo(screenInfo: ScreenInfo): void { + this.screenInfo = screenInfo; + } + + public getScreenInfo(): ScreenInfo | undefined { + return this.screenInfo; + } + + private async getScreenWidth(): Promise { + if (this.screenWidth) { + return this.screenWidth; + } + const temp = await this.requestWebDriverAgent(WDAMethod.GET_SCREEN_WIDTH); + if (temp.data.success && typeof temp.data.response === 'number') { + return (this.screenWidth = temp.data.response); + } + throw Error('Invalid response'); + } + + public async setMjpegServerOptions(opts: MjpegServerOptions): Promise { + const { mjpegServerFramerate, mjpegScalingFactor, mjpegServerScreenshotQuality } = opts; + const options: MjpegServerOptions = { + mjpegServerFramerate, + mjpegScalingFactor, + mjpegServerScreenshotQuality, + }; + if (!mjpegServerFramerate || isNaN(mjpegServerFramerate)) { + options.mjpegServerFramerate = DefaultMjpegServerOption.mjpegServerFramerate; + } + if (!mjpegScalingFactor || isNaN(mjpegScalingFactor)) { + options.mjpegScalingFactor = DefaultMjpegServerOption.mjpegScalingFactor; + } + if (!mjpegServerScreenshotQuality || isNaN(mjpegServerScreenshotQuality)) { + options.mjpegServerScreenshotQuality = DefaultMjpegServerOption.mjpegServerScreenshotQuality; + } + return this.requestWebDriverAgent(WDAMethod.APPIUM_SETTINGS, { options }); + } + + public async sendKeys(keys: string): Promise { + return this.requestWebDriverAgent(WDAMethod.SEND_KEYS, { + keys, + }); + } + + public async pressButton(name: string): Promise { + return this.requestWebDriverAgent(WDAMethod.PRESS_BUTTON, { + name, + }); + } + + public async performClick(position: Position): Promise { + if (!this.screenInfo) { + return; + } + const screenWidth = this.screenWidth || (await this.getScreenWidth()); + const point = WdaProxyClient.calculatePhysicalPoint(this.screenInfo, screenWidth, position); + if (!point) { + return; + } + return this.requestWebDriverAgent(WDAMethod.CLICK, { + x: point.x, + y: point.y, + }); + } + + public async performScroll(from: Position, to: Position): Promise { + if (!this.screenInfo) { + return; + } + const wdaScreen = this.screenWidth || (await this.getScreenWidth()); + const fromPoint = WdaProxyClient.calculatePhysicalPoint(this.screenInfo, wdaScreen, from); + const toPoint = WdaProxyClient.calculatePhysicalPoint(this.screenInfo, wdaScreen, to); + if (!fromPoint || !toPoint) { + return; + } + return this.requestWebDriverAgent(WDAMethod.SCROLL, { + from: { + x: fromPoint.x, + y: fromPoint.y, + }, + to: { + x: toPoint.x, + y: toPoint.y, + }, + }); + } + + public async runWebDriverAgent(): Promise { + const message: Message = { + id: this.getNextId(), + type: ControlCenterCommand.RUN_WDA, + data: { + udid: this.udid, + }, + }; + const response = await this.sendMessage(message); + this.hasSession = true; + return response as MessageRunWdaResponse; + } + + public async requestWebDriverAgent(method: WDAMethod, args?: any): Promise { + if (!this.hasSession) { + throw Error('No session'); + } + const message: Message = { + id: this.getNextId(), + type: ControlCenterCommand.REQUEST_WDA, + data: { + method, + args, + }, + }; + return this.sendMessage(message); + } + + protected supportMultiplexing(): boolean { + return true; + } + + protected getChannelInitData(): Buffer { + const udid = Util.stringToUtf8ByteArray(this.params.udid); + const buffer = Buffer.alloc(4 + 4 + udid.byteLength); + buffer.write(ChannelCode.WDAP, 'ascii'); + buffer.writeUInt32LE(udid.length, 4); + buffer.set(udid, 8); + return buffer; + } + + public stop(): void { + if (this.stopped) { + return; + } + this.stopped = true; + if (this.ws && this.ws.readyState === this.ws.OPEN) { + this.ws.close(); + } + } +} diff --git a/src/app/applDevice/toolbox/ApplMjpegMoreBox.ts b/src/app/applDevice/toolbox/ApplMjpegMoreBox.ts new file mode 100644 index 0000000..52736bc --- /dev/null +++ b/src/app/applDevice/toolbox/ApplMjpegMoreBox.ts @@ -0,0 +1,81 @@ +import { ApplMoreBox } from './ApplMoreBox'; +import { BasePlayer } from '../../player/BasePlayer'; +import { DefaultMjpegServerOption, MjpegServerOptions, WdaProxyClient } from '../client/WdaProxyClient'; + +export class ApplMjpegMoreBox extends ApplMoreBox { + private readonly framerateInput: HTMLInputElement; + private readonly scalingFactorInput: HTMLInputElement; + private readonly qualityInput: HTMLInputElement; + + constructor(udid: string, player: BasePlayer, wdaConnection: WdaProxyClient) { + super(udid, player, wdaConnection); + + const action = 'CHANGE_PARAMS'; + const text = 'Change stream parameters'; + const playerName = player.getName(); + const spoiler = document.createElement('div'); + const spoilerLabel = document.createElement('label'); + const spoilerCheck = document.createElement('input'); + + const innerDiv = document.createElement('div'); + const id = `spoiler_video_${udid}_${playerName}_${action}`; + const btn = document.createElement('button'); + + spoiler.className = 'spoiler'; + spoilerCheck.type = 'checkbox'; + spoilerCheck.id = id; + spoilerLabel.htmlFor = id; + spoilerLabel.innerText = text; + innerDiv.className = 'box'; + spoiler.appendChild(spoilerCheck); + spoiler.appendChild(spoilerLabel); + spoiler.appendChild(innerDiv); + const defaultOptions = DefaultMjpegServerOption; + + const framerateLabel = document.createElement('label'); + framerateLabel.innerText = 'Framerate:'; + const framerateInput = document.createElement('input'); + framerateInput.placeholder = `1 .. 60`; + framerateInput.value = `${defaultOptions.mjpegServerFramerate}`; + ApplMjpegMoreBox.wrap('div', [framerateLabel, framerateInput], innerDiv); + this.framerateInput = framerateInput; + + const scalingFactorLabel = document.createElement('label'); + scalingFactorLabel.innerText = 'Scaling factor:'; + const scalingFactorInput = document.createElement('input'); + scalingFactorInput.placeholder = `1 .. 100`; + scalingFactorInput.value = `${defaultOptions.mjpegScalingFactor}`; + const scalingWrapper = ApplMjpegMoreBox.wrap('div', [scalingFactorLabel, scalingFactorInput], innerDiv); + // FIXME: scaling factor changes are not handled + scalingWrapper.style.display = 'none'; + this.scalingFactorInput = scalingFactorInput; + + const qualityLabel = document.createElement('label'); + qualityLabel.innerText = 'Quality:'; + const qualityInput = document.createElement('input'); + qualityInput.placeholder = `1 .. 100`; + qualityInput.value = `${defaultOptions.mjpegServerScreenshotQuality}`; + ApplMjpegMoreBox.wrap('div', [qualityLabel, qualityInput], innerDiv); + this.qualityInput = qualityInput; + innerDiv.appendChild(btn); + btn.innerText = text; + btn.onclick = () => { + const mjpegServerFramerate = parseInt(this.framerateInput.value, 10); + const mjpegScalingFactor = parseInt(this.scalingFactorInput.value, 10); + const mjpegServerScreenshotQuality = parseInt(this.qualityInput.value, 10); + const options: MjpegServerOptions = { + mjpegServerFramerate, + mjpegScalingFactor, + mjpegServerScreenshotQuality, + }; + wdaConnection.setMjpegServerOptions(options); + }; + const holder = this.getHolderElement(); + const childNodes = holder.childNodes; + if (childNodes.length > 1) { + holder.insertBefore(spoiler, childNodes[childNodes.length - 2]); + } else { + holder.appendChild(spoiler); + } + } +} diff --git a/src/app/applDevice/toolbox/ApplMoreBox.ts b/src/app/applDevice/toolbox/ApplMoreBox.ts new file mode 100644 index 0000000..81d1bd5 --- /dev/null +++ b/src/app/applDevice/toolbox/ApplMoreBox.ts @@ -0,0 +1,94 @@ +import '../../../style/morebox.css'; +import { BasePlayer } from '../../player/BasePlayer'; +import Size from '../../Size'; +import { WdaProxyClient } from '../client/WdaProxyClient'; + +const TAG = '[ApplMoreBox]'; + +interface StopListener { + onStop: () => void; +} + +export class ApplMoreBox { + private stopListener?: StopListener; + private readonly holder: HTMLElement; + + constructor(udid: string, player: BasePlayer, wdaConnection: WdaProxyClient) { + const playerName = player.getName(); + const moreBox = document.createElement('div'); + moreBox.className = 'more-box'; + const nameBox = document.createElement('p'); + nameBox.innerText = `${udid} (${playerName})`; + nameBox.className = 'text-with-shadow'; + moreBox.appendChild(nameBox); + const input = document.createElement('textarea'); + input.classList.add('text-area'); + const sendButton = document.createElement('button'); + sendButton.innerText = 'Send as keys'; + + ApplMoreBox.wrap('p', [input, sendButton], moreBox); + sendButton.onclick = () => { + if (input.value) { + wdaConnection.sendKeys(input.value); + } + }; + + const qualityId = `show_video_quality_${udid}_${playerName}`; + const qualityLabel = document.createElement('label'); + const qualityCheck = document.createElement('input'); + qualityCheck.type = 'checkbox'; + qualityCheck.checked = BasePlayer.DEFAULT_SHOW_QUALITY_STATS; + qualityCheck.id = qualityId; + qualityLabel.htmlFor = qualityId; + qualityLabel.innerText = 'Show quality stats'; + ApplMoreBox.wrap('p', [qualityCheck, qualityLabel], moreBox); + qualityCheck.onchange = () => { + player.setShowQualityStats(qualityCheck.checked); + }; + + const stop = (ev?: string | Event) => { + if (ev && ev instanceof Event && ev.type === 'error') { + console.error(TAG, ev); + } + const parent = moreBox.parentElement; + if (parent) { + parent.removeChild(moreBox); + } + player.off('video-view-resize', this.onViewVideoResize); + if (this.stopListener) { + this.stopListener.onStop(); + delete this.stopListener; + } + }; + + const stopBtn = document.createElement('button') as HTMLButtonElement; + stopBtn.innerText = `Disconnect`; + stopBtn.onclick = stop; + + ApplMoreBox.wrap('p', [stopBtn], moreBox); + player.on('video-view-resize', this.onViewVideoResize); + this.holder = moreBox; + } + + private onViewVideoResize = (size: Size): void => { + // padding: 10px + this.holder.style.width = `${size.width - 2 * 10}px`; + }; + + protected static wrap(tagName: string, elements: HTMLElement[], parent: HTMLElement): HTMLElement { + const wrap = document.createElement(tagName); + elements.forEach((e) => { + wrap.appendChild(e); + }); + parent.appendChild(wrap); + return wrap; + } + + public getHolderElement(): HTMLElement { + return this.holder; + } + + public setOnStop(listener: StopListener): void { + this.stopListener = listener; + } +} diff --git a/src/app/applDevice/toolbox/ApplToolBox.ts b/src/app/applDevice/toolbox/ApplToolBox.ts new file mode 100644 index 0000000..acd3a5f --- /dev/null +++ b/src/app/applDevice/toolbox/ApplToolBox.ts @@ -0,0 +1,70 @@ +import { ToolBox } from '../../toolbox/ToolBox'; +import SvgImage from '../../ui/SvgImage'; +import { BasePlayer } from '../../player/BasePlayer'; +import { ToolBoxButton } from '../../toolbox/ToolBoxButton'; +import { ToolBoxElement } from '../../toolbox/ToolBoxElement'; +import { ToolBoxCheckbox } from '../../toolbox/ToolBoxCheckbox'; +import { WdaProxyClient } from '../client/WdaProxyClient'; + +const BUTTONS = [ + { + title: 'Home', + name: 'home', + icon: SvgImage.Icon.HOME, + }, +]; + +export interface StreamClient { + getDeviceName(): string; +} + +export class ApplToolBox extends ToolBox { + protected constructor(list: ToolBoxElement[]) { + super(list); + } + + public static createToolBox( + udid: string, + player: BasePlayer, + client: StreamClient, + wdaConnection: WdaProxyClient, + moreBox?: HTMLElement, + ): ApplToolBox { + const playerName = player.getName(); + const list = BUTTONS.slice(); + const handler = ( + _: K, + element: ToolBoxElement, + ) => { + if (!element.optional?.name) { + return; + } + const { name } = element.optional; + wdaConnection.pressButton(name); + }; + const elements: ToolBoxElement[] = list.map((item) => { + const button = new ToolBoxButton(item.title, item.icon, { + name: item.name, + }); + button.addEventListener('click', handler); + return button; + }); + if (player.supportsScreenshot) { + const screenshot = new ToolBoxButton('Take screenshot', SvgImage.Icon.CAMERA); + screenshot.addEventListener('click', () => { + player.createScreenshot(client.getDeviceName()); + }); + elements.push(screenshot); + } + + if (moreBox) { + const more = new ToolBoxCheckbox('More', SvgImage.Icon.MORE, `show_more_${udid}_${playerName}`); + more.addEventListener('click', (_, el) => { + const element = el.getElement(); + moreBox.style.display = element.checked ? 'block' : 'none'; + }); + elements.unshift(more); + } + return new ApplToolBox(elements); + } +} diff --git a/src/app/client/BaseClient.ts b/src/app/client/BaseClient.ts new file mode 100644 index 0000000..423ec57 --- /dev/null +++ b/src/app/client/BaseClient.ts @@ -0,0 +1,40 @@ +import { EventMap, TypedEmitter } from '../../common/TypedEmitter'; +import { ParamsBase } from '../../types/ParamsBase'; +import Util from '../Util'; + +export class BaseClient

extends TypedEmitter { + protected title = 'BaseClient'; + protected params: P; + + protected constructor(params: P) { + super(); + this.params = params; + } + + public static parseParameters(query: URLSearchParams): ParamsBase { + const action = Util.parseStringEnv(query.get('action')); + if (!action) { + throw TypeError('Invalid action'); + } + return { + action: action, + useProxy: Util.parseBooleanEnv(query.get('useProxy')), + secure: Util.parseBooleanEnv(query.get('secure')), + hostname: Util.parseStringEnv(query.get('hostname')), + port: Util.parseIntEnv(query.get('port')), + pathname: Util.parseStringEnv(query.get('pathname')), + }; + } + + public setTitle(text = this.title): void { + let titleTag: HTMLTitleElement | null = document.querySelector('head > title'); + if (!titleTag) { + titleTag = document.createElement('title'); + } + titleTag.innerText = text; + } + + public setBodyClass(text: string): void { + document.body.className = text; + } +} diff --git a/src/app/client/BaseDeviceTracker.ts b/src/app/client/BaseDeviceTracker.ts new file mode 100644 index 0000000..53229eb --- /dev/null +++ b/src/app/client/BaseDeviceTracker.ts @@ -0,0 +1,280 @@ +import { ManagerClient } from './ManagerClient'; +import { Message } from '../../types/Message'; +import { BaseDeviceDescriptor } from '../../types/BaseDeviceDescriptor'; +import { DeviceTrackerEvent } from '../../types/DeviceTrackerEvent'; +import { DeviceTrackerEventList } from '../../types/DeviceTrackerEventList'; +import { html } from '../ui/HtmlTag'; +import { ParamsDeviceTracker } from '../../types/ParamsDeviceTracker'; +import { HostItem } from '../../types/Configuration'; +import { Tool } from './Tool'; +import Util from '../Util'; +import { EventMap } from '../../common/TypedEmitter'; + +const TAG = '[BaseDeviceTracker]'; + +export abstract class BaseDeviceTracker

extends ManagerClient< + ParamsDeviceTracker, + TE +> { + public static readonly ACTION_LIST = 'devicelist'; + public static readonly ACTION_DEVICE = 'device'; + public static readonly HOLDER_ELEMENT_ID = 'devices'; + public static readonly AttributePrefixInterfaceSelectFor = 'interface_select_for_'; + public static readonly AttributePlayerFullName = 'data-player-full-name'; + public static readonly AttributePlayerCodeName = 'data-player-code-name'; + public static readonly AttributePrefixPlayerFor = 'player_for_'; + protected static tools: Set = new Set(); + protected static instanceId = 0; + + public static registerTool(tool: Tool): void { + this.tools.add(tool); + } + + public static buildUrl(item: HostItem): URL { + const { secure, port, hostname } = item; + const pathname = item.pathname ?? '/'; + const protocol = secure ? 'wss:' : 'ws:'; + const url = new URL(`${protocol}//${hostname}${pathname}`); + if (port) { + url.port = port.toString(); + } + return url; + } + + public static buildUrlForTracker(params: HostItem): URL { + const wsUrl = this.buildUrl(params); + wsUrl.searchParams.set('action', this.ACTION); + return wsUrl; + } + + public static buildLink(q: any, text: string, params: ParamsDeviceTracker): HTMLAnchorElement { + let { hostname } = params; + let port: string | number | undefined = params.port; + let pathname = params.pathname ?? location.pathname; + let protocol = params.secure ? 'https:' : 'http:'; + if (params.useProxy) { + q.hostname = hostname; + q.port = port; + q.pathname = pathname; + q.secure = params.secure; + q.useProxy = true; + protocol = location.protocol; + hostname = location.hostname; + port = location.port; + pathname = location.pathname; + } + const hash = `#!${new URLSearchParams(q).toString()}`; + const a = document.createElement('a'); + a.setAttribute('href', `${protocol}//${hostname}:${port}${pathname}${hash}`); + a.setAttribute('rel', 'noopener noreferrer'); + a.setAttribute('target', '_blank'); + a.classList.add(`link-${q.action}`); + a.innerText = text; + return a; + } + + protected title = 'Device list'; + protected tableId = 'base_device_list'; + protected descriptors: DD[] = []; + protected elementId: string; + protected trackerName = ''; + protected id = ''; + private created = false; + private messageId = 0; + + protected constructor(params: ParamsDeviceTracker, protected readonly directUrl: string) { + super(params); + this.elementId = `tracker_instance${++BaseDeviceTracker.instanceId}`; + this.trackerName = `Unavailable. Host: ${params.hostname}, type: ${params.type}`; + this.setBodyClass('list'); + this.setTitle(); + } + + public static parseParameters(params: URLSearchParams): ParamsDeviceTracker { + const typedParams = super.parseParameters(params); + const type = Util.parseString(params, 'type', true); + if (type !== 'android' && type !== 'ios') { + throw Error('Incorrect type'); + } + return { ...typedParams, type }; + } + + protected getNextId(): number { + return ++this.messageId; + } + + protected buildDeviceTable(): void { + const data = this.descriptors; + const devices = this.getOrCreateTableHolder(); + const tbody = this.getOrBuildTableBody(devices); + + const block = this.getOrCreateTrackerBlock(tbody, this.trackerName); + data.forEach((item) => { + this.buildDeviceRow(block, item); + }); + } + + private setNameValue(parent: Element | null, name: string): void { + if (!parent) { + return; + } + const nameBlockId = `${this.elementId}_name`; + let nameEl = document.getElementById(nameBlockId); + if (!nameEl) { + nameEl = document.createElement('div'); + nameEl.id = nameBlockId; + nameEl.className = 'tracker-name'; + } + nameEl.innerText = name; + parent.insertBefore(nameEl, parent.firstChild); + } + + private getOrCreateTrackerBlock(parent: Element, controlCenterName: string): Element { + let el = document.getElementById(this.elementId); + if (!el) { + el = document.createElement('div'); + el.id = this.elementId; + parent.appendChild(el); + this.created = true; + } else { + while (el.children.length) { + el.removeChild(el.children[0]); + } + } + this.setNameValue(el, controlCenterName); + return el; + } + + protected abstract buildDeviceRow(tbody: Element, device: DD): void; + + protected onSocketClose(event: CloseEvent): void { + if (this.destroyed) { + return; + } + console.log(TAG, `Connection closed: ${event.reason}`); + setTimeout(() => { + this.openNewConnection(); + }, 2000); + } + + protected onSocketMessage(event: MessageEvent): void { + console.log("接收到的参数3", event.data) + + let message: Message; + try { + message = JSON.parse(event.data); + } catch (error: any) { + console.error(TAG, error.message); + console.log(TAG, error.data); + return; + } + switch (message.type) { + case BaseDeviceTracker.ACTION_LIST: { + const event = message.data as DeviceTrackerEventList
; + this.descriptors = event.list; + this.setIdAndHostName(event.id, event.name); + this.buildDeviceTable(); + break; + } + case BaseDeviceTracker.ACTION_DEVICE: { + const event = message.data as DeviceTrackerEvent
; + this.setIdAndHostName(event.id, event.name); + this.updateDescriptor(event.device); + this.buildDeviceTable(); + break; + } + default: + console.log(TAG, `Unknown message type: ${message.type}`); + } + } + + protected setIdAndHostName(id: string, trackerName: string): void { + if (this.id === id && this.trackerName === trackerName) { + return; + } + this.id = id; + this.trackerName = trackerName; + this.setNameValue(document.getElementById(this.elementId), trackerName); + } + + protected getOrCreateTableHolder(): HTMLElement { + const id = BaseDeviceTracker.HOLDER_ELEMENT_ID; + let devices = document.getElementById(id); + if (!devices) { + devices = document.createElement('div'); + devices.id = id; + devices.className = 'table-wrapper'; + document.body.appendChild(devices); + } + return devices; + } + + protected updateDescriptor(descriptor: DD): void { + const idx = this.descriptors.findIndex((item: DD) => { + return item.udid === descriptor.udid; + }); + if (idx !== -1) { + this.descriptors[idx] = descriptor; + } else { + this.descriptors.push(descriptor); + } + } + + protected getOrBuildTableBody(parent: HTMLElement): Element { + const className = 'device-list'; + let tbody = document.querySelector( + `#${BaseDeviceTracker.HOLDER_ELEMENT_ID} #${this.tableId}.${className}`, + ) as Element; + if (!tbody) { + const fragment = html`
`.content; + parent.appendChild(fragment); + const last = parent.children.item(parent.children.length - 1); + if (last) { + tbody = last; + } + } + return tbody; + } + + public getDescriptorByUdid(udid: string): DD | undefined { + if (!this.descriptors.length) { + return; + } + return this.descriptors.find((descriptor: DD) => { + return descriptor.udid === udid; + }); + } + + public destroy(): void { + super.destroy(); + if (this.created) { + const el = document.getElementById(this.elementId); + if (el) { + const { parentElement } = el; + el.remove(); + if (parentElement && !parentElement.children.length) { + parentElement.remove(); + } + } + } + const holder = document.getElementById(BaseDeviceTracker.HOLDER_ELEMENT_ID); + if (holder && !holder.children.length) { + holder.remove(); + } + } + + protected supportMultiplexing(): boolean { + return true; + } + + protected getChannelCode(): string { + throw Error('Not implemented. Must override'); + } + + protected getChannelInitData(): Buffer { + const code = this.getChannelCode(); + const buffer = Buffer.alloc(code.length); + buffer.write(code, 'ascii'); + return buffer; + } +} diff --git a/src/app/client/HostTracker.ts b/src/app/client/HostTracker.ts new file mode 100644 index 0000000..05de478 --- /dev/null +++ b/src/app/client/HostTracker.ts @@ -0,0 +1,127 @@ +import { ManagerClient } from './ManagerClient'; +import { Message } from '../../types/Message'; +import { MessageError, MessageHosts, MessageType } from '../../common/HostTrackerMessage'; +import { ACTION } from '../../common/Action'; +import { DeviceTracker as GoogDeviceTracker } from '../googDevice/client/DeviceTracker'; +import { DeviceTracker as ApplDeviceTracker } from '../applDevice/client/DeviceTracker'; +import { ParamsBase } from '../../types/ParamsBase'; +import { HostItem } from '../../types/Configuration'; +import { ChannelCode } from '../../common/ChannelCode'; + +const TAG = '[HostTracker]'; + +export interface HostTrackerEvents { + // hosts: HostItem[]; + disconnected: CloseEvent; + error: string; +} + +export class HostTracker extends ManagerClient { + private static instance?: HostTracker; + + public static start(): void { + this.getInstance(); + } + + public static getInstance(): HostTracker { + if (!this.instance) { + this.instance = new HostTracker(); + } + return this.instance; + } + + private trackers: Array = []; + + constructor() { + super({ action: ACTION.LIST_HOSTS }); + this.openNewConnection(); + if (this.ws) { + this.ws.binaryType = 'arraybuffer'; + } + } + + protected onSocketClose(ev: CloseEvent): void { + console.log(TAG, 'WS closed'); + this.emit('disconnected', ev); + } + + protected onSocketMessage(event: MessageEvent): void { + console.log("接收到的参数4", event.data) + + let message: Message; + try { + // TODO: rewrite to binary + message = JSON.parse(event.data); + } catch (error: any) { + console.error(TAG, error.message); + console.log(TAG, error.data); + return; + } + switch (message.type) { + case MessageType.ERROR: { + const msg = message as MessageError; + console.error(TAG, msg.data); + this.emit('error', msg.data); + break; + } + case MessageType.HOSTS: { + const msg = message as MessageHosts; + // this.emit('hosts', msg.data); + if (msg.data.local) { + msg.data.local.forEach(({ type }) => { + const secure = location.protocol === 'https:'; + const port = location.port ? parseInt(location.port, 10) : secure ? 443 : 80; + const { hostname, pathname } = location; + if (type !== 'android' && type !== 'ios') { + console.warn(TAG, `Unsupported host type: "${type}"`); + return; + } + const hostItem: HostItem = { useProxy: false, secure, port, hostname, pathname, type }; + this.startTracker(hostItem); + }); + } + if (msg.data.remote) { + msg.data.remote.forEach((item) => this.startTracker(item)); + } + break; + } + default: + console.log(TAG, `Unknown message type: ${message.type}`); + } + } + + private startTracker(hostItem: HostItem): void { + switch (hostItem.type) { + case 'android': + this.trackers.push(GoogDeviceTracker.start(hostItem)); + break; + case 'ios': + this.trackers.push(ApplDeviceTracker.start(hostItem)); + break; + default: + console.warn(TAG, `Unsupported host type: "${hostItem.type}"`); + } + } + + protected onSocketOpen(): void { + // do nothing + } + + public destroy(): void { + super.destroy(); + this.trackers.forEach((tracker) => { + tracker.destroy(); + }); + this.trackers.length = 0; + } + + protected supportMultiplexing(): boolean { + return true; + } + + protected getChannelInitData(): Buffer { + const buffer = Buffer.alloc(4); + buffer.write(ChannelCode.HSTS, 'ascii'); + return buffer; + } +} diff --git a/src/app/client/ManagerClient.ts b/src/app/client/ManagerClient.ts new file mode 100644 index 0000000..9adc75f --- /dev/null +++ b/src/app/client/ManagerClient.ts @@ -0,0 +1,133 @@ +import { BaseClient } from './BaseClient'; +import { ACTION } from '../../common/Action'; +import { ParamsBase } from '../../types/ParamsBase'; +import Util from '../Util'; +import { Multiplexer } from '../../packages/multiplexer/Multiplexer'; +import { EventMap } from '../../common/TypedEmitter'; + +export abstract class ManagerClient

extends BaseClient { + public static ACTION = 'unknown'; + public static CODE = 'NONE'; + public static sockets: Map = new Map(); + protected destroyed = false; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unused-vars + public static start(..._rest: any[]): void { + throw Error('Not implemented'); + } + + protected readonly action?: string; + protected url: URL; + protected ws?: Multiplexer | WebSocket; + + protected constructor(params: P) { + super(params); + this.action = Util.parseStringEnv(params.action); + this.url = this.buildWebSocketUrl(); + } + + public hasConnection(): boolean { + return !!(this.ws && this.ws.readyState === this.ws.OPEN); + } + + protected openNewConnection(): Multiplexer | WebSocket { + if (this.ws && this.ws.readyState === this.ws.OPEN) { + this.ws.close(); + delete this.ws; + } + const url = this.url.toString(); + console.log(`连接到的websocket链接 ${url}`); + if (this.supportMultiplexing()) { + let openedMultiplexer = ManagerClient.sockets.get(url); + if (!openedMultiplexer) { + const ws = new WebSocket(url); + ws.addEventListener('close', () => { + ManagerClient.sockets.delete(url); + }); + const newMultiplexer = Multiplexer.wrap(ws); + newMultiplexer.on('empty', () => { + newMultiplexer.close(); + }); + ManagerClient.sockets.set(url, newMultiplexer); + openedMultiplexer = newMultiplexer; + } + const ws = openedMultiplexer.createChannel(this.getChannelInitData()); + ws.addEventListener('open', this.onSocketOpen.bind(this)); + ws.addEventListener('message', this.onSocketMessage.bind(this)); + ws.addEventListener('close', this.onSocketClose.bind(this)); + this.ws = ws; + } else { + const ws = new WebSocket(url); + ws.addEventListener('open', this.onSocketOpen.bind(this)); + ws.addEventListener('message', this.onSocketMessage.bind(this)); + ws.addEventListener('close', this.onSocketClose.bind(this)); + this.ws = ws; + } + return this.ws; + } + + public destroy(): void { + if (this.destroyed) { + console.error(new Error('Already disposed')); + return; + } + this.destroyed = true; + if (this.ws) { + if (this.ws.readyState === this.ws.OPEN) { + this.ws.close(); + } + } + } + + protected buildWebSocketUrl(): URL { + const directUrl = this.buildDirectWebSocketUrl(); + if (this.params.useProxy && !this.supportMultiplexing()) { + return this.wrapInProxy(directUrl); + } + return directUrl; + } + + protected buildDirectWebSocketUrl(): URL { + const { hostname, port, secure, action } = this.params; + const pathname = this.params.pathname ?? location.pathname; + let urlString: string; + if (typeof hostname === 'string' && typeof port === 'number') { + const protocol = secure ? 'wss:' : 'ws:'; + urlString = `${protocol}//${hostname}:${port}${pathname}`; + } else { + const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:'; + + // location.host includes hostname and port + urlString = `${protocol}${location.host}${pathname}`; + } + const directUrl = new URL(urlString); + if (this.supportMultiplexing()) { + directUrl.searchParams.set('action', ACTION.MULTIPLEX); + } else { + if (action) { + directUrl.searchParams.set('action', action); + } + } + return directUrl; + } + + protected wrapInProxy(directUrl: URL): URL { + const localProtocol = location.protocol === 'https:' ? 'wss:' : 'ws:'; + const localUrl = new URL(`${localProtocol}//${location.host}`); + localUrl.searchParams.set('action', ACTION.PROXY_WS); + localUrl.searchParams.set('ws', directUrl.toString()); + return localUrl; + } + + protected supportMultiplexing(): boolean { + return false; + } + + protected getChannelInitData(): Buffer { + return Buffer.from(ManagerClient.CODE); + } + + protected abstract onSocketOpen(event: Event): void; + protected abstract onSocketMessage(event: MessageEvent): void; + protected abstract onSocketClose(event: CloseEvent): void; +} diff --git a/src/app/client/StreamReceiver.ts b/src/app/client/StreamReceiver.ts new file mode 100644 index 0000000..1eca5d1 --- /dev/null +++ b/src/app/client/StreamReceiver.ts @@ -0,0 +1,252 @@ +import { ManagerClient } from './ManagerClient'; +import { ControlMessage } from '../controlMessage/ControlMessage'; +import DeviceMessage from '../googDevice/DeviceMessage'; +import VideoSettings from '../VideoSettings'; +import ScreenInfo from '../ScreenInfo'; +import Util from '../Util'; +import { DisplayInfo } from '../DisplayInfo'; +import { ParamsStream } from '../../types/ParamsStream'; +// import WebSocket, { Server } from 'ws'; // 新增WebSocket服务端依赖 + +const DEVICE_NAME_FIELD_LENGTH = 64; +const MAGIC_BYTES_INITIAL = Util.stringToUtf8ByteArray('scrcpy_initial'); + +export type ClientsStats = { + deviceName: string; + clientId: number; +}; + +export type DisplayCombinedInfo = { + displayInfo: DisplayInfo; + videoSettings?: VideoSettings; + screenInfo?: ScreenInfo; + connectionCount: number; +}; + +interface StreamReceiverEvents { + video: ArrayBuffer; + deviceMessage: DeviceMessage; + displayInfo: DisplayCombinedInfo[]; + clientsStats: ClientsStats; + encoders: string[]; + connected: void; + disconnected: CloseEvent; +} + +const TAG = '[StreamReceiver]'; + +export class StreamReceiver

extends ManagerClient { + // 新增WebSocket服务端相关属性 + // private wss: Server | null = null; + // private clientConnections: Set = new Set(); + + private events: ControlMessage[] = []; + private encodersSet: Set = new Set(); + private clientId = -1; + private deviceName = ''; + private readonly displayInfoMap: Map = new Map(); + private readonly connectionCountMap: Map = new Map(); + private readonly screenInfoMap: Map = new Map(); + private readonly videoSettingsMap: Map = new Map(); + private hasInitialInfo = false; + + constructor(params: P) { + super(params); + + // 新增WebSocket服务端初始化 + // this.startWebSocketServer(); + + this.openNewConnection(); + if (this.ws) { + this.ws.binaryType = 'arraybuffer'; + } + } + + + // 新增方法:启动WebSocket服务端 + // private startWebSocketServer(): void { + // this.wss = new WebSocket.Server({ port: 8765 }); + + // this.wss.on('connection', (ws: WebSocket) => { + // console.log(`连接成功`); + // this.clientConnections.add(ws); + + // // 设置消息处理器 + // ws.on('message', (data: WebSocket.Data) => { + // console.log(data, '接收到') + // }); + + // // 处理连接关闭 + // ws.on('close', () => { + // console.log(`${TAG} Client disconnected from server`); + // this.clientConnections.delete(ws); + // }); + + // // 发送初始信息给新客户端 + // }); + // } + + private handleInitialInfo(data: ArrayBuffer): void { + let offset = MAGIC_BYTES_INITIAL.length; + let nameBytes = new Uint8Array(data, offset, DEVICE_NAME_FIELD_LENGTH); + offset += DEVICE_NAME_FIELD_LENGTH; + let rest: Buffer = Buffer.from(new Uint8Array(data, offset)); + const displaysCount = rest.readInt32BE(0); + this.displayInfoMap.clear(); + this.connectionCountMap.clear(); + this.screenInfoMap.clear(); + this.videoSettingsMap.clear(); + rest = rest.slice(4); + for (let i = 0; i < displaysCount; i++) { + const displayInfoBuffer = rest.slice(0, DisplayInfo.BUFFER_LENGTH); + const displayInfo = DisplayInfo.fromBuffer(displayInfoBuffer); + const { displayId } = displayInfo; + this.displayInfoMap.set(displayId, displayInfo); + rest = rest.slice(DisplayInfo.BUFFER_LENGTH); + this.connectionCountMap.set(displayId, rest.readInt32BE(0)); + rest = rest.slice(4); + const screenInfoBytesCount = rest.readInt32BE(0); + rest = rest.slice(4); + if (screenInfoBytesCount) { + this.screenInfoMap.set(displayId, ScreenInfo.fromBuffer(rest.slice(0, screenInfoBytesCount))); + rest = rest.slice(screenInfoBytesCount); + } + const videoSettingsBytesCount = rest.readInt32BE(0); + rest = rest.slice(4); + if (videoSettingsBytesCount) { + this.videoSettingsMap.set(displayId, VideoSettings.fromBuffer(rest.slice(0, videoSettingsBytesCount))); + rest = rest.slice(videoSettingsBytesCount); + } + } + this.encodersSet.clear(); + const encodersCount = rest.readInt32BE(0); + rest = rest.slice(4); + for (let i = 0; i < encodersCount; i++) { + const nameLength = rest.readInt32BE(0); + rest = rest.slice(4); + const nameBytes = rest.slice(0, nameLength); + rest = rest.slice(nameLength); + const name = Util.utf8ByteArrayToString(nameBytes); + this.encodersSet.add(name); + } + this.clientId = rest.readInt32BE(0); + nameBytes = Util.filterTrailingZeroes(nameBytes); + this.deviceName = Util.utf8ByteArrayToString(nameBytes); + this.hasInitialInfo = true; + this.triggerInitialInfoEvents(); + } + + private static EqualArrays(a: ArrayLike, b: ArrayLike): boolean { + if (a.length !== b.length) { + return false; + } + for (let i = 0, l = a.length; i < l; i++) { + if (a[i] !== b[i]) { + return false; + } + } + return true; + } + + protected buildDirectWebSocketUrl(): URL { + const localUrl = super.buildDirectWebSocketUrl(); + if (this.supportMultiplexing()) { + return localUrl; + } + localUrl.searchParams.set('udid', this.params.udid); + return localUrl; + } + + protected onSocketClose(ev: CloseEvent): void { + console.log(`${TAG}. WS closed: ${ev.reason}`); + this.emit('disconnected', ev); + } + + protected onSocketMessage(event: MessageEvent): void { + // console.log("屏幕接收到的参数", event.data) + + if (event.data instanceof ArrayBuffer) { + + // works only because MAGIC_BYTES_INITIAL and MAGIC_BYTES_MESSAGE have same length + if (event.data.byteLength > MAGIC_BYTES_INITIAL.length) { + const magicBytes = new Uint8Array(event.data, 0, MAGIC_BYTES_INITIAL.length); + if (StreamReceiver.EqualArrays(magicBytes, MAGIC_BYTES_INITIAL)) { + this.handleInitialInfo(event.data); + return; + } + if (StreamReceiver.EqualArrays(magicBytes, DeviceMessage.MAGIC_BYTES_MESSAGE)) { + const message = DeviceMessage.fromBuffer(event.data); + console.log('屏幕接收到22', message) + this.emit('deviceMessage', message); + return; + } + } + + this.emit('video', new Uint8Array(event.data)); + } + } + + protected onSocketOpen(): void { + this.emit('connected', void 0); + let e = this.events.shift(); + while (e) { + this.sendEvent(e); + e = this.events.shift(); + } + } + + public sendEvent(event: ControlMessage): void { + + console.log('发送消息2', event) + + + + + if (this.ws && this.ws.readyState === this.ws.OPEN) { + // console.log('发送的参数2'); + + this.ws.send(event.toBuffer()); + } else { + // this.events.push(event); + } + } + + public stop(): void { + if (this.ws && this.ws.readyState === this.ws.OPEN) { + this.ws.close(); + } + this.events.length = 0; + } + + public getEncoders(): string[] { + return Array.from(this.encodersSet.values()); + } + + public getDeviceName(): string { + return this.deviceName; + } + + public triggerInitialInfoEvents(): void { + if (this.hasInitialInfo) { + const encoders = this.getEncoders(); + this.emit('encoders', encoders); + const { clientId, deviceName } = this; + this.emit('clientsStats', { clientId, deviceName }); + const infoArray: DisplayCombinedInfo[] = []; + this.displayInfoMap.forEach((displayInfo: DisplayInfo, displayId: number) => { + const connectionCount = this.connectionCountMap.get(displayId) || 0; + infoArray.push({ + displayInfo, + videoSettings: this.videoSettingsMap.get(displayId), + screenInfo: this.screenInfoMap.get(displayId), + connectionCount, + }); + }); + this.emit('displayInfo', infoArray); + } + } + + public getDisplayInfo(displayId: number): DisplayInfo | undefined { + return this.displayInfoMap.get(displayId); + } +} diff --git a/src/app/client/Tool.d.ts b/src/app/client/Tool.d.ts new file mode 100644 index 0000000..29b53e8 --- /dev/null +++ b/src/app/client/Tool.d.ts @@ -0,0 +1,12 @@ +import { ParamsDeviceTracker } from '../../types/ParamsDeviceTracker'; +import { BaseDeviceDescriptor } from '../../types/BaseDeviceDescriptor'; + +type Entry = HTMLElement | DocumentFragment; + +export interface Tool { + createEntryForDeviceList( + descriptor: BaseDeviceDescriptor, + blockClass: string, + params: ParamsDeviceTracker, + ): Array | Entry | undefined; +} diff --git a/src/app/controlMessage/CommandControlMessage.ts b/src/app/controlMessage/CommandControlMessage.ts new file mode 100644 index 0000000..b086ab8 --- /dev/null +++ b/src/app/controlMessage/CommandControlMessage.ts @@ -0,0 +1,226 @@ +import { ControlMessage } from './ControlMessage'; +import VideoSettings from '../VideoSettings'; +import Util from '../Util'; + +export enum FilePushState { + NEW, + START, + APPEND, + FINISH, + CANCEL, +} + +type FilePushParams = { + id: number; + state: FilePushState; + chunk?: Uint8Array; + fileName?: string; + fileSize?: number; +}; + +export class CommandControlMessage extends ControlMessage { + public static PAYLOAD_LENGTH = 0; + + public static Commands: Map = new Map([ + [ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL, 'Expand notifications'], + [ControlMessage.TYPE_EXPAND_SETTINGS_PANEL, 'Expand settings'], + [ControlMessage.TYPE_COLLAPSE_PANELS, 'Collapse panels'], + [ControlMessage.TYPE_GET_CLIPBOARD, 'Get clipboard'], + [ControlMessage.TYPE_SET_CLIPBOARD, 'Set clipboard'], + [ControlMessage.TYPE_ROTATE_DEVICE, 'Rotate device'], + [ControlMessage.TYPE_CHANGE_STREAM_PARAMETERS, 'Change video settings'], + ]); + + public static createSetVideoSettingsCommand(videoSettings: VideoSettings): CommandControlMessage { + const temp = videoSettings.toBuffer(); + const event = new CommandControlMessage(ControlMessage.TYPE_CHANGE_STREAM_PARAMETERS); + const offset = CommandControlMessage.PAYLOAD_LENGTH + 1; + const buffer = Buffer.alloc(offset + temp.length); + buffer.writeUInt8(event.type, 0); + temp.forEach((byte, index) => { + buffer.writeUInt8(byte, index + offset); + }); + event.buffer = buffer; + return event; + } + + public static createSetClipboardCommand(text: string, paste = true): CommandControlMessage { + const event = new CommandControlMessage(ControlMessage.TYPE_SET_CLIPBOARD); + const textBytes: Uint8Array | null = text ? Util.stringToUtf8ByteArray(text) : null; + const textLength = textBytes ? textBytes.length : 0; + let offset = 0; + const buffer = Buffer.alloc(1 + 1 + 4 + textLength); + offset = buffer.writeInt8(event.type, offset); + offset = buffer.writeInt8(paste ? 1 : 0, offset); + offset = buffer.writeInt32BE(textLength, offset); + if (textBytes) { + textBytes.forEach((byte: number, index: number) => { + buffer.writeUInt8(byte, index + offset); + }); + } + event.buffer = buffer; + return event; + } + + public static createSetScreenPowerModeCommand(mode: boolean): CommandControlMessage { + const event = new CommandControlMessage(ControlMessage.TYPE_SET_SCREEN_POWER_MODE); + let offset = 0; + const buffer = Buffer.alloc(1 + 1); + offset = buffer.writeInt8(event.type, offset); + buffer.writeUInt8(mode ? 1 : 0, offset); + event.buffer = buffer; + return event; + } + + public static createPushFileCommand(params: FilePushParams): CommandControlMessage { + const { id, fileName, fileSize, chunk, state } = params; + + if (state === FilePushState.START) { + return this.createPushFileStartCommand(id, fileName as string, fileSize as number); + } + if (state === FilePushState.APPEND) { + if (!chunk) { + throw TypeError('Invalid type'); + } + return this.createPushFileChunkCommand(id, chunk); + } + if (state === FilePushState.CANCEL || state === FilePushState.FINISH || state === FilePushState.NEW) { + return this.createPushFileOtherCommand(id, state); + } + + throw TypeError(`Unsupported state: "${state}"`); + } + + private static createPushFileStartCommand(id: number, fileName: string, fileSize: number): CommandControlMessage { + const event = new CommandControlMessage(ControlMessage.TYPE_PUSH_FILE); + const text = Util.stringToUtf8ByteArray(fileName); + const typeField = 1; + const idField = 2; + const stateField = 1; + const sizeField = 4; + const textLengthField = 2; + const textLength = text.length; + let offset = CommandControlMessage.PAYLOAD_LENGTH; + + const buffer = Buffer.alloc( + offset + typeField + idField + stateField + sizeField + textLengthField + textLength, + ); + buffer.writeUInt8(event.type, offset); + offset += typeField; + buffer.writeInt16BE(id, offset); + offset += idField; + buffer.writeInt8(FilePushState.START, offset); + offset += stateField; + buffer.writeUInt32BE(fileSize, offset); + offset += sizeField; + buffer.writeUInt16BE(textLength, offset); + offset += textLengthField; + text.forEach((byte, index) => { + buffer.writeUInt8(byte, index + offset); + }); + event.buffer = buffer; + return event; + } + + private static createPushFileChunkCommand(id: number, chunk: Uint8Array): CommandControlMessage { + const event = new CommandControlMessage(ControlMessage.TYPE_PUSH_FILE); + const typeField = 1; + const idField = 2; + const stateField = 1; + const chunkLengthField = 4; + const chunkLength = chunk.byteLength; + let offset = CommandControlMessage.PAYLOAD_LENGTH; + + const buffer = Buffer.alloc(offset + typeField + idField + stateField + chunkLengthField + chunkLength); + buffer.writeUInt8(event.type, offset); + offset += typeField; + buffer.writeInt16BE(id, offset); + offset += idField; + buffer.writeInt8(FilePushState.APPEND, offset); + offset += stateField; + buffer.writeUInt32BE(chunkLength, offset); + offset += chunkLengthField; + Array.from(chunk).forEach((byte, index) => { + buffer.writeUInt8(byte, index + offset); + }); + event.buffer = buffer; + return event; + } + + private static createPushFileOtherCommand(id: number, state: FilePushState): CommandControlMessage { + const event = new CommandControlMessage(ControlMessage.TYPE_PUSH_FILE); + const typeField = 1; + const idField = 2; + const stateField = 1; + let offset = CommandControlMessage.PAYLOAD_LENGTH; + const buffer = Buffer.alloc(offset + typeField + idField + stateField); + buffer.writeUInt8(event.type, offset); + offset += typeField; + buffer.writeInt16BE(id, offset); + offset += idField; + buffer.writeInt8(state, offset); + event.buffer = buffer; + return event; + } + + public static pushFileCommandFromBuffer(buffer: Buffer): { + id: number; + state: FilePushState; + chunk?: Buffer; + fileSize?: number; + fileName?: string; + } { + let offset = 0; + const type = buffer.readUInt8(offset); + offset += 1; + if (type !== CommandControlMessage.TYPE_PUSH_FILE) { + throw TypeError(`Incorrect type: "${type}"`); + } + const id = buffer.readInt16BE(offset); + offset += 2; + const state = buffer.readInt8(offset); + offset += 1; + let chunk: Buffer | undefined; + let fileSize: number | undefined; + let fileName: string | undefined; + if (state === FilePushState.APPEND) { + const chunkLength = buffer.readUInt32BE(offset); + offset += 4; + chunk = buffer.slice(offset, offset + chunkLength); + } else if (state === FilePushState.START) { + fileSize = buffer.readUInt32BE(offset); + offset += 4; + const textLength = buffer.readUInt16BE(offset); + offset += 2; + fileName = Util.utf8ByteArrayToString(buffer.slice(offset, offset + textLength)); + } + return { id, state, chunk, fileName, fileSize }; + } + + private buffer?: Buffer; + + constructor(readonly type: number) { + super(type); + } + + /** + * @override + */ + public toBuffer(): Buffer { + console.log("this.buffer", this.buffer) + if (!this.buffer) { + console.log('CommandControlMessage.PAYLOAD_LENGTH', CommandControlMessage.PAYLOAD_LENGTH) + const buffer = Buffer.alloc(CommandControlMessage.PAYLOAD_LENGTH + 1); + buffer.writeUInt8(this.type, 0); + this.buffer = buffer; + } + console.log("转换comm", this.buffer) + + return this.buffer; + } + + public toString(): string { + const buffer = this.buffer ? `, buffer=[${this.buffer.join(',')}]` : ''; + return `CommandControlMessage{action=${this.type}${buffer}}`; + } +} diff --git a/src/app/controlMessage/ControlMessage.ts b/src/app/controlMessage/ControlMessage.ts new file mode 100644 index 0000000..e3cab8d --- /dev/null +++ b/src/app/controlMessage/ControlMessage.ts @@ -0,0 +1,36 @@ +export interface ControlMessageInterface { + type: number; +} + +export class ControlMessage { + public static TYPE_KEYCODE = 0; + public static TYPE_TEXT = 1; + public static TYPE_TOUCH = 2; + public static TYPE_SCROLL = 3; + public static TYPE_BACK_OR_SCREEN_ON = 4; + public static TYPE_EXPAND_NOTIFICATION_PANEL = 5; + public static TYPE_EXPAND_SETTINGS_PANEL = 6; + public static TYPE_COLLAPSE_PANELS = 7; + public static TYPE_GET_CLIPBOARD = 8; + public static TYPE_SET_CLIPBOARD = 9; + public static TYPE_SET_SCREEN_POWER_MODE = 10; + public static TYPE_ROTATE_DEVICE = 11; + public static TYPE_CHANGE_STREAM_PARAMETERS = 101; + public static TYPE_PUSH_FILE = 102; + + constructor(readonly type: number) {} + + public toBuffer(): Buffer { + throw Error('Not implemented'); + } + + public toString(): string { + return 'ControlMessage'; + } + + public toJSON(): ControlMessageInterface { + return { + type: this.type, + }; + } +} diff --git a/src/app/controlMessage/KeyCodeControlMessage.ts b/src/app/controlMessage/KeyCodeControlMessage.ts new file mode 100644 index 0000000..2ad1ba2 --- /dev/null +++ b/src/app/controlMessage/KeyCodeControlMessage.ts @@ -0,0 +1,53 @@ +import { Buffer } from 'buffer'; +import { ControlMessage, ControlMessageInterface } from './ControlMessage'; + +export interface KeyCodeControlMessageInterface extends ControlMessageInterface { + action: number; + keycode: number; + repeat: number; + metaState: number; +} + +export class KeyCodeControlMessage extends ControlMessage { + public static PAYLOAD_LENGTH = 13; + + constructor( + readonly action: number, + readonly keycode: number, + readonly repeat: number, + readonly metaState: number, + ) { + super(ControlMessage.TYPE_KEYCODE); + } + + /** + * @override + */ + public toBuffer(): Buffer { + const buffer = Buffer.alloc(KeyCodeControlMessage.PAYLOAD_LENGTH + 1); + let offset = 0; + offset = buffer.writeInt8(this.type, offset); + offset = buffer.writeInt8(this.action, offset); + offset = buffer.writeInt32BE(this.keycode, offset); + offset = buffer.writeInt32BE(this.repeat, offset); + buffer.writeInt32BE(this.metaState, offset); + console.log("转换按钮", buffer) + console.log("鼠标按下的key", this.keycode) + + return buffer; + } + + public toString(): string { + return `KeyCodeControlMessage{action=${this.action}, keycode=${this.keycode}, metaState=${this.metaState}}`; + } + + public toJSON(): KeyCodeControlMessageInterface { + return { + type: this.type, + action: this.action, + keycode: this.keycode, + metaState: this.metaState, + repeat: this.repeat, + }; + } +} diff --git a/src/app/controlMessage/ScrollControlMessage.ts b/src/app/controlMessage/ScrollControlMessage.ts new file mode 100644 index 0000000..a0b4902 --- /dev/null +++ b/src/app/controlMessage/ScrollControlMessage.ts @@ -0,0 +1,47 @@ +import { ControlMessage, ControlMessageInterface } from './ControlMessage'; +import Position, { PositionInterface } from '../Position'; + +export interface ScrollControlMessageInterface extends ControlMessageInterface { + position: PositionInterface; + hScroll: number; + vScroll: number; +} + +export class ScrollControlMessage extends ControlMessage { + public static PAYLOAD_LENGTH = 20; + + constructor(readonly position: Position, readonly hScroll: number, readonly vScroll: number) { + super(ControlMessage.TYPE_SCROLL); + } + + /** + * @override + */ + public toBuffer(): Buffer { + const buffer = Buffer.alloc(ScrollControlMessage.PAYLOAD_LENGTH + 1); + let offset = 0; + offset = buffer.writeUInt8(this.type, offset); + offset = buffer.writeUInt32BE(this.position.point.x, offset); + offset = buffer.writeUInt32BE(this.position.point.y, offset); + offset = buffer.writeUInt16BE(this.position.screenSize.width, offset); + offset = buffer.writeUInt16BE(this.position.screenSize.height, offset); + offset = buffer.writeInt32BE(this.hScroll, offset); + buffer.writeInt32BE(this.vScroll, offset); + + console.log("转换text", buffer) + return buffer; + } + + public toString(): string { + return `ScrollControlMessage{hScroll=${this.hScroll}, vScroll=${this.vScroll}, position=${this.position}}`; + } + + public toJSON(): ScrollControlMessageInterface { + return { + type: this.type, + position: this.position.toJSON(), + hScroll: this.hScroll, + vScroll: this.vScroll, + }; + } +} diff --git a/src/app/controlMessage/TextControlMessage.ts b/src/app/controlMessage/TextControlMessage.ts new file mode 100644 index 0000000..00a7aff --- /dev/null +++ b/src/app/controlMessage/TextControlMessage.ts @@ -0,0 +1,43 @@ +import { Buffer } from 'buffer'; +import { ControlMessage, ControlMessageInterface } from './ControlMessage'; + +export interface TextControlMessageInterface extends ControlMessageInterface { + text: string; +} + +export class TextControlMessage extends ControlMessage { + private static TEXT_SIZE_FIELD_LENGTH = 4; + constructor(readonly text: string) { + super(ControlMessage.TYPE_TEXT); + } + + public getText(): string { + return this.text; + } + + /** + * @override + */ + public toBuffer(): Buffer { + const length = this.text.length; + const buffer = Buffer.alloc(length + 1 + TextControlMessage.TEXT_SIZE_FIELD_LENGTH); + let offset = 0; + offset = buffer.writeUInt8(this.type, offset); + offset = buffer.writeUInt32BE(length, offset); + buffer.write(this.text, offset); + console.log("转换text", buffer) + + return buffer; + } + + public toString(): string { + return `TextControlMessage{text=${this.text}}`; + } + + public toJSON(): TextControlMessageInterface { + return { + type: this.type, + text: this.text, + }; + } +} diff --git a/src/app/controlMessage/TouchControlMessage.ts b/src/app/controlMessage/TouchControlMessage.ts new file mode 100644 index 0000000..9cebe40 --- /dev/null +++ b/src/app/controlMessage/TouchControlMessage.ts @@ -0,0 +1,77 @@ +import { ControlMessage, ControlMessageInterface } from './ControlMessage'; +import Position, { PositionInterface } from '../Position'; + +export interface TouchControlMessageInterface extends ControlMessageInterface { + type: number; + action: number; + pointerId: number; + position: PositionInterface; + pressure: number; + buttons: number; +} + +export class TouchControlMessage extends ControlMessage { + public static PAYLOAD_LENGTH = 28; + /** + * - For a touch screen or touch pad, reports the approximate pressure + * applied to the surface by a finger or other tool. The value is + * normalized to a range from 0 (no pressure at all) to 1 (normal pressure), + * although values higher than 1 may be generated depending on the + * calibration of the input device. + * - For a trackball, the value is set to 1 if the trackball button is pressed + * or 0 otherwise. + * - For a mouse, the value is set to 1 if the primary mouse button is pressed + * or 0 otherwise. + * + * - scrcpy server expects signed short (2 bytes) for a pressure value + * - in browser TouchEvent has `force` property (values in 0..1 range), we + * use it as "pressure" for scrcpy + */ + public static readonly MAX_PRESSURE_VALUE = 0xffff; + + constructor( + readonly action: number, + readonly pointerId: number, + readonly position: Position, + readonly pressure: number, + readonly buttons: number, + ) { + super(ControlMessage.TYPE_TOUCH); + } + + /** + * @override + */ + public toBuffer(): Buffer { + const buffer: Buffer = Buffer.alloc(TouchControlMessage.PAYLOAD_LENGTH + 1); + let offset = 0; + offset = buffer.writeUInt8(this.type, offset); + offset = buffer.writeUInt8(this.action, offset); + offset = buffer.writeUInt32BE(0, offset); // pointerId is `long` (8 bytes) on java side + offset = buffer.writeUInt32BE(this.pointerId, offset); + offset = buffer.writeUInt32BE(this.position.point.x, offset); + offset = buffer.writeUInt32BE(this.position.point.y, offset); + offset = buffer.writeUInt16BE(this.position.screenSize.width, offset); + offset = buffer.writeUInt16BE(this.position.screenSize.height, offset); + offset = buffer.writeUInt16BE(this.pressure * TouchControlMessage.MAX_PRESSURE_VALUE, offset); + buffer.writeUInt32BE(this.buttons, offset); + // console.log("转换屏幕点击滑动事件", buffer) + + return buffer; + } + + public toString(): string { + return `TouchControlMessage{action=${this.action}, pointerId=${this.pointerId}, position=${this.position}, pressure=${this.pressure}, buttons=${this.buttons}}`; + } + + public toJSON(): TouchControlMessageInterface { + return { + type: this.type, + action: this.action, + pointerId: this.pointerId, + position: this.position.toJSON(), + pressure: this.pressure, + buttons: this.buttons, + }; + } +} diff --git a/src/app/googDevice/DeviceMessage.ts b/src/app/googDevice/DeviceMessage.ts new file mode 100644 index 0000000..7cbf040 --- /dev/null +++ b/src/app/googDevice/DeviceMessage.ts @@ -0,0 +1,54 @@ +import Util from '../Util'; + +export default class DeviceMessage { + public static TYPE_CLIPBOARD = 0; + public static TYPE_PUSH_RESPONSE = 101; + + public static readonly MAGIC_BYTES_MESSAGE = Util.stringToUtf8ByteArray('scrcpy_message'); + + constructor(public readonly type: number, protected readonly buffer: Buffer) { } + + public static fromBuffer(data: ArrayBuffer): DeviceMessage { + const magicSize = this.MAGIC_BYTES_MESSAGE.length; + const buffer = Buffer.from(data, magicSize, data.byteLength - magicSize); + const type = buffer.readUInt8(0); + return new DeviceMessage(type, buffer); + } + + public getText(): string { + if (this.type !== DeviceMessage.TYPE_CLIPBOARD) { + throw TypeError(`Wrong message type: ${this.type}`); + } + if (!this.buffer) { + throw Error('Empty buffer'); + } + let offset = 1; + const length = this.buffer.readInt32BE(offset); + offset += 4; + const textBytes = this.buffer.slice(offset, offset + length); + return Util.utf8ByteArrayToString(textBytes); + } + + public getPushStats(): { id: number; code: number } { + if (this.type !== DeviceMessage.TYPE_PUSH_RESPONSE) { + throw TypeError(`Wrong message type: ${this.type}`); + } + if (!this.buffer) { + throw Error('Empty buffer'); + } + console.log("从设备获取到的信息", this.buffer) + const id = this.buffer.readInt16BE(1); + const code = this.buffer.readInt8(3); + return { id, code }; + } + + public toString(): string { + let desc: string; + if (this.type === DeviceMessage.TYPE_CLIPBOARD && this.buffer) { + desc = `, text=[${this.getText()}]`; + } else { + desc = this.buffer ? `, buffer=[${this.buffer.join(',')}]` : ''; + } + return `DeviceMessage{type=${this.type}${desc}}`; + } +} diff --git a/src/app/googDevice/DragAndDropHandler.ts b/src/app/googDevice/DragAndDropHandler.ts new file mode 100644 index 0000000..789329f --- /dev/null +++ b/src/app/googDevice/DragAndDropHandler.ts @@ -0,0 +1,102 @@ +export interface DragEventListener { + onDragEnter: () => boolean; + onDragLeave: () => boolean; + onFilesDrop: (files: File[]) => boolean; + getElement: () => HTMLElement; +} + +export class DragAndDropHandler { + private static readonly listeners: Set = new Set(); + private static dropHandler = (ev: DragEvent): boolean => { + if (!ev.dataTransfer) { + return false; + } + + const files: File[] = []; + if (ev.dataTransfer.items) { + for (let i = 0; i < ev.dataTransfer.items.length; i++) { + if (ev.dataTransfer.items[i].kind === 'file') { + const file = ev.dataTransfer.items[i].getAsFile(); + + if (file) { + files.push(file); + } + } + } + } else { + for (let i = 0; i < ev.dataTransfer.files.length; i++) { + files.push(ev.dataTransfer.files[i]); + } + } + let handled = false; + DragAndDropHandler.listeners.forEach((listener) => { + const element = listener.getElement(); + if (element === ev.currentTarget) { + handled = handled || listener.onFilesDrop(files); + } + }); + if (handled) { + ev.preventDefault(); + return true; + } + return false; + }; + private static dragOverHandler = (ev: DragEvent): void => { + ev.preventDefault(); + }; + private static dragLeaveHandler = (ev: DragEvent): boolean => { + let handled = false; + DragAndDropHandler.listeners.forEach((listener) => { + const element = listener.getElement(); + if (element === ev.currentTarget) { + handled = handled || listener.onDragLeave(); + } + }); + if (handled) { + ev.preventDefault(); + return true; + } + return false; + }; + private static dragEnterHandler = (ev: DragEvent): boolean => { + let handled = false; + DragAndDropHandler.listeners.forEach((listener) => { + const element = listener.getElement(); + if (element === ev.currentTarget) { + handled = handled || listener.onDragEnter(); + } + }); + if (handled) { + ev.preventDefault(); + return true; + } + return false; + }; + private static attachListeners(element: HTMLElement): void { + element.addEventListener('drop', this.dropHandler); + element.addEventListener('dragover', this.dragOverHandler); + element.addEventListener('dragleave', this.dragLeaveHandler); + element.addEventListener('dragenter', this.dragEnterHandler); + } + private static detachListeners(element: HTMLElement): void { + element.removeEventListener('drop', this.dropHandler); + element.removeEventListener('dragover', this.dragOverHandler); + element.removeEventListener('dragleave', this.dragLeaveHandler); + element.removeEventListener('dragenter', this.dragEnterHandler); + } + + public static addEventListener(listener: DragEventListener): void { + if (this.listeners.has(listener)) { + return; + } + this.attachListeners(listener.getElement()); + this.listeners.add(listener); + } + public static removeEventListener(listener: DragEventListener): void { + if (!this.listeners.has(listener)) { + return; + } + this.detachListeners(listener.getElement()); + this.listeners.delete(listener); + } +} diff --git a/src/app/googDevice/DragAndPushLogger.ts b/src/app/googDevice/DragAndPushLogger.ts new file mode 100644 index 0000000..3567796 --- /dev/null +++ b/src/app/googDevice/DragAndPushLogger.ts @@ -0,0 +1,129 @@ +import FilePushHandler, { DragAndPushListener, PushUpdateParams } from './filePush/FilePushHandler'; + +const TAG = '[DragAndPushLogger]'; + +export default class DragAndPushLogger implements DragAndPushListener { + private static readonly X: number = 20; + private static readonly Y: number = 40; + private static readonly HEIGHT = 12; + private static readonly LOG_BACKGROUND: string = 'rgba(0,0,0, 0.5)'; + private static readonly DEBUG_COLOR: string = 'hsl(136, 85%,50%)'; + private static readonly ERROR_COLOR: string = 'hsl(336,85%,50%)'; + + private readonly ctx: CanvasRenderingContext2D | null = null; + private timeoutMap: Map = new Map(); + private dirtyMap: Map = new Map(); + private pushLineMap: Map = new Map(); + private linePushMap: Map = new Map(); + private dirtyLines: boolean[] = []; + constructor(element: HTMLElement) { + if (element instanceof HTMLCanvasElement) { + const canvas = element as HTMLCanvasElement; + this.ctx = canvas.getContext('2d'); + } + } + cleanDirtyLine = (line: number): void => { + if (!this.ctx) { + return; + } + const { X, Y, HEIGHT } = DragAndPushLogger; + const x = X; + const y = Y + HEIGHT * line * 2; + const dirty = this.dirtyMap.get(line); + if (dirty) { + const p = DragAndPushLogger.HEIGHT / 2; + const d = p * 2; + this.ctx.clearRect(x - p, y - HEIGHT - p, dirty + d, HEIGHT + d); + } + this.dirtyLines[line] = false; + }; + private logText(text: string, line: number, scheduleCleanup = false, error = false): void { + if (!this.ctx) { + error ? console.error(TAG, text) : console.log(TAG, text); + return; + } + if (error) { + console.error(TAG, text); + } + this.cleanDirtyLine(line); + + const { X, Y, HEIGHT } = DragAndPushLogger; + const x = X; + const y = Y + HEIGHT * line * 2; + this.ctx.save(); + this.ctx.font = `${HEIGHT}px monospace`; + const textMetrics = this.ctx.measureText(text); + const width = Math.abs(textMetrics.actualBoundingBoxLeft) + Math.abs(textMetrics.actualBoundingBoxRight); + this.dirtyMap.set(line, width); + this.ctx.fillStyle = DragAndPushLogger.LOG_BACKGROUND; + const p = DragAndPushLogger.HEIGHT / 2 - 1; + const d = p * 2; + this.ctx.fillRect(x - p, y - HEIGHT - p, width + d, HEIGHT + d); + this.ctx.fillStyle = error ? DragAndPushLogger.ERROR_COLOR : DragAndPushLogger.DEBUG_COLOR; + this.ctx.fillText(text, x, y); + this.ctx.restore(); + if (scheduleCleanup) { + this.dirtyLines[line] = true; + let timeout = this.timeoutMap.get(line); + if (timeout) { + clearTimeout(timeout); + } + timeout = window.setTimeout(() => { + this.cleanDirtyLine(line); + const key = this.linePushMap.get(line); + if (typeof key === 'string') { + this.linePushMap.delete(line); + this.pushLineMap.delete(key); + } + }, 5000); + this.timeoutMap.set(line, timeout); + } + } + + public onDragEnter(): boolean { + this.logText('Drop APK files here', 1); + return true; + } + + public onDragLeave(): boolean { + this.cleanDirtyLine(1); + return true; + } + + public onDrop(): boolean { + this.cleanDirtyLine(1); + return true; + } + + public onError(error: Error | string): void { + const text = typeof error === 'string' ? error : error.message; + this.logText(text, 1, true); + } + + onFilePushUpdate(data: PushUpdateParams): void { + const { pushId, message, fileName, error } = data; + const key = `${pushId}/${fileName}`; + const firstKey = `${FilePushHandler.REQUEST_NEW_PUSH_ID}/${fileName}`; + let line: number | undefined = this.pushLineMap.get(key); + let update = false; + if (typeof line === 'undefined' && key !== firstKey) { + line = this.pushLineMap.get(firstKey); + if (typeof line !== 'undefined') { + this.pushLineMap.delete(firstKey); + update = true; + } + } + if (typeof line === 'undefined') { + line = 2; + while (this.dirtyLines[line]) { + line++; + } + update = true; + } + if (update) { + this.pushLineMap.set(key, line); + this.linePushMap.set(line, key); + } + this.logText(`Upload "${fileName}": ${message}`, line, true, error); + } +} diff --git a/src/app/googDevice/Entry.ts b/src/app/googDevice/Entry.ts new file mode 100644 index 0000000..a80a353 --- /dev/null +++ b/src/app/googDevice/Entry.ts @@ -0,0 +1,11 @@ +import { Stats } from './Stats'; + +export class Entry extends Stats { + constructor(public name: string, mode: number, size: number, mtime: number) { + super(mode, size, mtime); + } + + public toString(): string { + return this.name; + } +} diff --git a/src/app/googDevice/KeyInputHandler.ts b/src/app/googDevice/KeyInputHandler.ts new file mode 100644 index 0000000..41aaaac --- /dev/null +++ b/src/app/googDevice/KeyInputHandler.ts @@ -0,0 +1,79 @@ +import { KeyCodeControlMessage } from '../controlMessage/KeyCodeControlMessage'; +import KeyEvent from './android/KeyEvent'; +import { KeyToCodeMap } from './KeyToCodeMap'; + +export interface KeyEventListener { + onKeyEvent: (event: KeyCodeControlMessage) => void; +} + +export class KeyInputHandler { + private static readonly repeatCounter: Map = new Map(); + private static readonly listeners: Set = new Set(); + private static handler = (event: Event): void => { + const keyboardEvent = event as KeyboardEvent; + const keyCode = KeyToCodeMap.get(keyboardEvent.code); + if (!keyCode) { + return; + } + let action: typeof KeyEvent.ACTION_DOWN | typeof KeyEvent.ACTION_DOWN; + let repeatCount = 0; + console.log('键盘按下', keyCode) + if (keyboardEvent.type === 'keydown') { + action = KeyEvent.ACTION_DOWN; + if (keyboardEvent.repeat) { + let count = KeyInputHandler.repeatCounter.get(keyCode); + if (typeof count !== 'number') { + count = 1; + } else { + count++; + } + repeatCount = count; + KeyInputHandler.repeatCounter.set(keyCode, count); + } + } else if (keyboardEvent.type === 'keyup') { + action = KeyEvent.ACTION_UP; + KeyInputHandler.repeatCounter.delete(keyCode); + } else { + return; + } + const metaState = + (keyboardEvent.getModifierState('Alt') ? KeyEvent.META_ALT_ON : 0) | + (keyboardEvent.getModifierState('Shift') ? KeyEvent.META_SHIFT_ON : 0) | + (keyboardEvent.getModifierState('Control') ? KeyEvent.META_CTRL_ON : 0) | + (keyboardEvent.getModifierState('Meta') ? KeyEvent.META_META_ON : 0) | + (keyboardEvent.getModifierState('CapsLock') ? KeyEvent.META_CAPS_LOCK_ON : 0) | + (keyboardEvent.getModifierState('ScrollLock') ? KeyEvent.META_SCROLL_LOCK_ON : 0) | + (keyboardEvent.getModifierState('NumLock') ? KeyEvent.META_NUM_LOCK_ON : 0); + + const controlMessage: KeyCodeControlMessage = new KeyCodeControlMessage( + action, + keyCode, + repeatCount, + metaState, + ); + KeyInputHandler.listeners.forEach((listener) => { + listener.onKeyEvent(controlMessage); + }); + event.preventDefault(); + }; + private static attachListeners(): void { + document.body.addEventListener('keydown', this.handler); + document.body.addEventListener('keyup', this.handler); + } + private static detachListeners(): void { + document.body.removeEventListener('keydown', this.handler); + document.body.removeEventListener('keyup', this.handler); + } + public static addEventListener(listener: KeyEventListener): void { + if (!this.listeners.size) { + this.attachListeners(); + } + this.listeners.add(listener); + } + public static removeEventListener(listener: KeyEventListener): void { + this.listeners.delete(listener); + if (!this.listeners.size) { + this.detachListeners(); + } + } +} diff --git a/src/app/googDevice/KeyToCodeMap.ts b/src/app/googDevice/KeyToCodeMap.ts new file mode 100644 index 0000000..f964eda --- /dev/null +++ b/src/app/googDevice/KeyToCodeMap.ts @@ -0,0 +1,118 @@ +import KeyEvent from './android/KeyEvent'; +import UIEventsCode from '../UIEventsCode'; + +export const KeyToCodeMap = new Map([ + [UIEventsCode.Backquote, KeyEvent.KEYCODE_GRAVE], + [UIEventsCode.Backslash, KeyEvent.KEYCODE_BACKSLASH], + [UIEventsCode.BracketLeft, KeyEvent.KEYCODE_LEFT_BRACKET], + [UIEventsCode.BracketRight, KeyEvent.KEYCODE_RIGHT_BRACKET], + [UIEventsCode.Comma, KeyEvent.KEYCODE_COMMA], + [UIEventsCode.Digit0, KeyEvent.KEYCODE_0], + [UIEventsCode.Digit1, KeyEvent.KEYCODE_1], + [UIEventsCode.Digit2, KeyEvent.KEYCODE_2], + [UIEventsCode.Digit3, KeyEvent.KEYCODE_3], + [UIEventsCode.Digit4, KeyEvent.KEYCODE_4], + [UIEventsCode.Digit5, KeyEvent.KEYCODE_5], + [UIEventsCode.Digit6, KeyEvent.KEYCODE_6], + [UIEventsCode.Digit7, KeyEvent.KEYCODE_7], + [UIEventsCode.Digit8, KeyEvent.KEYCODE_8], + [UIEventsCode.Digit9, KeyEvent.KEYCODE_9], + [UIEventsCode.Equal, KeyEvent.KEYCODE_EQUALS], + [UIEventsCode.IntlRo, KeyEvent.KEYCODE_RO], + [UIEventsCode.IntlYen, KeyEvent.KEYCODE_YEN], + [UIEventsCode.KeyA, KeyEvent.KEYCODE_A], + [UIEventsCode.KeyB, KeyEvent.KEYCODE_B], + [UIEventsCode.KeyC, KeyEvent.KEYCODE_C], + [UIEventsCode.KeyD, KeyEvent.KEYCODE_D], + [UIEventsCode.KeyE, KeyEvent.KEYCODE_E], + [UIEventsCode.KeyF, KeyEvent.KEYCODE_F], + [UIEventsCode.KeyG, KeyEvent.KEYCODE_G], + [UIEventsCode.KeyH, KeyEvent.KEYCODE_H], + [UIEventsCode.KeyI, KeyEvent.KEYCODE_I], + [UIEventsCode.KeyJ, KeyEvent.KEYCODE_J], + [UIEventsCode.KeyK, KeyEvent.KEYCODE_K], + [UIEventsCode.KeyL, KeyEvent.KEYCODE_L], + [UIEventsCode.KeyM, KeyEvent.KEYCODE_M], + [UIEventsCode.KeyN, KeyEvent.KEYCODE_N], + [UIEventsCode.KeyO, KeyEvent.KEYCODE_O], + [UIEventsCode.KeyP, KeyEvent.KEYCODE_P], + [UIEventsCode.KeyQ, KeyEvent.KEYCODE_Q], + [UIEventsCode.KeyR, KeyEvent.KEYCODE_R], + [UIEventsCode.KeyS, KeyEvent.KEYCODE_S], + [UIEventsCode.KeyT, KeyEvent.KEYCODE_T], + [UIEventsCode.KeyU, KeyEvent.KEYCODE_U], + [UIEventsCode.KeyV, KeyEvent.KEYCODE_V], + [UIEventsCode.KeyW, KeyEvent.KEYCODE_W], + [UIEventsCode.KeyX, KeyEvent.KEYCODE_X], + [UIEventsCode.KeyY, KeyEvent.KEYCODE_Y], + [UIEventsCode.KeyZ, KeyEvent.KEYCODE_Z], + [UIEventsCode.Minus, KeyEvent.KEYCODE_MINUS], + [UIEventsCode.Period, KeyEvent.KEYCODE_PERIOD], + [UIEventsCode.Quote, KeyEvent.KEYCODE_APOSTROPHE], + [UIEventsCode.Semicolon, KeyEvent.KEYCODE_SEMICOLON], + [UIEventsCode.Slash, KeyEvent.KEYCODE_SLASH], + [UIEventsCode.KanaMode, KeyEvent.KEYCODE_KANA], + [UIEventsCode.Delete, KeyEvent.KEYCODE_FORWARD_DEL], + [UIEventsCode.End, KeyEvent.KEYCODE_MOVE_END], + [UIEventsCode.Help, KeyEvent.KEYCODE_HELP], + [UIEventsCode.Home, KeyEvent.KEYCODE_MOVE_HOME], + [UIEventsCode.Insert, KeyEvent.KEYCODE_INSERT], + [UIEventsCode.PageDown, KeyEvent.KEYCODE_PAGE_DOWN], + [UIEventsCode.PageUp, KeyEvent.KEYCODE_PAGE_UP], + [UIEventsCode.AltLeft, KeyEvent.KEYCODE_ALT_LEFT], + [UIEventsCode.AltRight, KeyEvent.KEYCODE_ALT_RIGHT], + [UIEventsCode.Backspace, KeyEvent.KEYCODE_DEL], + [UIEventsCode.CapsLock, KeyEvent.KEYCODE_CAPS_LOCK], + [UIEventsCode.ControlLeft, KeyEvent.KEYCODE_CTRL_LEFT], + [UIEventsCode.ControlRight, KeyEvent.KEYCODE_CTRL_RIGHT], + [UIEventsCode.Enter, KeyEvent.KEYCODE_ENTER], + [UIEventsCode.MetaLeft, KeyEvent.KEYCODE_META_LEFT], + [UIEventsCode.MetaRight, KeyEvent.KEYCODE_META_RIGHT], + [UIEventsCode.ShiftLeft, KeyEvent.KEYCODE_SHIFT_LEFT], + [UIEventsCode.ShiftRight, KeyEvent.KEYCODE_SHIFT_RIGHT], + [UIEventsCode.Space, KeyEvent.KEYCODE_SPACE], + [UIEventsCode.Tab, KeyEvent.KEYCODE_TAB], + [UIEventsCode.ArrowLeft, KeyEvent.KEYCODE_DPAD_LEFT], + [UIEventsCode.ArrowUp, KeyEvent.KEYCODE_DPAD_UP], + [UIEventsCode.ArrowRight, KeyEvent.KEYCODE_DPAD_RIGHT], + [UIEventsCode.ArrowDown, KeyEvent.KEYCODE_DPAD_DOWN], + + [UIEventsCode.NumLock, KeyEvent.KEYCODE_NUM_LOCK], + [UIEventsCode.Numpad0, KeyEvent.KEYCODE_NUMPAD_0], + [UIEventsCode.Numpad1, KeyEvent.KEYCODE_NUMPAD_1], + [UIEventsCode.Numpad2, KeyEvent.KEYCODE_NUMPAD_2], + [UIEventsCode.Numpad3, KeyEvent.KEYCODE_NUMPAD_3], + [UIEventsCode.Numpad4, KeyEvent.KEYCODE_NUMPAD_4], + [UIEventsCode.Numpad5, KeyEvent.KEYCODE_NUMPAD_5], + [UIEventsCode.Numpad6, KeyEvent.KEYCODE_NUMPAD_6], + [UIEventsCode.Numpad7, KeyEvent.KEYCODE_NUMPAD_7], + [UIEventsCode.Numpad8, KeyEvent.KEYCODE_NUMPAD_8], + [UIEventsCode.Numpad9, KeyEvent.KEYCODE_NUMPAD_9], + [UIEventsCode.NumpadAdd, KeyEvent.KEYCODE_NUMPAD_ADD], + [UIEventsCode.NumpadComma, KeyEvent.KEYCODE_NUMPAD_COMMA], + [UIEventsCode.NumpadDecimal, KeyEvent.KEYCODE_NUMPAD_DOT], + [UIEventsCode.NumpadDivide, KeyEvent.KEYCODE_NUMPAD_DIVIDE], + [UIEventsCode.NumpadEnter, KeyEvent.KEYCODE_NUMPAD_ENTER], + [UIEventsCode.NumpadEqual, KeyEvent.KEYCODE_NUMPAD_EQUALS], + [UIEventsCode.NumpadMultiply, KeyEvent.KEYCODE_NUMPAD_MULTIPLY], + [UIEventsCode.NumpadParenLeft, KeyEvent.KEYCODE_NUMPAD_LEFT_PAREN], + [UIEventsCode.NumpadParenRight, KeyEvent.KEYCODE_NUMPAD_RIGHT_PAREN], + [UIEventsCode.NumpadSubtract, KeyEvent.KEYCODE_NUMPAD_SUBTRACT], + + [UIEventsCode.Escape, KeyEvent.KEYCODE_ESCAPE], + [UIEventsCode.F1, KeyEvent.KEYCODE_F1], + [UIEventsCode.F2, KeyEvent.KEYCODE_F2], + [UIEventsCode.F3, KeyEvent.KEYCODE_F3], + [UIEventsCode.F4, KeyEvent.KEYCODE_F4], + [UIEventsCode.F5, KeyEvent.KEYCODE_F5], + [UIEventsCode.F6, KeyEvent.KEYCODE_F6], + [UIEventsCode.F7, KeyEvent.KEYCODE_F7], + [UIEventsCode.F8, KeyEvent.KEYCODE_F8], + [UIEventsCode.F9, KeyEvent.KEYCODE_F9], + [UIEventsCode.F10, KeyEvent.KEYCODE_F10], + [UIEventsCode.F11, KeyEvent.KEYCODE_F11], + [UIEventsCode.F12, KeyEvent.KEYCODE_F12], + [UIEventsCode.Fn, KeyEvent.KEYCODE_FUNCTION], + [UIEventsCode.PrintScreen, KeyEvent.KEYCODE_SYSRQ], + [UIEventsCode.Pause, KeyEvent.KEYCODE_BREAK], +]); diff --git a/src/app/googDevice/Stats.ts b/src/app/googDevice/Stats.ts new file mode 100644 index 0000000..01ba124 --- /dev/null +++ b/src/app/googDevice/Stats.ts @@ -0,0 +1,67 @@ +export class Stats { + // The following constant were extracted from `man 2 stat` on Ubuntu 12.10. + public static S_IFMT = 0o170000; // bit mask for the file type bit fields + + public static S_IFSOCK = 0o140000; // socket + + public static S_IFLNK = 0o120000; // symbolic link + + public static S_IFREG = 0o100000; // regular file + + public static S_IFBLK = 0o060000; // block device + + public static S_IFDIR = 0o040000; // directory + + public static S_IFCHR = 0o020000; // character device + + public static S_IFIFO = 0o010000; // FIFO + + public static S_ISUID = 0o004000; // set UID bit + + public static S_ISGID = 0o002000; // set-group-ID bit (see below) + + public static S_ISVTX = 0o001000; // sticky bit (see below) + + public static S_IRWXU = 0o0700; // mask for file owner permissions + + public static S_IRUSR = 0o0400; // owner has read permission + + public static S_IWUSR = 0o0200; // owner has write permission + + public static S_IXUSR = 0o0100; // owner has execute permission + + public static S_IRWXG = 0o0070; // mask for group permissions + + public static S_IRGRP = 0o0040; // group has read permission + + public readonly mtime: Date; + + constructor(public readonly mode: number, public readonly size: number, mtime: number) { + this.mtime = new Date(mtime * 1000); + } + + private checkModeProperty(property: number): boolean { + return (this.mode & Stats.S_IFMT) === property; + } + public isBlockDevice(): boolean { + return this.checkModeProperty(Stats.S_IFBLK); + } + public isCharacterDevice(): boolean { + return this.checkModeProperty(Stats.S_IFCHR); + } + public isDirectory(): boolean { + return this.checkModeProperty(Stats.S_IFDIR); + } + public isFIFO(): boolean { + return this.checkModeProperty(Stats.S_IFIFO); + } + public isisSocket(): boolean { + return this.checkModeProperty(Stats.S_IFSOCK); + } + public isSymbolicLink(): boolean { + return this.checkModeProperty(Stats.S_IFLNK); + } + public isFile(): boolean { + return this.checkModeProperty(Stats.S_IFREG); + } +} diff --git a/src/app/googDevice/android/KeyEvent.ts b/src/app/googDevice/android/KeyEvent.ts new file mode 100644 index 0000000..c8be54d --- /dev/null +++ b/src/app/googDevice/android/KeyEvent.ts @@ -0,0 +1,317 @@ +export default class KeyEvent { + // 定义按键常量 + public static readonly ACTION_DOWN: number = 0; + public static readonly ACTION_UP: number = 1; + + public static readonly KEYCODE_0: number = 7; + public static readonly KEYCODE_1: number = 8; + public static readonly KEYCODE_11: number = 227; + public static readonly KEYCODE_12: number = 228; + public static readonly KEYCODE_2: number = 9; + public static readonly KEYCODE_3: number = 10; + public static readonly KEYCODE_3D_MODE: number = 206; + public static readonly KEYCODE_4: number = 11; + public static readonly KEYCODE_5: number = 12; + public static readonly KEYCODE_6: number = 13; + public static readonly KEYCODE_7: number = 14; + public static readonly KEYCODE_8: number = 15; + public static readonly KEYCODE_9: number = 16; + public static readonly KEYCODE_A: number = 29; + public static readonly KEYCODE_ALL_APPS: number = 284; + public static readonly KEYCODE_ALT_LEFT: number = 57; + public static readonly KEYCODE_ALT_RIGHT: number = 58; + public static readonly KEYCODE_APOSTROPHE: number = 75; + public static readonly KEYCODE_APP_SWITCH: number = 187; + public static readonly KEYCODE_ASSIST: number = 219; + public static readonly KEYCODE_AT: number = 77; + public static readonly KEYCODE_AVR_INPUT: number = 182; + public static readonly KEYCODE_AVR_POWER: number = 181; + public static readonly KEYCODE_B: number = 30; + public static readonly KEYCODE_BACK: number = 4; + public static readonly KEYCODE_BACKSLASH: number = 73; + public static readonly KEYCODE_BOOKMARK: number = 174; + public static readonly KEYCODE_BREAK: number = 121; + public static readonly KEYCODE_BRIGHTNESS_DOWN: number = 220; + public static readonly KEYCODE_BRIGHTNESS_UP: number = 221; + public static readonly KEYCODE_BUTTON_1: number = 188; + public static readonly KEYCODE_BUTTON_10: number = 197; + public static readonly KEYCODE_BUTTON_11: number = 198; + public static readonly KEYCODE_BUTTON_12: number = 199; + public static readonly KEYCODE_BUTTON_13: number = 200; + public static readonly KEYCODE_BUTTON_14: number = 201; + public static readonly KEYCODE_BUTTON_15: number = 202; + public static readonly KEYCODE_BUTTON_16: number = 203; + public static readonly KEYCODE_BUTTON_2: number = 189; + public static readonly KEYCODE_BUTTON_3: number = 190; + public static readonly KEYCODE_BUTTON_4: number = 191; + public static readonly KEYCODE_BUTTON_5: number = 192; + public static readonly KEYCODE_BUTTON_6: number = 193; + public static readonly KEYCODE_BUTTON_7: number = 194; + public static readonly KEYCODE_BUTTON_8: number = 195; + public static readonly KEYCODE_BUTTON_9: number = 196; + public static readonly KEYCODE_BUTTON_A: number = 96; + public static readonly KEYCODE_BUTTON_B: number = 97; + public static readonly KEYCODE_BUTTON_C: number = 98; + public static readonly KEYCODE_BUTTON_L1: number = 102; + public static readonly KEYCODE_BUTTON_L2: number = 104; + public static readonly KEYCODE_BUTTON_MODE: number = 110; + public static readonly KEYCODE_BUTTON_R1: number = 103; + public static readonly KEYCODE_BUTTON_R2: number = 105; + public static readonly KEYCODE_BUTTON_SELECT: number = 109; + public static readonly KEYCODE_BUTTON_START: number = 108; + public static readonly KEYCODE_BUTTON_THUMBL: number = 106; + public static readonly KEYCODE_BUTTON_THUMBR: number = 107; + public static readonly KEYCODE_BUTTON_X: number = 99; + public static readonly KEYCODE_BUTTON_Y: number = 100; + public static readonly KEYCODE_BUTTON_Z: number = 101; + public static readonly KEYCODE_C: number = 31; + public static readonly KEYCODE_CALCULATOR: number = 210; + public static readonly KEYCODE_CALENDAR: number = 208; + public static readonly KEYCODE_CALL: number = 5; + public static readonly KEYCODE_CAMERA: number = 27; + public static readonly KEYCODE_CAPS_LOCK: number = 115; + public static readonly KEYCODE_CAPTIONS: number = 175; + public static readonly KEYCODE_CHANNEL_DOWN: number = 167; + public static readonly KEYCODE_CHANNEL_UP: number = 166; + public static readonly KEYCODE_CLEAR: number = 28; + public static readonly KEYCODE_COMMA: number = 55; + public static readonly KEYCODE_CONTACTS: number = 207; + public static readonly KEYCODE_COPY: number = 278; + public static readonly KEYCODE_CTRL_LEFT: number = 113; + public static readonly KEYCODE_CTRL_RIGHT: number = 114; + public static readonly KEYCODE_CUT: number = 277; + public static readonly KEYCODE_D: number = 32; + public static readonly KEYCODE_DEL: number = 67; + public static readonly KEYCODE_DPAD_CENTER: number = 23; + public static readonly KEYCODE_DPAD_DOWN: number = 20; + public static readonly KEYCODE_DPAD_DOWN_LEFT: number = 269; + public static readonly KEYCODE_DPAD_DOWN_RIGHT: number = 271; + public static readonly KEYCODE_DPAD_LEFT: number = 21; + public static readonly KEYCODE_DPAD_RIGHT: number = 22; + public static readonly KEYCODE_DPAD_UP: number = 19; + public static readonly KEYCODE_DPAD_UP_LEFT: number = 268; + public static readonly KEYCODE_DPAD_UP_RIGHT: number = 270; + public static readonly KEYCODE_DVR: number = 173; + public static readonly KEYCODE_E: number = 33; + public static readonly KEYCODE_EISU: number = 212; + public static readonly KEYCODE_ENDCALL: number = 6; + public static readonly KEYCODE_ENTER: number = 66; + public static readonly KEYCODE_ENVELOPE: number = 65; + public static readonly KEYCODE_EQUALS: number = 70; + public static readonly KEYCODE_ESCAPE: number = 111; + public static readonly KEYCODE_EXPLORER: number = 64; + public static readonly KEYCODE_F: number = 34; + public static readonly KEYCODE_F1: number = 131; + public static readonly KEYCODE_F10: number = 140; + public static readonly KEYCODE_F11: number = 141; + public static readonly KEYCODE_F12: number = 142; + public static readonly KEYCODE_F2: number = 132; + public static readonly KEYCODE_F3: number = 133; + public static readonly KEYCODE_F4: number = 134; + public static readonly KEYCODE_F5: number = 135; + public static readonly KEYCODE_F6: number = 136; + public static readonly KEYCODE_F7: number = 137; + public static readonly KEYCODE_F8: number = 138; + public static readonly KEYCODE_F9: number = 139; + public static readonly KEYCODE_FOCUS: number = 80; + public static readonly KEYCODE_FORWARD: number = 125; + public static readonly KEYCODE_FORWARD_DEL: number = 112; + public static readonly KEYCODE_FUNCTION: number = 119; + public static readonly KEYCODE_G: number = 35; + public static readonly KEYCODE_GRAVE: number = 68; + public static readonly KEYCODE_GUIDE: number = 172; + public static readonly KEYCODE_H: number = 36; + public static readonly KEYCODE_HEADSETHOOK: number = 79; + public static readonly KEYCODE_HELP: number = 259; + public static readonly KEYCODE_HENKAN: number = 214; + public static readonly KEYCODE_HOME: number = 3; + public static readonly KEYCODE_I: number = 37; + public static readonly KEYCODE_INFO: number = 165; + public static readonly KEYCODE_INSERT: number = 124; + public static readonly KEYCODE_J: number = 38; + public static readonly KEYCODE_K: number = 39; + public static readonly KEYCODE_KANA: number = 218; + public static readonly KEYCODE_KATAKANA_HIRAGANA: number = 215; + public static readonly KEYCODE_L: number = 40; + public static readonly KEYCODE_LANGUAGE_SWITCH: number = 204; + public static readonly KEYCODE_LAST_CHANNEL: number = 229; + public static readonly KEYCODE_LEFT_BRACKET: number = 71; + public static readonly KEYCODE_M: number = 41; + public static readonly KEYCODE_MANNER_MODE: number = 205; + public static readonly KEYCODE_MEDIA_AUDIO_TRACK: number = 222; + public static readonly KEYCODE_MEDIA_CLOSE: number = 128; + public static readonly KEYCODE_MEDIA_EJECT: number = 129; + public static readonly KEYCODE_MEDIA_FAST_FORWARD: number = 90; + public static readonly KEYCODE_MEDIA_NEXT: number = 87; + public static readonly KEYCODE_MEDIA_PAUSE: number = 127; + public static readonly KEYCODE_MEDIA_PLAY: number = 126; + public static readonly KEYCODE_MEDIA_PLAY_PAUSE: number = 85; + public static readonly KEYCODE_MEDIA_PREVIOUS: number = 88; + public static readonly KEYCODE_MEDIA_RECORD: number = 130; + public static readonly KEYCODE_MEDIA_REWIND: number = 89; + public static readonly KEYCODE_MEDIA_SKIP_BACKWARD: number = 273; + public static readonly KEYCODE_MEDIA_SKIP_FORWARD: number = 272; + public static readonly KEYCODE_MEDIA_STEP_BACKWARD: number = 275; + public static readonly KEYCODE_MEDIA_STEP_FORWARD: number = 274; + public static readonly KEYCODE_MEDIA_STOP: number = 86; + public static readonly KEYCODE_MEDIA_TOP_MENU: number = 226; + public static readonly KEYCODE_MENU: number = 82; + public static readonly KEYCODE_META_LEFT: number = 117; + public static readonly KEYCODE_META_RIGHT: number = 118; + public static readonly KEYCODE_MINUS: number = 69; + public static readonly KEYCODE_MOVE_END: number = 123; + public static readonly KEYCODE_MOVE_HOME: number = 122; + public static readonly KEYCODE_MUHENKAN: number = 213; + public static readonly KEYCODE_MUSIC: number = 209; + public static readonly KEYCODE_MUTE: number = 91; + public static readonly KEYCODE_N: number = 42; + public static readonly KEYCODE_NAVIGATE_IN: number = 262; + public static readonly KEYCODE_NAVIGATE_NEXT: number = 261; + public static readonly KEYCODE_NAVIGATE_OUT: number = 263; + public static readonly KEYCODE_NAVIGATE_PREVIOUS: number = 260; + public static readonly KEYCODE_NOTIFICATION: number = 83; + public static readonly KEYCODE_NUM: number = 78; + public static readonly KEYCODE_NUMPAD_0: number = 144; + public static readonly KEYCODE_NUMPAD_1: number = 145; + public static readonly KEYCODE_NUMPAD_2: number = 146; + public static readonly KEYCODE_NUMPAD_3: number = 147; + public static readonly KEYCODE_NUMPAD_4: number = 148; + public static readonly KEYCODE_NUMPAD_5: number = 149; + public static readonly KEYCODE_NUMPAD_6: number = 150; + public static readonly KEYCODE_NUMPAD_7: number = 151; + public static readonly KEYCODE_NUMPAD_8: number = 152; + public static readonly KEYCODE_NUMPAD_9: number = 153; + public static readonly KEYCODE_NUMPAD_ADD: number = 157; + public static readonly KEYCODE_NUMPAD_COMMA: number = 159; + public static readonly KEYCODE_NUMPAD_DIVIDE: number = 154; + public static readonly KEYCODE_NUMPAD_DOT: number = 158; + public static readonly KEYCODE_NUMPAD_ENTER: number = 160; + public static readonly KEYCODE_NUMPAD_EQUALS: number = 161; + public static readonly KEYCODE_NUMPAD_LEFT_PAREN: number = 162; + public static readonly KEYCODE_NUMPAD_MULTIPLY: number = 155; + public static readonly KEYCODE_NUMPAD_RIGHT_PAREN: number = 163; + public static readonly KEYCODE_NUMPAD_SUBTRACT: number = 156; + public static readonly KEYCODE_NUM_LOCK: number = 143; + public static readonly KEYCODE_O: number = 43; + public static readonly KEYCODE_P: number = 44; + public static readonly KEYCODE_PAGE_DOWN: number = 93; + public static readonly KEYCODE_PAGE_UP: number = 92; + public static readonly KEYCODE_PAIRING: number = 225; + public static readonly KEYCODE_PASTE: number = 279; + public static readonly KEYCODE_PERIOD: number = 56; + public static readonly KEYCODE_PICTSYMBOLS: number = 94; + public static readonly KEYCODE_PLUS: number = 81; + public static readonly KEYCODE_POUND: number = 18; + public static readonly KEYCODE_POWER: number = 26; + public static readonly KEYCODE_PROFILE_SWITCH: number = 288; + public static readonly KEYCODE_PROG_BLUE: number = 186; + public static readonly KEYCODE_PROG_GREEN: number = 184; + public static readonly KEYCODE_PROG_RED: number = 183; + public static readonly KEYCODE_PROG_YELLOW: number = 185; + public static readonly KEYCODE_Q: number = 45; + public static readonly KEYCODE_R: number = 46; + public static readonly KEYCODE_REFRESH: number = 285; + public static readonly KEYCODE_RIGHT_BRACKET: number = 72; + public static readonly KEYCODE_RO: number = 217; + public static readonly KEYCODE_S: number = 47; + public static readonly KEYCODE_SCROLL_LOCK: number = 116; + public static readonly KEYCODE_SEARCH: number = 84; + public static readonly KEYCODE_SEMICOLON: number = 74; + public static readonly KEYCODE_SETTINGS: number = 176; + public static readonly KEYCODE_SHIFT_LEFT: number = 59; + public static readonly KEYCODE_SHIFT_RIGHT: number = 60; + public static readonly KEYCODE_SLASH: number = 76; + public static readonly KEYCODE_SLEEP: number = 223; + public static readonly KEYCODE_SOFT_LEFT: number = 1; + public static readonly KEYCODE_SOFT_RIGHT: number = 2; + public static readonly KEYCODE_SOFT_SLEEP: number = 276; + public static readonly KEYCODE_SPACE: number = 62; + public static readonly KEYCODE_STAR: number = 17; + public static readonly KEYCODE_STB_INPUT: number = 180; + public static readonly KEYCODE_STB_POWER: number = 179; + public static readonly KEYCODE_STEM_1: number = 265; + public static readonly KEYCODE_STEM_2: number = 266; + public static readonly KEYCODE_STEM_3: number = 267; + public static readonly KEYCODE_STEM_PRIMARY: number = 264; + public static readonly KEYCODE_SWITCH_CHARSET: number = 95; + public static readonly KEYCODE_SYM: number = 63; + public static readonly KEYCODE_SYSRQ: number = 120; + public static readonly KEYCODE_SYSTEM_NAVIGATION_DOWN: number = 281; + public static readonly KEYCODE_SYSTEM_NAVIGATION_LEFT: number = 282; + public static readonly KEYCODE_SYSTEM_NAVIGATION_RIGHT: number = 283; + public static readonly KEYCODE_SYSTEM_NAVIGATION_UP: number = 280; + public static readonly KEYCODE_T: number = 48; + public static readonly KEYCODE_TAB: number = 61; + public static readonly KEYCODE_THUMBS_DOWN: number = 287; + public static readonly KEYCODE_THUMBS_UP: number = 286; + public static readonly KEYCODE_TV: number = 170; + public static readonly KEYCODE_TV_ANTENNA_CABLE: number = 242; + public static readonly KEYCODE_TV_AUDIO_DESCRIPTION: number = 252; + public static readonly KEYCODE_TV_AUDIO_DESCRIPTION_MIX_DOWN: number = 254; + public static readonly KEYCODE_TV_AUDIO_DESCRIPTION_MIX_UP: number = 253; + public static readonly KEYCODE_TV_CONTENTS_MENU: number = 256; + public static readonly KEYCODE_TV_DATA_SERVICE: number = 230; + public static readonly KEYCODE_TV_INPUT: number = 178; + public static readonly KEYCODE_TV_INPUT_COMPONENT_1: number = 249; + public static readonly KEYCODE_TV_INPUT_COMPONENT_2: number = 250; + public static readonly KEYCODE_TV_INPUT_COMPOSITE_1: number = 247; + public static readonly KEYCODE_TV_INPUT_COMPOSITE_2: number = 248; + public static readonly KEYCODE_TV_INPUT_HDMI_1: number = 243; + public static readonly KEYCODE_TV_INPUT_HDMI_2: number = 244; + public static readonly KEYCODE_TV_INPUT_HDMI_3: number = 245; + public static readonly KEYCODE_TV_INPUT_HDMI_4: number = 246; + public static readonly KEYCODE_TV_INPUT_VGA_1: number = 251; + public static readonly KEYCODE_TV_MEDIA_CONTEXT_MENU: number = 257; + public static readonly KEYCODE_TV_NETWORK: number = 241; + public static readonly KEYCODE_TV_NUMBER_ENTRY: number = 234; + public static readonly KEYCODE_TV_POWER: number = 177; + public static readonly KEYCODE_TV_RADIO_SERVICE: number = 232; + public static readonly KEYCODE_TV_SATELLITE: number = 237; + public static readonly KEYCODE_TV_SATELLITE_BS: number = 238; + public static readonly KEYCODE_TV_SATELLITE_CS: number = 239; + public static readonly KEYCODE_TV_SATELLITE_SERVICE: number = 240; + public static readonly KEYCODE_TV_TELETEXT: number = 233; + public static readonly KEYCODE_TV_TERRESTRIAL_ANALOG: number = 235; + public static readonly KEYCODE_TV_TERRESTRIAL_DIGITAL: number = 236; + public static readonly KEYCODE_TV_TIMER_PROGRAMMING: number = 258; + public static readonly KEYCODE_TV_ZOOM_MODE: number = 255; + public static readonly KEYCODE_U: number = 49; + public static readonly KEYCODE_UNKNOWN: number = 0; + public static readonly KEYCODE_V: number = 50; + public static readonly KEYCODE_VOICE_ASSIST: number = 231; + public static readonly KEYCODE_VOLUME_DOWN: number = 25; + public static readonly KEYCODE_VOLUME_MUTE: number = 164; + public static readonly KEYCODE_VOLUME_UP: number = 24; + public static readonly KEYCODE_W: number = 51; + public static readonly KEYCODE_WAKEUP: number = 224; + public static readonly KEYCODE_WINDOW: number = 171; + public static readonly KEYCODE_X: number = 52; + public static readonly KEYCODE_Y: number = 53; + public static readonly KEYCODE_YEN: number = 216; + public static readonly KEYCODE_Z: number = 54; + public static readonly KEYCODE_ZENKAKU_HANKAKU: number = 211; + public static readonly KEYCODE_ZOOM_IN: number = 168; + public static readonly KEYCODE_ZOOM_OUT: number = 169; + + public static readonly META_ALT_LEFT_ON: number = 16; + public static readonly META_ALT_MASK: number = 50; + public static readonly META_ALT_ON: number = 2; + public static readonly META_ALT_RIGHT_ON: number = 32; + public static readonly META_CAPS_LOCK_ON: number = 1048576; + public static readonly META_CTRL_LEFT_ON: number = 8192; + public static readonly META_CTRL_MASK: number = 28672; + public static readonly META_CTRL_ON: number = 4096; + public static readonly META_CTRL_RIGHT_ON: number = 16384; + public static readonly META_FUNCTION_ON: number = 8; + public static readonly META_META_LEFT_ON: number = 131072; + public static readonly META_META_MASK: number = 458752; + public static readonly META_META_ON: number = 65536; + public static readonly META_META_RIGHT_ON: number = 262144; + public static readonly META_NUM_LOCK_ON: number = 2097152; + public static readonly META_SCROLL_LOCK_ON: number = 4194304; + public static readonly META_SHIFT_LEFT_ON: number = 64; + public static readonly META_SHIFT_MASK: number = 193; + public static readonly META_SHIFT_ON: number = 1; + public static readonly META_SHIFT_RIGHT_ON: number = 128; + public static readonly META_SYM_ON: number = 4; +} diff --git a/src/app/googDevice/android/MediaFormat.ts b/src/app/googDevice/android/MediaFormat.ts new file mode 100644 index 0000000..0df3307 --- /dev/null +++ b/src/app/googDevice/android/MediaFormat.ts @@ -0,0 +1,935 @@ +export default class MediaFormat { + public static readonly MIMETYPE_VIDEO_VP8: string = 'video/x-vnd.on2.vp8'; + public static readonly MIMETYPE_VIDEO_VP9: string = 'video/x-vnd.on2.vp9'; + public static readonly MIMETYPE_VIDEO_AV1: string = 'video/av01'; + public static readonly MIMETYPE_VIDEO_AVC: string = 'video/avc'; + public static readonly MIMETYPE_VIDEO_HEVC: string = 'video/hevc'; + public static readonly MIMETYPE_VIDEO_MPEG4: string = 'video/mp4v-es'; + public static readonly MIMETYPE_VIDEO_H263: string = 'video/3gpp'; + public static readonly MIMETYPE_VIDEO_MPEG2: string = 'video/mpeg2'; + public static readonly MIMETYPE_VIDEO_RAW: string = 'video/raw'; + public static readonly MIMETYPE_VIDEO_DOLBY_VISION: string = 'video/dolby-vision'; + public static readonly MIMETYPE_VIDEO_SCRAMBLED: string = 'video/scrambled'; + + public static readonly MIMETYPE_AUDIO_AMR_NB: string = 'audio/3gpp'; + public static readonly MIMETYPE_AUDIO_AMR_WB: string = 'audio/amr-wb'; + public static readonly MIMETYPE_AUDIO_MPEG: string = 'audio/mpeg'; + public static readonly MIMETYPE_AUDIO_AAC: string = 'audio/mp4a-latm'; + public static readonly MIMETYPE_AUDIO_QCELP: string = 'audio/qcelp'; + public static readonly MIMETYPE_AUDIO_VORBIS: string = 'audio/vorbis'; + public static readonly MIMETYPE_AUDIO_OPUS: string = 'audio/opus'; + public static readonly MIMETYPE_AUDIO_G711_ALAW: string = 'audio/g711-alaw'; + public static readonly MIMETYPE_AUDIO_G711_MLAW: string = 'audio/g711-mlaw'; + public static readonly MIMETYPE_AUDIO_RAW: string = 'audio/raw'; + public static readonly MIMETYPE_AUDIO_FLAC: string = 'audio/flac'; + public static readonly MIMETYPE_AUDIO_MSGSM: string = 'audio/gsm'; + public static readonly MIMETYPE_AUDIO_AC3: string = 'audio/ac3'; + public static readonly MIMETYPE_AUDIO_EAC3: string = 'audio/eac3'; + public static readonly MIMETYPE_AUDIO_EAC3_JOC: string = 'audio/eac3-joc'; + public static readonly MIMETYPE_AUDIO_AC4: string = 'audio/ac4'; + public static readonly MIMETYPE_AUDIO_SCRAMBLED: string = 'audio/scrambled'; + + /** + * MIME type for HEIF still image data encoded in HEVC. + * + * To decode such an image, {@link MediaCodec} decoder for + * {@link #MIMETYPE_VIDEO_HEVC} shall be used. The client needs to form + * the correct {@link #MediaFormat} based on additional information in + * the track format, and send it to {@link MediaCodec#configure}. + * + * The track's MediaFormat will come with {@link #KEY_WIDTH} and + * {@link #KEY_HEIGHT} keys, which describes the width and height + * of the image. If the image doesn't contain grid (i.e. none of + * {@link #KEY_TILE_WIDTH}, {@link #KEY_TILE_HEIGHT}, + * {@link #KEY_GRID_ROWS}, {@link #KEY_GRID_COLUMNS} are present}), the + * track will contain a single sample of coded data for the entire image, + * and the image width and height should be used to set up the decoder. + * + * If the image does come with grid, each sample from the track will + * contain one tile in the grid, of which the size is described by + * {@link #KEY_TILE_WIDTH} and {@link #KEY_TILE_HEIGHT}. This size + * (instead of {@link #KEY_WIDTH} and {@link #KEY_HEIGHT}) should be + * used to set up the decoder. The track contains {@link #KEY_GRID_ROWS} + * by {@link #KEY_GRID_COLUMNS} samples in row-major, top-row first, + * left-to-right order. The output image should be reconstructed by + * first tiling the decoding results of the tiles in the correct order, + * then trimming (before rotation is applied) on the bottom and right + * side, if the tiled area is larger than the image width and height. + */ + public static readonly MIMETYPE_IMAGE_ANDROID_HEIC: string = 'image/vnd.android.heic'; + + /** + * MIME type for WebVTT subtitle data. + */ + public static readonly MIMETYPE_TEXT_VTT: string = 'text/vtt'; + + /** + * MIME type for SubRip (SRT) container. + */ + public static readonly MIMETYPE_TEXT_SUBRIP: string = 'application/x-subrip'; + + /** + * MIME type for CEA-608 closed caption data. + */ + public static readonly MIMETYPE_TEXT_CEA_608: string = 'text/cea-608'; + + /** + * MIME type for CEA-708 closed caption data. + */ + public static readonly MIMETYPE_TEXT_CEA_708: string = 'text/cea-708'; + + // private mMap: Map = new Map(); + + /** + * A key describing the mime type of the MediaFormat. + * The associated value is a string. + */ + public static readonly KEY_MIME: string = 'mime'; + + /** + * A key describing the language of the content, using either ISO 639-1 + * or 639-2/T codes. The associated value is a string. + */ + public static readonly KEY_LANGUAGE: string = 'language'; + + /** + * A key describing the sample rate of an audio format. + * The associated value is an integer + */ + public static readonly KEY_SAMPLE_RATE: string = 'sample-rate'; + + /** + * A key describing the number of channels in an audio format. + * The associated value is an integer + */ + public static readonly KEY_CHANNEL_COUNT: string = 'channel-count'; + + /** + * A key describing the width of the content in a video format. + * The associated value is an integer + */ + public static readonly KEY_WIDTH: string = 'width'; + + /** + * A key describing the height of the content in a video format. + * The associated value is an integer + */ + public static readonly KEY_HEIGHT: string = 'height'; + + /** + * A key describing the maximum expected width of the content in a video + * decoder format, in case there are resolution changes in the video content. + * The associated value is an integer + */ + public static readonly KEY_MAX_WIDTH: string = 'max-width'; + + /** + * A key describing the maximum expected height of the content in a video + * decoder format, in case there are resolution changes in the video content. + * The associated value is an integer + */ + public static readonly KEY_MAX_HEIGHT: string = 'max-height'; + + /** A key describing the maximum size in bytes of a buffer of data + * described by this MediaFormat. + * The associated value is an integer + */ + public static readonly KEY_MAX_INPUT_SIZE: string = 'max-input-size'; + + /** + * A key describing the average bitrate in bits/sec. + * The associated value is an integer + */ + public static readonly KEY_BIT_RATE: string = 'bitrate'; + + /** + * A key describing the max bitrate in bits/sec. + * This is usually over a one-second sliding window (e.g. over any window of one second). + * The associated value is an integer + * @hide + */ + public static readonly KEY_MAX_BIT_RATE: string = 'max-bitrate'; + + /** + * A key describing the color format of the content in a video format. + * Constants are declared in {@link android.media.MediaCodecInfo.CodecCapabilities}. + */ + public static readonly KEY_COLOR_FORMAT: string = 'color-format'; + + /** + * A key describing the frame rate of a video format in frames/sec. + * The associated value is normally an integer when the value is used by the platform, + * but video codecs also accept float configuration values. + * Specifically, {@link MediaExtractor#getTrackFormat MediaExtractor} provides an integer + * value corresponding to the frame rate information of the track if specified and non-zero. + * Otherwise, this key is not present. {@link MediaCodec#configure MediaCodec} accepts both + * float and integer values. This represents the desired operating frame rate if the + * {@link #KEY_OPERATING_RATE} is not present and {@link #KEY_PRIORITY} is {@code 0} + * (realtime). For video encoders this value corresponds to the intended frame rate, + * although encoders are expected + * to support variable frame rate based on {@link MediaCodec.BufferInfo#presentationTimeUs + * buffer timestamp}. This key is not used in the {@code MediaCodec} + * {@link MediaCodec#getInputFormat input}/{@link MediaCodec#getOutputFormat output} formats, + * nor by {@link MediaMuxer#addTrack MediaMuxer}. + */ + public static readonly KEY_FRAME_RATE: string = 'frame-rate'; + + /** + * A key describing the width (in pixels) of each tile of the content in a + * {@link #MIMETYPE_IMAGE_ANDROID_HEIC} track. The associated value is an integer. + * + * Refer to {@link #MIMETYPE_IMAGE_ANDROID_HEIC} on decoding instructions of such tracks. + * + * @see #KEY_TILE_HEIGHT + * @see #KEY_GRID_ROWS + * @see #KEY_GRID_COLUMNS + */ + public static readonly KEY_TILE_WIDTH: string = 'tile-width'; + + /** + * A key describing the height (in pixels) of each tile of the content in a + * {@link #MIMETYPE_IMAGE_ANDROID_HEIC} track. The associated value is an integer. + * + * Refer to {@link #MIMETYPE_IMAGE_ANDROID_HEIC} on decoding instructions of such tracks. + * + * @see #KEY_TILE_WIDTH + * @see #KEY_GRID_ROWS + * @see #KEY_GRID_COLUMNS + */ + public static readonly KEY_TILE_HEIGHT: string = 'tile-height'; + + /** + * A key describing the number of grid rows in the content in a + * {@link #MIMETYPE_IMAGE_ANDROID_HEIC} track. The associated value is an integer. + * + * Refer to {@link #MIMETYPE_IMAGE_ANDROID_HEIC} on decoding instructions of such tracks. + * + * @see #KEY_TILE_WIDTH + * @see #KEY_TILE_HEIGHT + * @see #KEY_GRID_COLUMNS + */ + public static readonly KEY_GRID_ROWS: string = 'grid-rows'; + + /** + * A key describing the number of grid columns in the content in a + * {@link #MIMETYPE_IMAGE_ANDROID_HEIC} track. The associated value is an integer. + * + * Refer to {@link #MIMETYPE_IMAGE_ANDROID_HEIC} on decoding instructions of such tracks. + * + * @see #KEY_TILE_WIDTH + * @see #KEY_TILE_HEIGHT + * @see #KEY_GRID_ROWS + */ + public static readonly KEY_GRID_COLUMNS: string = 'grid-cols'; + + /** + * A key describing the raw audio sample encoding/format. + * + *

The associated value is an integer, using one of the + * {@link AudioFormat}.ENCODING_PCM_ values.

+ * + *

This is an optional key for audio decoders and encoders specifying the + * desired raw audio sample format during {@link MediaCodec#configure + * MediaCodec.configure(…)} call. Use {@link MediaCodec#getInputFormat + * MediaCodec.getInput}/{@link MediaCodec#getOutputFormat OutputFormat(…)} + * to confirm the actual format. For the PCM decoder this key specifies both + * input and output sample encodings.

+ * + *

This key is also used by {@link MediaExtractor} to specify the sample + * format of audio data, if it is specified.

+ * + *

If this key is missing, the raw audio sample format is signed 16-bit short.

+ */ + public static readonly KEY_PCM_ENCODING: string = 'pcm-encoding'; + + /** + * A key describing the capture rate of a video format in frames/sec. + *

+ * When capture rate is different than the frame rate, it means that the + * video is acquired at a different rate than the playback, which produces + * slow motion or timelapse effect during playback. Application can use the + * value of this key to tell the relative speed ratio between capture and + * playback rates when the video was recorded. + *

+ *

+ * The associated value is an integer or a float. + *

+ */ + public static readonly KEY_CAPTURE_RATE: string = 'capture-rate'; + + /** + * A key describing the frequency of key frames expressed in seconds between key frames. + *

+ * This key is used by video encoders. + * A negative value means no key frames are requested after the first frame. + * A zero value means a stream containing all key frames is requested. + *

+ * Most video encoders will convert this value of the number of non-key-frames between + * key-frames, using the {@linkplain #KEY_FRAME_RATE frame rate} information; therefore, + * if the actual frame rate differs (e.g. input frames are dropped or the frame rate + * changes), the time interval between key frames will not be the + * configured value. + *

+ * The associated value is an integer (or float since + * {@link android.os.Build.VERSION_CODES#N_MR1}). + */ + public static readonly KEY_I_FRAME_INTERVAL: string = 'i-frame-interval'; + + /** + * An optional key describing the period of intra refresh in frames. This is an + * optional parameter that applies only to video encoders. If encoder supports it + * ({@link MediaCodecInfo.CodecCapabilities#FEATURE_IntraRefresh}), the whole + * frame is completely refreshed after the specified period. Also for each frame, + * a fix subset of macroblocks must be intra coded which leads to more constant bitrate + * than inserting a key frame. This key is recommended for video streaming applications + * as it provides low-delay and good error-resilience. This key is ignored if the + * video encoder does not support the intra refresh feature. Use the output format to + * verify that this feature was enabled. + * The associated value is an integer. + */ + public static readonly KEY_INTRA_REFRESH_PERIOD: string = 'intra-refresh-period'; + + /** + * An optional key describing whether encoders prepend headers to sync frames (e.g. + * SPS and PPS to IDR frames for H.264). This is an optional parameter that applies only + * to video encoders. A video encoder may not support this feature; the component will fail + * to configure in that case. For other components, this key is ignored. + * + * The value is an integer, with 1 indicating to prepend headers to every sync frames, + * or 0 otherwise. The default value is 0. + */ + public static readonly KEY_PREPEND_HEADER_TO_SYNC_FRAMES: string = 'prepend-sps-pps-to-idr-frames'; + + /** + * A key describing the temporal layering schema. This is an optional parameter + * that applies only to video encoders. Use {@link MediaCodec#getOutputFormat} + * after {@link MediaCodec#configure configure} to query if the encoder supports + * the desired schema. Supported values are {@code webrtc.vp8.N-layer}, + * {@code android.generic.N}, {@code android.generic.N+M} and {@code none}, where + * {@code N} denotes the total number of non-bidirectional layers (which must be at least 1) + * and {@code M} denotes the total number of bidirectional layers (which must be non-negative). + *

{@code android.generic.*} schemas have been added in {@link + * android.os.Build.VERSION_CODES#N_MR1}. + *

+ * The encoder may support fewer temporal layers, in which case the output format + * will contain the configured schema. If the encoder does not support temporal + * layering, the output format will not have an entry with this key. + * The associated value is a string. + */ + public static readonly KEY_TEMPORAL_LAYERING: string = 'ts-schema'; + + /** + * A key describing the stride of the video bytebuffer layout. + * Stride (or row increment) is the difference between the index of a pixel + * and that of the pixel directly underneath. For YUV 420 formats, the + * stride corresponds to the Y plane; the stride of the U and V planes can + * be calculated based on the color format, though it is generally undefined + * and depends on the device and release. + * The associated value is an integer, representing number of bytes. + */ + public static readonly KEY_STRIDE: string = 'stride'; + + /** + * A key describing the plane height of a multi-planar (YUV) video bytebuffer layout. + * Slice height (or plane height/vertical stride) is the number of rows that must be skipped + * to get from the top of the Y plane to the top of the U plane in the bytebuffer. In essence + * the offset of the U plane is sliceHeight * stride. The height of the U/V planes + * can be calculated based on the color format, though it is generally undefined + * and depends on the device and release. + * The associated value is an integer, representing number of rows. + */ + public static readonly KEY_SLICE_HEIGHT: string = 'slice-height'; + + /** + * Applies only when configuring a video encoder in "surface-input" mode. + * The associated value is a long and gives the time in microseconds + * after which the frame previously submitted to the encoder will be + * repeated (once) if no new frame became available since. + */ + public static readonly KEY_REPEAT_PREVIOUS_FRAME_AFTER: string = 'repeat-previous-frame-after'; + + /** + * Instruct the video encoder in "surface-input" mode to drop excessive + * frames from the source, so that the input frame rate to the encoder + * does not exceed the specified fps. + * + * The associated value is a float, representing the max frame rate to + * feed the encoder at. + * + */ + public static readonly KEY_MAX_FPS_TO_ENCODER: string = 'max-fps-to-encoder'; + + /** + * Instruct the video encoder in "surface-input" mode to limit the gap of + * timestamp between any two adjacent frames fed to the encoder to the + * specified amount (in micro-second). + * + * The associated value is a long int. When positive, it represents the max + * timestamp gap between two adjacent frames fed to the encoder. When negative, + * the absolute value represents a fixed timestamp gap between any two adjacent + * frames fed to the encoder. Note that this will also apply even when the + * original timestamp goes backward in time. Under normal conditions, such frames + * would be dropped and not sent to the encoder. + * + * The output timestamp will be restored to the original timestamp and will + * not be affected. + * + * This is used in some special scenarios where input frames arrive sparingly + * but it's undesirable to allocate more bits to any single frame, or when it's + * important to ensure all frames are captured (rather than captured in the + * correct order). + * + */ + public static readonly KEY_MAX_PTS_GAP_TO_ENCODER: string = 'max-pts-gap-to-encoder'; + + /** + * If specified when configuring a video encoder that's in "surface-input" + * mode, it will instruct the encoder to put the surface source in suspended + * state when it's connected. No video frames will be accepted until a resume + * operation (see {@link MediaCodec#PARAMETER_KEY_SUSPEND}), optionally with + * timestamp specified via {@link MediaCodec#PARAMETER_KEY_SUSPEND_TIME}, is + * received. + * + * The value is an integer, with 1 indicating to create with the surface + * source suspended, or 0 otherwise. The default value is 0. + * + * If this key is not set or set to 0, the surface source will accept buffers + * as soon as it's connected to the encoder (although they may not be encoded + * immediately). This key can be used when the client wants to prepare the + * encoder session in advance, but do not want to accept buffers immediately. + */ + public static readonly KEY_CREATE_INPUT_SURFACE_SUSPENDED: string = 'create-input-buffers-suspended'; + + /** + * If specified when configuring a video decoder rendering to a surface, + * causes the decoder to output "blank", i.e. black frames to the surface + * when stopped to clear out any previously displayed contents. + * The associated value is an integer of value 1. + */ + public static readonly KEY_PUSH_BLANK_BUFFERS_ON_STOP: string = 'push-blank-buffers-on-shutdown'; + + /** + * A key describing the duration (in microseconds) of the content. + * The associated value is a long. + */ + public static readonly KEY_DURATION: string = 'durationUs'; + + /** + * A key mapping to a value of 1 if the content is AAC audio and + * audio frames are prefixed with an ADTS header. + * The associated value is an integer (0 or 1). + * This key is only supported when _decoding_ content, it cannot + * be used to configure an encoder to emit ADTS output. + */ + public static readonly KEY_IS_ADTS: string = 'is-adts'; + + /** + * A key describing the channel composition of audio content. This mask + * is composed of bits drawn from channel mask definitions in {@link android.media.AudioFormat}. + * The associated value is an integer. + */ + public static readonly KEY_CHANNEL_MASK: string = 'channel-mask'; + + /** + * A key describing the AAC profile to be used (AAC audio formats only). + * Constants are declared in {@link android.media.MediaCodecInfo.CodecProfileLevel}. + */ + public static readonly KEY_AAC_PROFILE: string = 'aac-profile'; + + /** + * A key describing the AAC SBR mode to be used (AAC audio formats only). + * The associated value is an integer and can be set to following values: + *

    + *
  • 0 - no SBR should be applied
  • + *
  • 1 - single rate SBR
  • + *
  • 2 - double rate SBR
  • + *
+ * Note: If this key is not defined the default SRB mode for the desired AAC profile will + * be used. + *

This key is only used during encoding. + */ + public static readonly KEY_AAC_SBR_MODE: string = 'aac-sbr-mode'; + + /** + * A key describing the maximum number of channels that can be output by the AAC decoder. + * By default, the decoder will output the same number of channels as present in the encoded + * stream, if supported. Set this value to limit the number of output channels, and use + * the downmix information in the stream, if available. + *

Values larger than the number of channels in the content to decode are ignored. + *

This key is only used during decoding. + */ + public static readonly KEY_AAC_MAX_OUTPUT_CHANNEL_COUNT: string = 'aac-max-output-channel_count'; + + /** + * A key describing a gain to be applied so that the output loudness matches the + * Target Reference Level. This is typically used to normalize loudness across program items. + * The gain is derived as the difference between the Target Reference Level and the + * Program Reference Level. The latter can be given in the bitstream and indicates the actual + * loudness value of the program item. + *

The Target Reference Level controls loudness normalization for both MPEG-4 DRC and + * MPEG-D DRC. + *

The value is given as an integer value between + * 40 and 127, and is calculated as -4 * Target Reference Level in LKFS. + * Therefore, it represents the range of -10 to -31.75 LKFS. + *

The default value on mobile devices is 64 (-16 LKFS). + *

This key is only used during decoding. + */ + public static readonly KEY_AAC_DRC_TARGET_REFERENCE_LEVEL: string = 'aac-target-ref-level'; + + /** + * A key describing for selecting the DRC effect type for MPEG-D DRC. + * The supported values are defined in ISO/IEC 23003-4:2015 and are described as follows: + * + * + * + * + * + * + * + * + * + * + *
ValueEffect
-1Off
0None
1Late night
2Noisy environment
3Limited playback range
4Low playback level
5Dialog enhancement
6General compression
+ *

The value -1 (Off) disables DRC processing, while loudness normalization may still be + * active and dependent on KEY_AAC_DRC_TARGET_REFERENCE_LEVEL.
+ * The value 0 (None) automatically enables DRC processing if necessary to prevent signal + * clipping
+ * The value 6 (General compression) can be used for enabling MPEG-D DRC without particular + * DRC effect type request.
+ * The default DRC effect type is 3 ("Limited playback range") on mobile devices. + *

This key is only used during decoding. + */ + public static readonly KEY_AAC_DRC_EFFECT_TYPE: string = 'aac-drc-effect-type'; + + /** + * A key describing the target reference level that was assumed at the encoder for + * calculation of attenuation gains for clipping prevention. + *

If it is known, this information can be provided as an integer value between + * 0 and 127, which is calculated as -4 * Encoded Target Level in LKFS. + * If the Encoded Target Level is unknown, the value can be set to -1. + *

The default value is -1 (unknown). + *

The value is ignored when heavy compression is used (see + * {@link #KEY_AAC_DRC_HEAVY_COMPRESSION}). + *

This key is only used during decoding. + */ + public static readonly KEY_AAC_ENCODED_TARGET_LEVEL: string = 'aac-encoded-target-level'; + + /** + * A key describing the boost factor allowing to adapt the dynamics of the output to the + * actual listening requirements. This relies on DRC gain sequences that can be transmitted in + * the encoded bitstream to be able to reduce the dynamics of the output signal upon request. + * This factor enables the user to select how much of the gains are applied. + *

Positive gains (boost) and negative gains (attenuation, see + * {@link #KEY_AAC_DRC_ATTENUATION_FACTOR}) can be controlled separately for a better match + * to different use-cases. + *

Typically, attenuation gains are sent for loud signal segments, and boost gains are sent + * for soft signal segments. If the output is listened to in a noisy environment, for example, + * the boost factor is used to enable the positive gains, i.e. to amplify soft signal segments + * beyond the noise floor. But for listening late at night, the attenuation + * factor is used to enable the negative gains, to prevent loud signal from surprising + * the listener. In applications which generally need a low dynamic range, both the boost factor + * and the attenuation factor are used in order to enable all DRC gains. + *

In order to prevent clipping, it is also recommended to apply the attenuation gains + * in case of a downmix and/or loudness normalization to high target reference levels. + *

Both the boost and the attenuation factor parameters are given as integer values + * between 0 and 127, representing the range of the factor of 0 (i.e. don't apply) + * to 1 (i.e. fully apply boost/attenuation gains respectively). + *

The default value is 127 (fully apply boost DRC gains). + *

This key is only used during decoding. + */ + public static readonly KEY_AAC_DRC_BOOST_FACTOR: string = 'aac-drc-boost-level'; + + /** + * A key describing the attenuation factor allowing to adapt the dynamics of the output to the + * actual listening requirements. + * See {@link #KEY_AAC_DRC_BOOST_FACTOR} for a description of the role of this attenuation + * factor and the value range. + *

The default value is 127 (fully apply attenuation DRC gains). + *

This key is only used during decoding. + */ + public static readonly KEY_AAC_DRC_ATTENUATION_FACTOR: string = 'aac-drc-cut-level'; + + /** + * A key describing the selection of the heavy compression profile for DRC. + * Two separate DRC gain sequences can be transmitted in one bitstream: MPEG-4 DRC light + * compression, and DVB-specific heavy compression. When selecting the application of the heavy + * compression, one of the sequences is selected: + *

    + *
  • 0 enables light compression,
  • + *
  • 1 enables heavy compression instead. + *
+ * Note that only light compression offers the features of scaling of DRC gains + * (see {@link #KEY_AAC_DRC_BOOST_FACTOR} and {@link #KEY_AAC_DRC_ATTENUATION_FACTOR} for the + * boost and attenuation factors, and frequency-selective (multiband) DRC. + * Light compression usually contains clipping prevention for stereo downmixing while heavy + * compression, if additionally provided in the bitstream, is usually stronger, and contains + * clipping prevention for stereo and mono downmixing. + *

The default is 1 (heavy compression). + *

This key is only used during decoding. + */ + public static readonly KEY_AAC_DRC_HEAVY_COMPRESSION: string = 'aac-drc-heavy-compression'; + + /** + * A key describing the FLAC compression level to be used (FLAC audio format only). + * The associated value is an integer ranging from 0 (fastest, least compression) + * to 8 (slowest, most compression). + */ + public static readonly KEY_FLAC_COMPRESSION_LEVEL: string = 'flac-compression-level'; + + /** + * A key describing the encoding complexity. + * The associated value is an integer. These values are device and codec specific, + * but lower values generally result in faster and/or less power-hungry encoding. + * + * @see MediaCodecInfo.EncoderCapabilities#getComplexityRange() + */ + public static readonly KEY_COMPLEXITY: string = 'complexity'; + + /** + * A key describing the desired encoding quality. + * The associated value is an integer. This key is only supported for encoders + * that are configured in constant-quality mode. These values are device and + * codec specific, but lower values generally result in more efficient + * (smaller-sized) encoding. + * + * @see MediaCodecInfo.EncoderCapabilities#getQualityRange() + */ + public static readonly KEY_QUALITY: string = 'quality'; + + /** + * A key describing the desired codec priority. + *

+ * The associated value is an integer. Higher value means lower priority. + *

+ * Currently, only two levels are supported:
+ * 0: realtime priority - meaning that the codec shall support the given + * performance configuration (e.g. framerate) at realtime. This should + * only be used by media playback, capture, and possibly by realtime + * communication scenarios if best effort performance is not suitable.
+ * 1: non-realtime priority (best effort). + *

+ * This is a hint used at codec configuration and resource planning - to understand + * the realtime requirements of the application; however, due to the nature of + * media components, performance is not guaranteed. + * + */ + public static readonly KEY_PRIORITY: string = 'priority'; + + /** + * A key describing the desired operating frame rate for video or sample rate for audio + * that the codec will need to operate at. + *

+ * The associated value is an integer or a float representing frames-per-second or + * samples-per-second + *

+ * This is used for cases like high-speed/slow-motion video capture, where the video encoder + * format contains the target playback rate (e.g. 30fps), but the component must be able to + * handle the high operating capture rate (e.g. 240fps). + *

+ * This rate will be used by codec for resource planning and setting the operating points. + * + */ + public static readonly KEY_OPERATING_RATE: string = 'operating-rate'; + + /** + * A key describing the desired profile to be used by an encoder. + * The associated value is an integer. + * Constants are declared in {@link MediaCodecInfo.CodecProfileLevel}. + * This key is used as a hint, and is only supported for codecs + * that specify a profile. Note: Codecs are free to use all the available + * coding tools at the specified profile. + * + * @see MediaCodecInfo.CodecCapabilities#profileLevels + */ + public static readonly KEY_PROFILE: string = 'profile'; + + /** + * A key describing the desired profile to be used by an encoder. + * The associated value is an integer. + * Constants are declared in {@link MediaCodecInfo.CodecProfileLevel}. + * This key is used as a further hint when specifying a desired profile, + * and is only supported for codecs that specify a level. + *

+ * This key is ignored if the {@link #KEY_PROFILE profile} is not specified. + * + * @see MediaCodecInfo.CodecCapabilities#profileLevels + */ + public static readonly KEY_LEVEL: string = 'level'; + + /** + * An optional key describing the desired encoder latency in frames. This is an optional + * parameter that applies only to video encoders. If encoder supports it, it should ouput + * at least one output frame after being queued the specified number of frames. This key + * is ignored if the video encoder does not support the latency feature. Use the output + * format to verify that this feature was enabled and the actual value used by the encoder. + *

+ * If the key is not specified, the default latency will be implenmentation specific. + * The associated value is an integer. + */ + public static readonly KEY_LATENCY: string = 'latency'; + + /** + * An optional key describing the maximum number of non-display-order coded frames. + * This is an optional parameter that applies only to video encoders. Application should + * check the value for this key in the output format to see if codec will produce + * non-display-order coded frames. If encoder supports it, the output frames' order will be + * different from the display order and each frame's display order could be retrived from + * {@link MediaCodec.BufferInfo#presentationTimeUs}. Before API level 27, application may + * receive non-display-order coded frames even though the application did not request it. + * Note: Application should not rearrange the frames to display order before feeding them + * to {@link MediaMuxer#writeSampleData}. + *

+ * The default value is 0. + */ + public static readonly KEY_OUTPUT_REORDER_DEPTH: string = 'output-reorder-depth'; + + /** + * A key describing the desired clockwise rotation on an output surface. + * This key is only used when the codec is configured using an output surface. + * The associated value is an integer, representing degrees. Supported values + * are 0, 90, 180 or 270. This is an optional field; if not specified, rotation + * defaults to 0. + * + * @see MediaCodecInfo.CodecCapabilities#profileLevels + */ + public static readonly KEY_ROTATION: string = 'rotation-degrees'; + + /** + * A key describing the desired bitrate mode to be used by an encoder. + * Constants are declared in {@link MediaCodecInfo.CodecCapabilities}. + * + * @see MediaCodecInfo.EncoderCapabilities#isBitrateModeSupported(int) + */ + public static readonly KEY_BITRATE_MODE: string = 'bitrate-mode'; + + /** + * A key describing the audio session ID of the AudioTrack associated + * to a tunneled video codec. + * The associated value is an integer. + * + * @see MediaCodecInfo.CodecCapabilities#FEATURE_TunneledPlayback + */ + public static readonly KEY_AUDIO_SESSION_ID: string = 'audio-session-id'; + + /** + * A key for boolean AUTOSELECT behavior for the track. Tracks with AUTOSELECT=true + * are considered when automatically selecting a track without specific user + * choice, based on the current locale. + * This is currently only used for subtitle tracks, when the user selected + * 'Default' for the captioning locale. + * The associated value is an integer, where non-0 means TRUE. This is an optional + * field; if not specified, AUTOSELECT defaults to TRUE. + */ + public static readonly KEY_IS_AUTOSELECT: string = 'is-autoselect'; + + /** + * A key for boolean DEFAULT behavior for the track. The track with DEFAULT=true is + * selected in the absence of a specific user choice. + * This is currently used in two scenarios: + * 1) for subtitle tracks, when the user selected 'Default' for the captioning locale. + * 2) for a {@link #MIMETYPE_IMAGE_ANDROID_HEIC} track, indicating the image is the + * primary item in the file. + + * The associated value is an integer, where non-0 means TRUE. This is an optional + * field; if not specified, DEFAULT is considered to be FALSE. + */ + public static readonly KEY_IS_DEFAULT: string = 'is-default'; + + /** + * A key for the FORCED field for subtitle tracks. True if it is a + * forced subtitle track. Forced subtitle tracks are essential for the + * content and are shown even when the user turns off Captions. They + * are used for example to translate foreign/alien dialogs or signs. + * The associated value is an integer, where non-0 means TRUE. This is an + * optional field; if not specified, FORCED defaults to FALSE. + */ + public static readonly KEY_IS_FORCED_SUBTITLE: string = 'is-forced-subtitle'; + + /** + * A key describing the number of haptic channels in an audio format. + * The associated value is an integer. + */ + public static readonly KEY_HAPTIC_CHANNEL_COUNT: string = 'haptic-channel-count'; + + /** @hide */ + public static readonly KEY_IS_TIMED_TEXT: string = 'is-timed-text'; + + // The following color aspect values must be in sync with the ones in HardwareAPI.h. + /** + * An optional key describing the color primaries, white point and + * luminance factors for video content. + * + * The associated value is an integer: 0 if unspecified, or one of the + * COLOR_STANDARD_ values. + */ + public static readonly KEY_COLOR_STANDARD: string = 'color-standard'; + + /** BT.709 color chromacity coordinates with KR = 0.2126, KB = 0.0722. */ + public static readonly COLOR_STANDARD_BT709: number = 1; + + /** BT.601 625 color chromacity coordinates with KR = 0.299, KB = 0.114. */ + public static readonly COLOR_STANDARD_BT601_PAL: number = 2; + + /** BT.601 525 color chromacity coordinates with KR = 0.299, KB = 0.114. */ + public static readonly COLOR_STANDARD_BT601_NTSC: number = 4; + + /** BT.2020 color chromacity coordinates with KR = 0.2627, KB = 0.0593. */ + public static readonly COLOR_STANDARD_BT2020: number = 6; + + // /** @hide */ + // @IntDef({ + // COLOR_STANDARD_BT709, + // COLOR_STANDARD_BT601_PAL, + // COLOR_STANDARD_BT601_NTSC, + // COLOR_STANDARD_BT2020, + // }) + // @Retention(RetentionPolicy.SOURCE) + // public @interface ColorStandard {} + + /** + * An optional key describing the opto-electronic transfer function used + * for the video content. + * + * The associated value is an integer: 0 if unspecified, or one of the + * COLOR_TRANSFER_ values. + */ + public static readonly KEY_COLOR_TRANSFER: string = 'color-transfer'; + + /** Linear transfer characteristic curve. */ + public static readonly COLOR_TRANSFER_LINEAR: number = 1; + + /** SMPTE 170M transfer characteristic curve used by BT.601/BT.709/BT.2020. This is the curve + * used by most non-HDR video content. */ + public static readonly COLOR_TRANSFER_SDR_VIDEO: number = 3; + + /** SMPTE ST 2084 transfer function. This is used by some HDR video content. */ + public static readonly COLOR_TRANSFER_ST2084: number = 6; + + /** ARIB STD-B67 hybrid-log-gamma transfer function. This is used by some HDR video content. */ + public static readonly COLOR_TRANSFER_HLG: number = 7; + + /** @hide */ + // @IntDef({ + // COLOR_TRANSFER_LINEAR, + // COLOR_TRANSFER_SDR_VIDEO, + // COLOR_TRANSFER_ST2084, + // COLOR_TRANSFER_HLG, + // }) + // @Retention(RetentionPolicy.SOURCE) + // public @interface ColorTransfer {} + + /** + * An optional key describing the range of the component values of the video content. + * + * The associated value is an integer: 0 if unspecified, or one of the + * COLOR_RANGE_ values. + */ + public static readonly KEY_COLOR_RANGE: string = 'color-range'; + + /** Limited range. Y component values range from 16 to 235 for 8-bit content. + * Cr, Cy values range from 16 to 240 for 8-bit content. + * This is the default for video content. */ + public static readonly COLOR_RANGE_LIMITED: number = 2; + + /** Full range. Y, Cr and Cb component values range from 0 to 255 for 8-bit content. */ + public static readonly COLOR_RANGE_FULL: number = 1; + + // /** @hide */ + // @IntDef({ + // COLOR_RANGE_LIMITED, + // COLOR_RANGE_FULL, + // }) + // @Retention(RetentionPolicy.SOURCE) + // public @interface ColorRange {} + + /** + * An optional key describing the static metadata of HDR (high-dynamic-range) video content. + * + * The associated value is a ByteBuffer. This buffer contains the raw contents of the + * Static Metadata Descriptor (including the descriptor ID) of an HDMI Dynamic Range and + * Mastering InfoFrame as defined by CTA-861.3. This key must be provided to video decoders + * for HDR video content unless this information is contained in the bitstream and the video + * decoder supports an HDR-capable profile. This key must be provided to video encoders for + * HDR video content. + */ + public static readonly KEY_HDR_STATIC_INFO: string = 'hdr-static-info'; + + /** + * An optional key describing the HDR10+ metadata of the video content. + * + * The associated value is a ByteBuffer containing HDR10+ metadata conforming to the + * user_data_registered_itu_t_t35() syntax of SEI message for ST 2094-40. This key will + * be present on: + *

+ * - The formats of output buffers of a decoder configured for HDR10+ profiles (such as + * {@link MediaCodecInfo.CodecProfileLevel#VP9Profile2HDR10Plus}, {@link + * MediaCodecInfo.CodecProfileLevel#VP9Profile3HDR10Plus} or {@link + * MediaCodecInfo.CodecProfileLevel#HEVCProfileMain10HDR10Plus}), or + *

+ * - The formats of output buffers of an encoder configured for an HDR10+ profiles that + * uses out-of-band metadata (such as {@link + * MediaCodecInfo.CodecProfileLevel#VP9Profile2HDR10Plus} or {@link + * MediaCodecInfo.CodecProfileLevel#VP9Profile3HDR10Plus}). + * + * @see MediaCodec#PARAMETER_KEY_HDR10_PLUS_INFO + */ + public static readonly KEY_HDR10_PLUS_INFO: string = 'hdr10-plus-info'; + + /** + * A key describing a unique ID for the content of a media track. + * + *

This key is used by {@link MediaExtractor}. Some extractors provide multiple encodings + * of the same track (e.g. float audio tracks for FLAC and WAV may be expressed as two + * tracks via MediaExtractor: a normal PCM track for backward compatibility, and a float PCM + * track for added fidelity. Similarly, Dolby Vision extractor may provide a baseline SDR + * version of a DV track.) This key can be used to identify which MediaExtractor tracks refer + * to the same underlying content. + *

+ * + * The associated value is an integer. + */ + public static readonly KEY_TRACK_ID: string = 'track-id'; + + /** + * A key describing the system id of the conditional access system used to scramble + * a media track. + *

+ * This key is set by {@link MediaExtractor} if the track is scrambled with a conditional + * access system, regardless of the presence of a valid {@link MediaCas} object. + *

+ * The associated value is an integer. + * @hide + */ + public static readonly KEY_CA_SYSTEM_ID: string = 'ca-system-id'; + + /** + * A key describing the {@link MediaCas.Session} object associated with a media track. + *

+ * This key is set by {@link MediaExtractor} if the track is scrambled with a conditional + * access system, after it receives a valid {@link MediaCas} object. + *

+ * The associated value is a ByteBuffer. + * @hide + */ + public static readonly KEY_CA_SESSION_ID: string = 'ca-session-id'; + + /** + * A key describing the private data in the CA_descriptor associated with a media track. + *

+ * This key is set by {@link MediaExtractor} if the track is scrambled with a conditional + * access system, before it receives a valid {@link MediaCas} object. + *

+ * The associated value is a ByteBuffer. + * @hide + */ + public static readonly KEY_CA_PRIVATE_DATA: string = 'ca-private-data'; + + /** + * A key describing the maximum number of B frames between I or P frames, + * to be used by a video encoder. + * The associated value is an integer. The default value is 0, which means + * that no B frames are allowed. Note that non-zero value does not guarantee + * B frames; it's up to the encoder to decide. + */ + public static readonly KEY_MAX_B_FRAMES: string = 'max-bframes'; +} diff --git a/src/app/googDevice/client/ConfigureScrcpy.ts b/src/app/googDevice/client/ConfigureScrcpy.ts new file mode 100644 index 0000000..742ae93 --- /dev/null +++ b/src/app/googDevice/client/ConfigureScrcpy.ts @@ -0,0 +1,697 @@ +import '../../../style/dialog.css'; +import GoogDeviceDescriptor from '../../../types/GoogDeviceDescriptor'; +import { DisplayCombinedInfo } from '../../client/StreamReceiver'; +import VideoSettings from '../../VideoSettings'; +import { StreamClientScrcpy } from './StreamClientScrcpy'; +import Size from '../../Size'; +import Util from '../../Util'; +import { DisplayInfo } from '../../DisplayInfo'; +import { ToolBoxButton } from '../../toolbox/ToolBoxButton'; +import SvgImage from '../../ui/SvgImage'; +import { PlayerClass } from '../../player/BasePlayer'; +import { ToolBoxCheckbox } from '../../toolbox/ToolBoxCheckbox'; +import { DeviceTracker } from './DeviceTracker'; +import { Attribute } from '../../Attribute'; +import { StreamReceiverScrcpy } from './StreamReceiverScrcpy'; +import { ParamsStreamScrcpy } from '../../../types/ParamsStreamScrcpy'; +import { BaseClient } from '../../client/BaseClient'; + +interface ConfigureScrcpyEvents { + closed: { dialog: ConfigureScrcpy; result: boolean }; +} + +type Range = { + max: number; + min: number; + step: number; + formatter?: (value: number) => string; +}; + +export class ConfigureScrcpy extends BaseClient { + private readonly TAG: string; + private readonly udid: string; + private readonly escapedUdid: string; + private readonly playerStorageKey: string; + private deviceName: string; + private streamReceiver?: StreamReceiverScrcpy; + private playerName?: string; + private displayInfo?: DisplayInfo; + private background: HTMLElement; + private dialogBody?: HTMLElement; + private okButton?: HTMLButtonElement; + private fitToScreenCheckbox?: HTMLInputElement; + private resetSettingsButton?: HTMLButtonElement; + private loadSettingsButton?: HTMLButtonElement; + private saveSettingsButton?: HTMLButtonElement; + private playerSelectElement?: HTMLSelectElement; + private displayIdSelectElement?: HTMLSelectElement; + private encoderSelectElement?: HTMLSelectElement; + private connectionStatusElement?: HTMLElement; + private dialogContainer?: HTMLElement; + private statusText = ''; + private connectionCount = 0; + + constructor(private readonly tracker: DeviceTracker, descriptor: GoogDeviceDescriptor, params: ParamsStreamScrcpy) { + super(params); + this.udid = descriptor.udid; + this.escapedUdid = Util.escapeUdid(this.udid); + this.playerStorageKey = `configure_stream::${this.escapedUdid}::player`; + this.deviceName = descriptor['ro.product.model']; + this.TAG = `ConfigureScrcpy[${this.udid}]`; + this.createStreamReceiver(params); + this.setTitle(`${this.deviceName}. Configure stream`); + this.background = this.createUI(); + } + + public getTracker(): DeviceTracker { + return this.tracker; + } + + // 创建流接收器 + private createStreamReceiver(params: ParamsStreamScrcpy): void { + // 如果已经存在流接收器,则先解绑事件监听器,并停止流接收器 + if (this.streamReceiver) { + this.detachEventsListeners(this.streamReceiver); + this.streamReceiver.stop(); + } + // 创建新的流接收器 + this.streamReceiver = new StreamReceiverScrcpy(params); + // 绑定事件监听器 + this.attachEventsListeners(this.streamReceiver); + } + + // 事件监听器,用于监听StreamReceiverScrcpy对象的事件 + private attachEventsListeners(streamReceiver: StreamReceiverScrcpy): void { + // 监听encoders事件,当有新的编码器连接时触发 + streamReceiver.on('encoders', this.onEncoders); + // 监听displayInfo事件,当显示信息发生变化时触发 + streamReceiver.on('displayInfo', this.onDisplayInfo); + // 监听connected事件,当连接成功时触发 + streamReceiver.on('connected', this.onConnected); + // 监听disconnected事件,当连接断开时触发 + streamReceiver.on('disconnected', this.onDisconnected); + } + + // 移除事件监听器 + private detachEventsListeners(streamReceiver: StreamReceiverScrcpy): void { + // 移除encoders事件监听器 + streamReceiver.off('encoders', this.onEncoders); + // 移除displayInfo事件监听器 + streamReceiver.off('displayInfo', this.onDisplayInfo); + // 移除connected事件监听器 + streamReceiver.off('connected', this.onConnected); + // 移除disconnected事件监听器 + streamReceiver.off('disconnected', this.onDisconnected); + } + + // 更新连接状态 + private updateStatus(): void { + // 如果没有连接状态元素,则返回 + if (!this.connectionStatusElement) { + return; + } + // 获取连接状态文本 + let text = this.statusText; + // 如果有其他客户端连接,则添加其他客户端连接数 + if (this.connectionCount) { + text = `${text}. Other clients: ${this.connectionCount}.`; + } + // 将连接状态文本设置到连接状态元素中 + this.connectionStatusElement.innerText = text; + } + + private onEncoders = (encoders: string[]): void => { + // console.log(this.TAG, 'Encoders', encoders); + const select = this.encoderSelectElement || document.createElement('select'); + let child; + while ((child = select.firstChild)) { + select.removeChild(child); + } + encoders.unshift(''); + encoders.forEach((value) => { + const optionElement = document.createElement('option'); + optionElement.setAttribute('value', value); + optionElement.innerText = value; + select.appendChild(optionElement); + }); + this.encoderSelectElement = select; + }; + + private onDisplayInfo = (infoArray: DisplayCombinedInfo[]): void => { + // console.log(this.TAG, 'Received info'); + this.statusText = 'Ready'; + this.updateStatus(); + this.dialogContainer?.classList.add('ready'); + const select = this.displayIdSelectElement || document.createElement('select'); + let child; + while ((child = select.firstChild)) { + select.removeChild(child); + } + let selectedOptionIdx = -1; + infoArray.forEach((value: DisplayCombinedInfo, idx: number) => { + const { displayInfo } = value; + const { displayId, size } = displayInfo; + const optionElement = document.createElement('option'); + optionElement.setAttribute('value', displayId.toString()); + optionElement.innerText = `ID: ${displayId}; ${size.width}x${size.height}`; + select.appendChild(optionElement); + if ( + (this.displayInfo && this.displayInfo.displayId === displayId) || + (!this.displayInfo && displayId === DisplayInfo.DEFAULT_DISPLAY) + ) { + selectedOptionIdx = idx; + } + }); + if (selectedOptionIdx > -1) { + select.selectedIndex = selectedOptionIdx; + const { videoSettings, connectionCount, displayInfo } = infoArray[selectedOptionIdx]; + this.displayInfo = displayInfo; + if (connectionCount > 0 && videoSettings) { + // console.log(this.TAG, 'Apply other clients settings'); + this.fillInputsFromVideoSettings(videoSettings, false); + } else { + // console.log(this.TAG, 'Apply settings for current player'); + this.updateVideoSettingsForPlayer(); + } + this.connectionCount = connectionCount; + this.updateStatus(); + } + this.displayIdSelectElement = select; + if (this.dialogBody) { + this.dialogBody.classList.remove('hidden'); + this.dialogBody.classList.add('visible'); + } + }; + + private onConnected = (): void => { + // console.log(this.TAG, 'Connected'); + this.statusText = 'Waiting for info...'; + this.updateStatus(); + if (this.okButton) { + this.okButton.disabled = false; + } + }; + + private onDisconnected = (): void => { + // console.log(this.TAG, 'Disconnected'); + this.statusText = 'Disconnected'; + this.updateStatus(); + if (this.okButton) { + this.okButton.disabled = true; + } + if (this.dialogBody) { + this.dialogBody.classList.remove('visible'); + this.dialogBody.classList.add('hidden'); + } + }; + + private onPlayerChange = (): void => { + this.updateVideoSettingsForPlayer(); + }; + + private onDisplayIdChange = (): void => { + const select = this.displayIdSelectElement; + if (!select || !this.streamReceiver) { + return; + } + const value = select.options[select.selectedIndex].value; + const displayId = parseInt(value, 10); + if (!isNaN(displayId)) { + this.displayInfo = this.streamReceiver.getDisplayInfo(displayId); + } + this.updateVideoSettingsForPlayer(); + }; + + // 获取玩家 + private getPlayer(): PlayerClass | undefined { + // 如果没有选择玩家,则返回undefined + if (!this.playerSelectElement) { + return; + } + // 获取选择的玩家名称 + const playerName = this.playerSelectElement.options[this.playerSelectElement.selectedIndex].value; + // 在StreamClientScrcpy中查找玩家 + return StreamClientScrcpy.getPlayers().find((playerClass) => { + // 如果玩家名称匹配,则返回该玩家 + return playerClass.playerFullName === playerName; + }); + } + + // 更新播放器的视频设置 + private updateVideoSettingsForPlayer(): void { + // 获取播放器 + const player = this.getPlayer(); + // 如果播放器存在 + if (player) { + // 设置播放器名称 + this.playerName = player.playerFullName; + // 获取存储或首选的视频设置 + const storedOrPreferred = player.loadVideoSettings(this.udid, this.displayInfo); + // 获取屏幕适应状态 + const fitToScreen = player.getFitToScreenStatus(this.udid, this.displayInfo); + // 从视频设置中填充输入 + this.fillInputsFromVideoSettings(storedOrPreferred, fitToScreen); + } + } + + // 根据id获取基本的输入元素 + private getBasicInput(id: string): HTMLInputElement | null { + // 根据id和udid获取元素 + const element = document.getElementById(`${id}_${this.escapedUdid}`); + // 如果元素不存在,返回null + if (!element) { + return null; + } + // 返回元素 + return element as HTMLInputElement; + } + + // 根据视频设置填充输入框 + private fillInputsFromVideoSettings(videoSettings: VideoSettings, fitToScreen: boolean): void { + // 如果显示信息和视频设置中的显示id不匹配,则输出错误信息 + if (this.displayInfo && this.displayInfo.displayId !== videoSettings.displayId) { + console.error(this.TAG, `Display id from VideoSettings and DisplayInfo don't match`); + } + // 根据视频设置填充基本输入框 + this.fillBasicInput({ id: 'bitrate' }, videoSettings); + this.fillBasicInput({ id: 'maxFps' }, videoSettings); + this.fillBasicInput({ id: 'iFrameInterval' }, videoSettings); + // this.fillBasicInput({ id: 'displayId' }, videoSettings); + this.fillBasicInput({ id: 'codecOptions' }, videoSettings); + // 如果视频设置中有边界,则填充最大宽度和最大高度输入框 + if (videoSettings.bounds) { + const { width, height } = videoSettings.bounds; + const widthInput = this.getBasicInput('maxWidth'); + if (widthInput) { + widthInput.value = width.toString(10); + } + const heightInput = this.getBasicInput('maxHeight'); + if (heightInput) { + heightInput.value = height.toString(10); + } + } + // 如果有编码器选择框,则根据视频设置填充编码器选择框 + if (this.encoderSelectElement) { + const encoderName = videoSettings.encoderName || ''; + const option = Array.from(this.encoderSelectElement.options).find((element) => { + return element.value === encoderName; + }); + if (option) { + this.encoderSelectElement.selectedIndex = option.index; + } + } + // 如果有适应屏幕复选框,则根据fitToScreen参数填充复选框,并调用onFitToScreenChanged方法 + if (this.fitToScreenCheckbox) { + this.fitToScreenCheckbox.checked = fitToScreen; + this.onFitToScreenChanged(fitToScreen); + } + } + + // 当fitToScreenCheckbox的值改变时调用 + private onFitToScreenChanged(checked: boolean) { + // 获取maxHeight和maxWidth的输入框 + const heightInput = this.getBasicInput('maxHeight'); + const widthInput = this.getBasicInput('maxWidth'); + // 如果fitToScreenCheckbox、heightInput或widthInput不存在,则返回 + if (!this.fitToScreenCheckbox || !heightInput || !widthInput) { + return; + } + // 如果checked为true,则禁用heightInput和widthInput + heightInput.disabled = widthInput.disabled = checked; + // 如果checked为true,则将heightInput和widthInput的值清空,并保存原来的值 + if (checked) { + heightInput.setAttribute(Attribute.VALUE, heightInput.value); + heightInput.value = ''; + widthInput.setAttribute(Attribute.VALUE, widthInput.value); + widthInput.value = ''; + } else { + // 如果checked为false,则将heightInput和widthInput的值恢复为原来的值 + const storedHeight = heightInput.getAttribute(Attribute.VALUE); + if (typeof storedHeight === 'string') { + heightInput.value = storedHeight; + heightInput.removeAttribute(Attribute.VALUE); + } + const storedWidth = widthInput.getAttribute(Attribute.VALUE); + if (typeof storedWidth === 'string') { + widthInput.value = storedWidth; + widthInput.removeAttribute(Attribute.VALUE); + } + } + } + + // 根据传入的参数,填充视频设置的基本输入框 + private fillBasicInput(opts: { id: keyof VideoSettings }, videoSettings: VideoSettings): void { + // 获取基本输入框 + const input = this.getBasicInput(opts.id); + // 获取视频设置中对应id的值 + const value = videoSettings[opts.id]; + // 如果输入框存在 + if (input) { + // 如果值存在且不为'-'、0、null + if (typeof value !== 'undefined' && value !== '-' && value !== 0 && value !== null) { + // 将值转换为字符串并赋值给输入框 + input.value = value.toString(10); + // 如果输入框的类型为range,触发input事件 + if (input.getAttribute('type') === 'range') { + input.dispatchEvent(new Event('input')); + } + } else { + // 否则将输入框的值置为空 + input.value = ''; + } + } + } + + private appendBasicInput( + parent: HTMLElement, + opts: { label: string; id: string; range?: Range }, + ): HTMLInputElement { + const label = document.createElement('label'); + label.classList.add('label'); + label.innerText = `${opts.label}:`; + label.id = `label_${opts.id}_${this.escapedUdid}`; + parent.appendChild(label); + const input = document.createElement('input'); + input.classList.add('input'); + input.id = label.htmlFor = `${opts.id}_${this.escapedUdid}`; + const { range } = opts; + if (range) { + label.setAttribute('title', opts.label); + input.oninput = () => { + const value = range.formatter ? range.formatter(parseInt(input.value, 10)) : input.value; + label.innerText = `${opts.label} (${value}):`; + }; + input.setAttribute('type', 'range'); + input.setAttribute('max', range.max.toString()); + input.setAttribute('min', range.min.toString()); + input.setAttribute('step', range.step.toString()); + } + parent.appendChild(input); + return input; + } + + private getNumberValueFromInput(name: string): number { + const value = (document.getElementById(`${name}_${this.escapedUdid}`) as HTMLInputElement).value; + return parseInt(value, 10); + } + + private getStringValueFromInput(name: string): string { + return (document.getElementById(`${name}_${this.escapedUdid}`) as HTMLInputElement).value; + } + + private getValueFromSelect(name: string): string { + const select = document.getElementById(`${name}_${this.escapedUdid}`) as HTMLSelectElement; + return select.options[select.selectedIndex].value; + } + + private buildVideoSettings(): VideoSettings | null { + try { + const bitrate = this.getNumberValueFromInput('bitrate'); + const maxFps = this.getNumberValueFromInput('maxFps'); + const iFrameInterval = this.getNumberValueFromInput('iFrameInterval'); + const maxWidth = this.getNumberValueFromInput('maxWidth'); + const maxHeight = this.getNumberValueFromInput('maxHeight'); + const displayId = this.getNumberValueFromInput('displayId'); + const codecOptions = this.getStringValueFromInput('codecOptions') || undefined; + let bounds: Size | undefined; + if (!isNaN(maxWidth) && !isNaN(maxHeight) && maxWidth && maxHeight) { + bounds = new Size(maxWidth, maxHeight); + } + const encoderName = this.getValueFromSelect('encoderName') || undefined; + return new VideoSettings({ + bitrate, + bounds, + maxFps, + iFrameInterval, + displayId, + codecOptions, + encoderName, + }); + } catch (error: any) { + console.error(this.TAG, error.message); + return null; + } + } + + private getFitToScreenValue(): boolean { + if (!this.fitToScreenCheckbox) { + return false; + } + return this.fitToScreenCheckbox.checked; + } + + private getPreviouslyUsedPlayer(): string { + if (!window.localStorage) { + return ''; + } + const result = window.localStorage.getItem(this.playerStorageKey); + if (result) { + return result; + } else { + return ''; + } + } + + private setPreviouslyUsedPlayer(playerName: string): void { + if (!window.localStorage) { + return; + } + window.localStorage.setItem(this.playerStorageKey, playerName); + } + + private createUI(): HTMLElement { + const dialogName = 'configureDialog'; + const blockClass = 'dialog-block'; + const background = document.createElement('div'); + background.classList.add('dialog-background', dialogName); + const dialogContainer = (this.dialogContainer = document.createElement('div')); + dialogContainer.classList.add('dialog-container', dialogName); + const dialogHeader = document.createElement('div'); + dialogHeader.classList.add('dialog-header', dialogName, 'control-wrapper'); + const backButton = new ToolBoxButton('Back', SvgImage.Icon.ARROW_BACK); + + backButton.addEventListener('click', () => { + this.cancel(); + }); + backButton.getAllElements().forEach((el) => { + dialogHeader.appendChild(el); + }); + + const deviceName = document.createElement('span'); + deviceName.classList.add('dialog-title', 'main-title'); + deviceName.innerText = this.deviceName; + dialogHeader.appendChild(deviceName); + const dialogBody = (this.dialogBody = document.createElement('div')); + dialogBody.classList.add('dialog-body', blockClass, dialogName, 'hidden'); + const playerWrapper = document.createElement('div'); + playerWrapper.classList.add('controls'); + const playerLabel = document.createElement('label'); + playerLabel.classList.add('label'); + playerLabel.innerText = 'Player:'; + playerWrapper.appendChild(playerLabel); + const playerSelect = (this.playerSelectElement = document.createElement('select')); + playerSelect.classList.add('input'); + playerSelect.id = playerLabel.htmlFor = `player_${this.escapedUdid}`; + playerWrapper.appendChild(playerSelect); + dialogBody.appendChild(playerWrapper); + const previouslyUsedPlayer = this.getPreviouslyUsedPlayer(); + StreamClientScrcpy.getPlayers().forEach((playerClass, index) => { + const { playerFullName } = playerClass; + const optionElement = document.createElement('option'); + optionElement.setAttribute('value', playerFullName); + optionElement.innerText = playerFullName; + playerSelect.appendChild(optionElement); + if (playerFullName === previouslyUsedPlayer) { + playerSelect.selectedIndex = index; + } + }); + playerSelect.onchange = this.onPlayerChange; + this.updateVideoSettingsForPlayer(); + + const controls = document.createElement('div'); + controls.classList.add('controls', 'control-wrapper'); + const displayIdLabel = document.createElement('label'); + displayIdLabel.classList.add('label'); + displayIdLabel.innerText = 'Display:'; + controls.appendChild(displayIdLabel); + if (!this.displayIdSelectElement) { + this.displayIdSelectElement = document.createElement('select'); + } + controls.appendChild(this.displayIdSelectElement); + this.displayIdSelectElement.classList.add('input'); + this.displayIdSelectElement.id = displayIdLabel.htmlFor = `displayId_${this.escapedUdid}`; + this.displayIdSelectElement.onchange = this.onDisplayIdChange; + + this.appendBasicInput(controls, { + label: 'Bitrate', + id: 'bitrate', + range: { min: 524288, max: 8388608, step: 524288, formatter: Util.prettyBytes }, + }); + this.appendBasicInput(controls, { + label: 'Max FPS', + id: 'maxFps', + range: { min: 1, max: 60, step: 1 }, + }); + this.appendBasicInput(controls, { label: 'I-Frame interval', id: 'iFrameInterval' }); + const fitLabel = document.createElement('label'); + fitLabel.innerText = 'Fit to screen'; + fitLabel.classList.add('label'); + controls.appendChild(fitLabel); + const fitToggle = new ToolBoxCheckbox( + 'Fit to screen', + { off: SvgImage.Icon.TOGGLE_OFF, on: SvgImage.Icon.TOGGLE_ON }, + 'fit_to_screen', + ); + fitToggle.getAllElements().forEach((el) => { + controls.appendChild(el); + if (el instanceof HTMLLabelElement) { + fitLabel.htmlFor = el.htmlFor; + el.classList.add('input'); + } + if (el instanceof HTMLInputElement) { + this.fitToScreenCheckbox = el; + } + }); + fitToggle.addEventListener('click', (_, el) => { + const element = el.getElement(); + this.onFitToScreenChanged(element.checked); + }); + this.appendBasicInput(controls, { label: 'Max width', id: 'maxWidth' }); + this.appendBasicInput(controls, { label: 'Max height', id: 'maxHeight' }); + this.appendBasicInput(controls, { label: 'Codec options', id: 'codecOptions' }); + + const encoderLabel = document.createElement('label'); + encoderLabel.classList.add('label'); + encoderLabel.innerText = 'Encoder:'; + controls.appendChild(encoderLabel); + if (!this.encoderSelectElement) { + this.encoderSelectElement = document.createElement('select'); + } + controls.appendChild(this.encoderSelectElement); + this.encoderSelectElement.classList.add('input'); + this.encoderSelectElement.id = encoderLabel.htmlFor = `encoderName_${this.escapedUdid}`; + + dialogBody.appendChild(controls); + + const buttonsWrapper = document.createElement('div'); + buttonsWrapper.classList.add('controls'); + + const resetSettingsButton = (this.resetSettingsButton = document.createElement('button')); + resetSettingsButton.classList.add('button'); + resetSettingsButton.innerText = 'Reset settings'; + resetSettingsButton.addEventListener('click', this.resetSettings); + buttonsWrapper.appendChild(resetSettingsButton); + + const loadSettingsButton = (this.loadSettingsButton = document.createElement('button')); + loadSettingsButton.classList.add('button'); + loadSettingsButton.innerText = 'Load settings'; + loadSettingsButton.addEventListener('click', this.loadSettings); + buttonsWrapper.appendChild(loadSettingsButton); + + const saveSettingsButton = (this.saveSettingsButton = document.createElement('button')); + saveSettingsButton.classList.add('button'); + saveSettingsButton.innerText = 'Save settings'; + saveSettingsButton.addEventListener('click', this.saveSettings); + buttonsWrapper.appendChild(saveSettingsButton); + + dialogBody.appendChild(buttonsWrapper); + + const dialogFooter = document.createElement('div'); + dialogFooter.classList.add('dialog-footer', blockClass, dialogName); + const statusElement = document.createElement('span'); + statusElement.classList.add('subtitle'); + this.connectionStatusElement = statusElement; + dialogFooter.appendChild(statusElement); + this.statusText = `Connecting...`; + this.updateStatus(); + + // const cancelButton = (this.cancelButton = document.createElement('button')); + // cancelButton.innerText = 'Cancel'; + // cancelButton.addEventListener('click', this.cancel); + const okButton = (this.okButton = document.createElement('button')); + okButton.innerText = 'Open'; + okButton.disabled = true; + okButton.addEventListener('click', this.openStream); + dialogFooter.appendChild(okButton); + // dialogFooter.appendChild(cancelButton); + dialogBody.appendChild(dialogFooter); + dialogContainer.appendChild(dialogHeader); + dialogContainer.appendChild(dialogBody); + dialogContainer.appendChild(dialogFooter); + background.appendChild(dialogContainer); + background.addEventListener('click', this.onBackgroundClick); + document.body.appendChild(background); + return background; + } + + private removeUI(): void { + document.body.removeChild(this.background); + this.okButton?.removeEventListener('click', this.openStream); + // this.cancelButton?.removeEventListener('click', this.cancel); + this.resetSettingsButton?.removeEventListener('click', this.resetSettings); + this.loadSettingsButton?.removeEventListener('click', this.loadSettings); + this.saveSettingsButton?.removeEventListener('click', this.saveSettings); + this.background.removeEventListener('click', this.onBackgroundClick); + } + + private onBackgroundClick = (event: MouseEvent): void => { + if (event.target !== event.currentTarget) { + return; + } + this.cancel(); + }; + + private cancel = (): void => { + if (this.streamReceiver) { + this.detachEventsListeners(this.streamReceiver); + this.streamReceiver.stop(); + } + this.emit('closed', { dialog: this, result: false }); + this.removeUI(); + }; + + private resetSettings = (): void => { + const player = this.getPlayer(); + if (player) { + this.fillInputsFromVideoSettings(player.getPreferredVideoSetting(), false); + } + }; + + private loadSettings = (): void => { + this.updateVideoSettingsForPlayer(); + }; + + private saveSettings = (): void => { + const videoSettings = this.buildVideoSettings(); + const player = this.getPlayer(); + if (videoSettings && player) { + const fitToScreen = this.getFitToScreenValue(); + player.saveVideoSettings(this.udid, videoSettings, fitToScreen, this.displayInfo); + } + }; + + private openStream = (): void => { + const videoSettings = this.buildVideoSettings(); + if (!videoSettings || !this.streamReceiver || !this.playerName) { + return; + } + const fitToScreen = this.getFitToScreenValue(); + this.detachEventsListeners(this.streamReceiver); + this.emit('closed', { dialog: this, result: true }); + this.removeUI(); + const player = StreamClientScrcpy.createPlayer(this.playerName, this.udid, this.displayInfo); + if (!player) { + return; + } + this.setPreviouslyUsedPlayer(this.playerName); + // return; + player.setVideoSettings(videoSettings, fitToScreen, false); + const params: ParamsStreamScrcpy = { + ...this.params, + udid: this.udid, + fitToScreen, + }; + StreamClientScrcpy.start(params, this.streamReceiver, player, fitToScreen, videoSettings); + this.streamReceiver.triggerInitialInfoEvents(); + }; +} diff --git a/src/app/googDevice/client/DeviceTracker.ts b/src/app/googDevice/client/DeviceTracker.ts new file mode 100644 index 0000000..1b30e72 --- /dev/null +++ b/src/app/googDevice/client/DeviceTracker.ts @@ -0,0 +1,364 @@ +import '../../../style/devicelist.css'; +import { BaseDeviceTracker } from '../../client/BaseDeviceTracker'; +import { SERVER_PORT } from '../../../common/Constants'; +import { ACTION } from '../../../common/Action'; +import GoogDeviceDescriptor from '../../../types/GoogDeviceDescriptor'; +import { ControlCenterCommand } from '../../../common/ControlCenterCommand'; +import { StreamClientScrcpy } from './StreamClientScrcpy'; +import SvgImage from '../../ui/SvgImage'; +import { html } from '../../ui/HtmlTag'; +import Util from '../../Util'; +import { Attribute } from '../../Attribute'; +import { DeviceState } from '../../../common/DeviceState'; +import { Message } from '../../../types/Message'; +import { ParamsDeviceTracker } from '../../../types/ParamsDeviceTracker'; +import { HostItem } from '../../../types/Configuration'; +import { ChannelCode } from '../../../common/ChannelCode'; +import { Tool } from '../../client/Tool'; + +type Field = keyof GoogDeviceDescriptor | ((descriptor: GoogDeviceDescriptor) => string); +type DescriptionColumn = { title: string; field: Field }; + +const DESC_COLUMNS: DescriptionColumn[] = [ + { + title: 'Net Interface', + field: 'interfaces', + }, + { + title: 'Server PID', + field: 'pid', + }, +]; + +export class DeviceTracker extends BaseDeviceTracker { + public static readonly ACTION = ACTION.GOOG_DEVICE_LIST; + public static readonly CREATE_DIRECT_LINKS = true; + private static instancesByUrl: Map = new Map(); + protected static tools: Set = new Set(); + protected tableId = 'goog_device_list'; + + public static start(hostItem: HostItem): DeviceTracker { + const url = this.buildUrlForTracker(hostItem).toString(); + let instance = this.instancesByUrl.get(url); + if (!instance) { + instance = new DeviceTracker(hostItem, url); + } + return instance; + } + + public static getInstance(hostItem: HostItem): DeviceTracker { + return this.start(hostItem); + } + + protected constructor(params: HostItem, directUrl: string) { + super({ ...params, action: DeviceTracker.ACTION }, directUrl); + DeviceTracker.instancesByUrl.set(directUrl, this); + this.buildDeviceTable(); + this.openNewConnection(); + } + + protected onSocketOpen(): void { + // nothing here; + } + + protected setIdAndHostName(id: string, hostName: string): void { + super.setIdAndHostName(id, hostName); + for (const value of DeviceTracker.instancesByUrl.values()) { + if (value.id === id && value !== this) { + console.warn( + `Tracker with url: "${this.url}" has the same id(${this.id}) as tracker with url "${value.url}"`, + ); + console.warn(`This tracker will shut down`); + this.destroy(); + } + } + } + + onInterfaceSelected = (event: Event): void => { + const selectElement = event.currentTarget as HTMLSelectElement; + const option = selectElement.selectedOptions[0]; + const url = decodeURI(option.getAttribute(Attribute.URL) || ''); + const name = option.getAttribute(Attribute.NAME) || ''; + const fullName = decodeURIComponent(selectElement.getAttribute(Attribute.FULL_NAME) || ''); + const udid = selectElement.getAttribute(Attribute.UDID) || ''; + this.updateLink({ url, name, fullName, udid, store: true }); + }; + + private updateLink(params: { url: string; name: string; fullName: string; udid: string; store: boolean }): void { + const { url, name, fullName, udid, store } = params; + const playerTds = document.getElementsByName( + encodeURIComponent(`${DeviceTracker.AttributePrefixPlayerFor}${fullName}`), + ); + if (typeof udid !== 'string') { + return; + } + if (store) { + const localStorageKey = DeviceTracker.getLocalStorageKey(fullName || ''); + if (localStorage && name) { + localStorage.setItem(localStorageKey, name); + } + } + const action = ACTION.STREAM_SCRCPY; + playerTds.forEach((item) => { + item.innerHTML = ''; + const playerFullName = item.getAttribute(DeviceTracker.AttributePlayerFullName); + const playerCodeName = item.getAttribute(DeviceTracker.AttributePlayerCodeName); + if (!playerFullName || !playerCodeName) { + return; + } + const link = DeviceTracker.buildLink( + { + action, + udid, + player: decodeURIComponent(playerCodeName), + ws: url, + }, + decodeURIComponent(playerFullName), + this.params, + ); + item.appendChild(link); + }); + } + + onActionButtonClick = (event: MouseEvent): void => { + const button = event.currentTarget as HTMLButtonElement; + const udid = button.getAttribute(Attribute.UDID); + const pidString = button.getAttribute(Attribute.PID) || ''; + const command = button.getAttribute(Attribute.COMMAND) as string; + const pid = parseInt(pidString, 10); + const data: Message = { + id: this.getNextId(), + type: command, + data: { + udid: typeof udid === 'string' ? udid : undefined, + pid: isNaN(pid) ? undefined : pid, + }, + }; + + if (this.ws && this.ws.readyState === this.ws.OPEN) { + console.log('发送的参数3'); + + this.ws.send(JSON.stringify(data)); + } + }; + + private static getLocalStorageKey(udid: string): string { + return `device_list::${udid}::interface`; + } + + protected static createUrl(params: ParamsDeviceTracker, udid = ''): URL { + const secure = !!params.secure; + const hostname = params.hostname || location.hostname; + const port = typeof params.port === 'number' ? params.port : secure ? 443 : 80; + const pathname = params.pathname || location.pathname; + const urlObject = this.buildUrl({ ...params, secure, hostname, port, pathname }); + if (udid) { + urlObject.searchParams.set('action', ACTION.PROXY_ADB); + urlObject.searchParams.set('remote', `tcp:${SERVER_PORT.toString(10)}`); + urlObject.searchParams.set('udid', udid); + } + return urlObject; + } + + protected static createInterfaceOption(name: string, url: string): HTMLOptionElement { + const optionElement = document.createElement('option'); + optionElement.setAttribute(Attribute.URL, url); + optionElement.setAttribute(Attribute.NAME, name); + optionElement.innerText = `proxy over adb`; + return optionElement; + } + + private static titleToClassName(title: string): string { + return title.toLowerCase().replace(/\s/g, '_'); + } + + protected buildDeviceRow(tbody: Element, device: GoogDeviceDescriptor): void { + let selectedInterfaceUrl = ''; + let selectedInterfaceName = ''; + const blockClass = 'desc-block'; + const fullName = `${this.id}_${Util.escapeUdid(device.udid)}`; + const isActive = device.state === DeviceState.DEVICE; + let hasPid = false; + const servicesId = `device_services_${fullName}`; + const row = html`

+
+
${device['ro.product.manufacturer']} ${device['ro.product.model']}
+
${device.udid}
+
+
${device['ro.build.version.release']}
+
${device['ro.build.version.sdk']}
+
+
+
+
+
`.content; + const services = row.getElementById(servicesId); + if (!services) { + return; + } + + DeviceTracker.tools.forEach((tool) => { + const entry = tool.createEntryForDeviceList(device, blockClass, this.params); + if (entry) { + if (Array.isArray(entry)) { + entry.forEach((item) => { + item && services.appendChild(item); + }); + } else { + services.appendChild(entry); + } + } + }); + + const streamEntry = StreamClientScrcpy.createEntryForDeviceList(device, blockClass, fullName, this.params); + streamEntry && services.appendChild(streamEntry); + + DESC_COLUMNS.forEach((item) => { + const { title } = item; + const fieldName = item.field; + let value: string; + if (typeof item.field === 'string') { + value = '' + device[item.field]; + } else { + value = item.field(device); + } + const td = document.createElement('div'); + td.classList.add(DeviceTracker.titleToClassName(title), blockClass); + services.appendChild(td); + if (fieldName === 'pid') { + hasPid = value !== '-1'; + const actionButton = document.createElement('button'); + actionButton.className = 'action-button kill-server-button'; + actionButton.setAttribute(Attribute.UDID, device.udid); + actionButton.setAttribute(Attribute.PID, value); + let command: string; + if (isActive) { + actionButton.classList.add('active'); + actionButton.onclick = this.onActionButtonClick; + if (hasPid) { + command = ControlCenterCommand.KILL_SERVER; + actionButton.title = 'Kill server'; + actionButton.appendChild(SvgImage.create(SvgImage.Icon.CANCEL)); + } else { + command = ControlCenterCommand.START_SERVER; + actionButton.title = 'Start server'; + actionButton.appendChild(SvgImage.create(SvgImage.Icon.REFRESH)); + } + actionButton.setAttribute(Attribute.COMMAND, command); + } else { + const timestamp = device['last.update.timestamp']; + if (timestamp) { + const date = new Date(timestamp); + actionButton.title = `Last update on ${date.toLocaleDateString()} at ${date.toLocaleTimeString()}`; + } else { + actionButton.title = `Not active`; + } + actionButton.appendChild(SvgImage.create(SvgImage.Icon.OFFLINE)); + } + const span = document.createElement('span'); + span.innerText = value; + actionButton.appendChild(span); + td.appendChild(actionButton); + } else if (fieldName === 'interfaces') { + const proxyInterfaceUrl = DeviceTracker.createUrl(this.params, device.udid).toString(); + const proxyInterfaceName = 'proxy'; + const localStorageKey = DeviceTracker.getLocalStorageKey(fullName); + const lastSelected = localStorage && localStorage.getItem(localStorageKey); + const selectElement = document.createElement('select'); + selectElement.setAttribute(Attribute.UDID, device.udid); + selectElement.setAttribute(Attribute.FULL_NAME, fullName); + selectElement.setAttribute( + 'name', + encodeURIComponent(`${DeviceTracker.AttributePrefixInterfaceSelectFor}${fullName}`), + ); + /// #if SCRCPY_LISTENS_ON_ALL_INTERFACES + device.interfaces.forEach((value) => { + const params = { + ...this.params, + secure: false, + hostname: value.ipv4, + port: SERVER_PORT, + }; + const url = DeviceTracker.createUrl(params).toString(); + const optionElement = DeviceTracker.createInterfaceOption(value.name, url); + optionElement.innerText = `${value.name}: ${value.ipv4}`; + selectElement.appendChild(optionElement); + if (lastSelected) { + if (lastSelected === value.name || !selectedInterfaceName) { + optionElement.selected = true; + selectedInterfaceUrl = url; + selectedInterfaceName = value.name; + } + } else if (device['wifi.interface'] === value.name) { + optionElement.selected = true; + } + }); + /// #else + selectedInterfaceUrl = proxyInterfaceUrl; + selectedInterfaceName = proxyInterfaceName; + td.classList.add('hidden'); + /// #endif + if (isActive) { + const adbProxyOption = DeviceTracker.createInterfaceOption(proxyInterfaceName, proxyInterfaceUrl); + if (lastSelected === proxyInterfaceName || !selectedInterfaceName) { + adbProxyOption.selected = true; + selectedInterfaceUrl = proxyInterfaceUrl; + selectedInterfaceName = proxyInterfaceName; + } + selectElement.appendChild(adbProxyOption); + const actionButton = document.createElement('button'); + actionButton.className = 'action-button update-interfaces-button active'; + actionButton.title = `Update information`; + actionButton.appendChild(SvgImage.create(SvgImage.Icon.REFRESH)); + actionButton.setAttribute(Attribute.UDID, device.udid); + actionButton.setAttribute(Attribute.COMMAND, ControlCenterCommand.UPDATE_INTERFACES); + actionButton.onclick = this.onActionButtonClick; + td.appendChild(actionButton); + } + selectElement.onchange = this.onInterfaceSelected; + td.appendChild(selectElement); + } else { + td.innerText = value; + } + }); + + if (DeviceTracker.CREATE_DIRECT_LINKS) { + const name = `${DeviceTracker.AttributePrefixPlayerFor}${fullName}`; + StreamClientScrcpy.getPlayers().forEach((playerClass) => { + const { playerCodeName, playerFullName } = playerClass; + const playerTd = document.createElement('div'); + playerTd.classList.add(blockClass); + playerTd.setAttribute('name', encodeURIComponent(name)); + playerTd.setAttribute(DeviceTracker.AttributePlayerFullName, encodeURIComponent(playerFullName)); + playerTd.setAttribute(DeviceTracker.AttributePlayerCodeName, encodeURIComponent(playerCodeName)); + services.appendChild(playerTd); + }); + } + + tbody.appendChild(row); + if (DeviceTracker.CREATE_DIRECT_LINKS && hasPid && selectedInterfaceUrl) { + this.updateLink({ + url: selectedInterfaceUrl, + name: selectedInterfaceName, + fullName, + udid: device.udid, + store: false, + }); + } + } + + protected getChannelCode(): string { + return ChannelCode.GTRC; + } + + public destroy(): void { + super.destroy(); + DeviceTracker.instancesByUrl.delete(this.url.toString()); + if (!DeviceTracker.instancesByUrl.size) { + const holder = document.getElementById(BaseDeviceTracker.HOLDER_ELEMENT_ID); + if (holder && holder.parentElement) { + holder.parentElement.removeChild(holder); + } + } + } +} diff --git a/src/app/googDevice/client/DevtoolsClient.ts b/src/app/googDevice/client/DevtoolsClient.ts new file mode 100644 index 0000000..d886da0 --- /dev/null +++ b/src/app/googDevice/client/DevtoolsClient.ts @@ -0,0 +1,381 @@ +import '../../../style/devtools.css'; +import { ManagerClient } from '../../client/ManagerClient'; +import { ACTION } from '../../../common/Action'; +import { ParamsDevtools } from '../../../types/ParamsDevtools'; +import { RemoteDevtoolsCommand } from '../../../types/RemoteDevtoolsCommand'; +import { Message } from '../../../types/Message'; +import { DevtoolsInfo, RemoteBrowserInfo, RemoteTarget, TargetDescription } from '../../../types/RemoteDevtools'; +import GoogDeviceDescriptor from '../../../types/GoogDeviceDescriptor'; +import { BaseDeviceTracker } from '../../client/BaseDeviceTracker'; +import { ParamsDeviceTracker } from '../../../types/ParamsDeviceTracker'; +import Util from '../../Util'; + +const FRONTEND_RE = /^https?:\/\/chrome-devtools-frontend\.appspot\.com\/serve_rev\/(@.*)/; + +const TAG = '[DevtoolsClient]'; + +export class DevtoolsClient extends ManagerClient { + public static readonly ACTION = ACTION.DEVTOOLS; + public static readonly TIMEOUT = 1000; + + public static start(params: ParamsDevtools): DevtoolsClient { + return new DevtoolsClient(params); + } + + private timeout?: number; + private readonly hiddenInput: HTMLInputElement; + private readonly tooltip: HTMLSpanElement; + private hideTimeout?: number; + private readonly udid: string; + constructor(params: ParamsDevtools) { + super(params); + this.udid = this.params.udid; + this.openNewConnection(); + this.setTitle(`Devtools ${this.udid}`); + this.setBodyClass('devtools'); + this.hiddenInput = document.createElement('input'); + this.hiddenInput.className = 'hidden'; + this.hiddenInput.setAttribute('hidden', 'hidden'); + document.body.appendChild(this.hiddenInput); + this.tooltip = document.createElement('span'); + this.tooltip.innerText = 'Copied!'; + this.tooltip.className = 'tooltip'; + this.tooltip.style.display = 'none'; + document.body.appendChild(this.tooltip); + } + + public static parseParameters(params: URLSearchParams): ParamsDevtools { + const typedParams = super.parseParameters(params); + const { action } = typedParams; + if (action !== ACTION.DEVTOOLS) { + throw Error('Incorrect action'); + } + return { ...typedParams, action, udid: Util.parseString(params, 'udid', true) }; + } + + private static compareBrowsers = (a: RemoteBrowserInfo, b: RemoteBrowserInfo): number => { + const aBrowser = a.version.Browser; + const bBrowser = b.version.Browser; + if (aBrowser > bBrowser) { + return 1; + } else if (aBrowser < bBrowser) { + return -1; + } + return 0; + }; + + protected buildDirectWebSocketUrl(): URL { + const localUrl = super.buildDirectWebSocketUrl(); + if (typeof this.params.udid === 'string') { + localUrl.searchParams.set('udid', this.params.udid); + } + return localUrl; + } + + protected onSocketClose(event: CloseEvent): void { + console.error(TAG, `Socket closed. Code: ${event.code}.${event.reason ? ' Reason: ' + event.reason : ''}`); + setTimeout(() => { + this.openNewConnection(); + }, 2000); + } + + protected onSocketMessage(event: MessageEvent): void { + console.log("接收到的参数234", event.data) + + let message: Message; + try { + message = JSON.parse(event.data); + } catch (error: any) { + console.error(TAG, error.message); + console.log(TAG, error.data); + return; + } + if (message.type !== DevtoolsClient.ACTION) { + console.log(TAG, `Unknown message type: ${message.type}`); + return; + } + const list = message.data as DevtoolsInfo; + this.buildList(list); + if (!this.timeout) { + this.timeout = window.setTimeout(this.requestListUpdate, DevtoolsClient.TIMEOUT); + } + } + + protected onSocketOpen(): void { + this.requestListUpdate(); + } + + private requestListUpdate = (): void => { + this.timeout = undefined; + if (!this.ws || this.ws.readyState !== this.ws.OPEN) { + return; + } + console.log('发送的参数4'); + + this.ws.send( + JSON.stringify({ + command: RemoteDevtoolsCommand.LIST_DEVTOOLS, + }), + ); + }; + + private createDeviceBlock(info: DevtoolsInfo): HTMLDivElement { + const d = document.createElement('div'); + d.className = 'device'; + d.id = `device:${info.deviceSerial}`; + return d; + } + private createDeviceHeader(info: DevtoolsInfo): HTMLDivElement { + const h = document.createElement('div'); + h.className = 'device-header'; + const n = document.createElement('div'); + n.className = 'device-name'; + n.innerText = info.deviceName; + const s = document.createElement('div'); + s.className = 'device-serial'; + s.innerText = `#${info.deviceSerial.toUpperCase()}`; + const p = document.createElement('div'); + p.className = 'device-ports'; + h.appendChild(n); + h.appendChild(s); + h.appendChild(p); + return h; + } + + private createBrowsersBlock(info: DevtoolsInfo): HTMLDivElement { + const { deviceSerial } = info; + const bs = document.createElement('div'); + bs.className = 'browsers'; + info.browsers.sort(DevtoolsClient.compareBrowsers).forEach((browser) => { + const b = this.createBrowserBlock(deviceSerial, browser); + bs.appendChild(b); + }); + return bs; + } + + private createBrowserBlock(serial: string, info: RemoteBrowserInfo): HTMLDivElement { + const { socket } = info; + const b = document.createElement('div'); + b.id = `${serial}:${socket}`; + b.className = 'browser'; + const h = document.createElement('div'); + h.className = 'browser-header'; + b.appendChild(h); + const n = document.createElement('div'); + n.className = 'browser-name'; + + h.appendChild(n); + const pkg = info.version['Android-Package']; + const browser = info.version.Browser; + let version: string; + const temp = browser.split('/'); + if (temp.length > 1) { + version = temp[1]; + } else { + version = browser; + } + const prefix = socket.indexOf('webview') === 0 ? 'WebView in ' : ''; + n.innerText = `${prefix}${pkg}(${version})`; + const s = document.createElement('span'); + s.setAttribute('tabIndex', '1'); + s.className = 'action'; + s.innerText = 'trace'; + s.setAttribute('hidden', 'hidden'); + h.appendChild(s); + const pages = document.createElement('div'); + pages.className = 'list pages'; + info.targets.forEach((page) => { + pages.appendChild(this.createPageBlock(page, version)); + }); + b.appendChild(pages); + return b; + } + + private createPageBlock(page: RemoteTarget, version?: string): HTMLDivElement { + const row = document.createElement('div'); + row.className = 'row'; + const props = document.createElement('div'); + props.className = 'properties-box'; + row.appendChild(props); + if (page.faviconUrl) { + const img = document.createElement('img'); + img.src = page.faviconUrl; + props.appendChild(img); + } + const subrow = document.createElement('div'); + subrow.className = 'subrow-box'; + props.appendChild(subrow); + const sub1 = document.createElement('div'); + sub1.className = 'subrow'; + subrow.appendChild(sub1); + const n = document.createElement('div'); + n.className = 'name'; + if (page.title) { + n.innerText = page.title; + } + sub1.appendChild(n); + const u = document.createElement('div'); + u.className = 'url'; + if (page.url) { + u.innerText = page.url; + } + sub1.appendChild(u); + const sub2 = document.createElement('div'); + sub2.className = 'subrow webview'; + subrow.appendChild(sub2); + if (page.description) { + try { + const desc = JSON.parse(page.description) as TargetDescription; + const position = document.createElement('div'); + position.className = 'position'; + position.innerText = `at (${desc.screenX}, ${desc.screenY})`; + sub2.appendChild(position); + const size = document.createElement('div'); + size.className = 'size'; + size.innerText = `size ${desc.width} × ${desc.height}`; + sub2.appendChild(size); + } catch (error: any) { } + } + const absoluteAddress = page.devtoolsFrontendUrl && page.devtoolsFrontendUrl.startsWith('http'); + + const actions = document.createElement('div'); + actions.className = 'actions'; + subrow.appendChild(actions); + const inspect = document.createElement('a'); + inspect.setAttribute('tabIndex', '1'); + inspect.className = 'action'; + inspect.innerText = 'inspect'; + actions.appendChild(inspect); + + if (page.devtoolsFrontendUrl) { + inspect.setAttribute('href', page.devtoolsFrontendUrl); + inspect.setAttribute('rel', 'noopener noreferrer'); + inspect.setAttribute('target', '_blank'); + } else { + inspect.classList.add('disabled'); + } + + if (!absoluteAddress) { + inspect.classList.add('disabled'); + } + + if (page.webSocketDebuggerUrl) { + const bundled = document.createElement('a'); + bundled.setAttribute('tabIndex', '1'); + bundled.className = 'action copy'; + bundled.innerText = 'bundled'; + bundled.title = 'Copy link and open manually'; + actions.appendChild(bundled); + + const base = 'devtools://devtools/bundled/inspector.html?experiments=true&ws='; + bundled.setAttribute('href', `${base}${page.webSocketDebuggerUrl}`); + bundled.setAttribute('rel', 'noopener noreferrer'); + bundled.setAttribute('target', '_blank'); + bundled.onclick = this.onDevtoolsLinkClick; + } + + if (page.devtoolsFrontendUrl && page.webSocketDebuggerUrl && absoluteAddress) { + const ur = new URL(page.devtoolsFrontendUrl); + ur.searchParams.delete('ws'); + const urStr = ur.toString(); + const match = urStr.match(FRONTEND_RE); + if (match) { + const str = match[1]; + const temp = str.split('/'); + const revision = temp.shift(); + const rest = temp.join('/'); + const remoteVersion = version ? `remoteVersion=${version}&` : ''; + const opts = `remoteFrontend=true&dockSide=undocked&`; + const ws = `ws=${page.webSocketDebuggerUrl}`; + const url = `devtools://devtools/remote/serve_rev/${revision}/${rest}?${remoteVersion}${opts}${ws}`; + + const remote = document.createElement('a'); + remote.setAttribute('tabIndex', '1'); + remote.className = 'action copy'; + remote.innerText = 'remote'; + remote.title = 'Copy link and open manually'; + actions.appendChild(remote); + + remote.setAttribute('href', url); + remote.setAttribute('rel', 'noopener noreferrer'); + remote.setAttribute('target', '_blank'); + remote.onclick = this.onDevtoolsLinkClick; + } + } + + const pause = document.createElement('span'); + pause.setAttribute('hidden', 'hidden'); + pause.setAttribute('tabIndex', '1'); + pause.className = 'action'; + pause.innerText = 'pause'; + actions.appendChild(pause); + return row; + } + + private onDevtoolsLinkClick = (event: MouseEvent): void => { + const a = event.target as HTMLAnchorElement; + const url = a.getAttribute('href'); + if (!url) { + return; + } + this.hiddenInput.value = url; + this.hiddenInput.removeAttribute('hidden'); + this.hiddenInput.select(); + this.hiddenInput.setSelectionRange(0, url.length); + document.execCommand('copy'); + this.hiddenInput.setAttribute('hidden', 'hidden'); + this.tooltip.style.left = `${event.clientX}px`; + this.tooltip.style.top = `${event.clientY}px`; + this.tooltip.style.display = 'block'; + this.hideTooltip(); + event.preventDefault(); + }; + + private hideTooltip() { + if (this.hideTimeout) { + clearTimeout(this.hideTimeout); + } + this.hideTimeout = window.setTimeout(() => { + this.hideTimeout = undefined; + this.tooltip.style.display = 'none'; + }, 1000); + } + + public buildList(info: DevtoolsInfo): void { + // console.log(info); + const block = this.createDeviceBlock(info); + const header = this.createDeviceHeader(info); + const browsers = this.createBrowsersBlock(info); + block.appendChild(header); + block.appendChild(browsers); + const old = document.getElementById(block.id); + if (old) { + old.parentElement?.replaceChild(block, old); + } else { + document.body.appendChild(block); + } + } + + public static createEntryForDeviceList( + descriptor: GoogDeviceDescriptor, + blockClass: string, + params: ParamsDeviceTracker, + ): HTMLElement | DocumentFragment | undefined { + if (descriptor.state !== 'device') { + return; + } + const entry = document.createElement('div'); + entry.classList.add('devtools', blockClass); + entry.appendChild( + BaseDeviceTracker.buildLink( + { + action: ACTION.DEVTOOLS, + udid: descriptor.udid, + }, + 'devtools', + params, + ), + ); + return entry; + } +} diff --git a/src/app/googDevice/client/FileListingClient.ts b/src/app/googDevice/client/FileListingClient.ts new file mode 100644 index 0000000..accf6f4 --- /dev/null +++ b/src/app/googDevice/client/FileListingClient.ts @@ -0,0 +1,596 @@ +import '../../../style/filelisting.css'; +import { ParamsFileListing } from '../../../types/ParamsFileListing'; +import { ManagerClient } from '../../client/ManagerClient'; +import GoogDeviceDescriptor from '../../../types/GoogDeviceDescriptor'; +import { BaseDeviceTracker } from '../../client/BaseDeviceTracker'; +import { ACTION } from '../../../common/Action'; +import { ParamsDeviceTracker } from '../../../types/ParamsDeviceTracker'; +import Util from '../../Util'; +import Protocol from '@dead50f7/adbkit/lib/adb/protocol'; +import { Entry } from '../Entry'; +import { html } from '../../ui/HtmlTag'; +import * as path from 'path'; +import { ChannelCode } from '../../../common/ChannelCode'; +import { Multiplexer } from '../../../packages/multiplexer/Multiplexer'; +import FilePushHandler, { DragAndPushListener, PushUpdateParams } from '../filePush/FilePushHandler'; +import { AdbkitFilePushStream } from '../filePush/AdbkitFilePushStream'; + +const TAG = '[FileListing]'; + +const parentDirLinkBox = 'parentDirLinkBox'; +const rootDirLinkBox = 'rootDirLinkBox'; +const tempDirLinkBox = 'tempDirLinkBox'; +const storageDirLinkBox = 'storageDirLinkBox'; + +const rootPath = '/'; +const tempPath = '/data/local/tmp'; +const storagePath = '/storage'; + +type Download = { + receivedBytes: number; + entry?: Entry; + progressEl?: HTMLElement; + anchor?: HTMLElement; + chunks: Uint8Array[]; + path: string; + pathToLoadAfter: string; +}; +type Upload = { row: HTMLElement; progressEl: HTMLElement; anchor: HTMLElement; timeout: number | null }; + +enum Foreground { + Drop = 'drop-target', + Connect = 'connect', +} + +const Message: Record = { + [Foreground.Drop]: 'Drop files here', + [Foreground.Connect]: 'Connection lost', +}; + +export class FileListingClient extends ManagerClient implements DragAndPushListener { + public static readonly ACTION = ACTION.FILE_LISTING; + public static readonly PARENT_DIR = '..'; + public static readonly PROPERTY_NAME = 'data-name'; + public static readonly PROPERTY_ENTRY_ID = 'data-entry-id'; + public static REMOVE_ROW_TIMEOUT = 2000; + + public static start(params: ParamsFileListing): FileListingClient { + return new FileListingClient(params); + } + + public static createEntryForDeviceList( + descriptor: GoogDeviceDescriptor, + blockClass: string, + params: ParamsDeviceTracker, + ): HTMLElement | DocumentFragment | undefined { + if (descriptor.state !== 'device') { + return; + } + const entry = document.createElement('div'); + entry.classList.add('file-listing', blockClass); + entry.appendChild( + BaseDeviceTracker.buildLink( + { + action: ACTION.FILE_LISTING, + udid: descriptor.udid, + path: `${tempPath}/`, + }, + 'list files', + params, + ), + ); + return entry; + } + + private readonly serial: string; + private readonly name: string; + private readonly tableBodyId: string; + private readonly wrapperId: string; + private readonly filePushHandler?: FilePushHandler; + private readonly parent: HTMLElement; + private enterCount = 0; + private entries: Entry[] = []; + private path: string; + private requireClean = false; + private requestedPath = ''; + private downloads: Map = new Map(); + private uploads: Map = new Map(); + private tableBody: HTMLElement; + private channels: Set = new Set(); + constructor(params: ParamsFileListing) { + super(params); + this.parent = document.body; + this.serial = this.params.udid; + this.path = this.params.path; + this.openNewConnection(); + this.setTitle(`Listing ${this.serial}`); + this.setBodyClass('file-listing'); + this.name = `${TAG} [${this.serial}]`; + this.tableBodyId = `${Util.escapeUdid(this.serial)}_list`; + this.wrapperId = `wrapper_${this.tableBodyId}`; + const fragment = html`
+

Contents ${this.path}

+ + + + + + + + + + + + + +
NameSizeMTime
+
`.content; + this.tableBody = fragment.getElementById(this.tableBodyId) as HTMLElement; + const wrapper = fragment.getElementById(this.wrapperId); + if (wrapper) { + wrapper.addEventListener('click', (e) => { + if (!e.target || !(e.target instanceof HTMLElement)) { + return; + } + const name = e.target.getAttribute(FileListingClient.PROPERTY_NAME); + if (!name) { + return; + } + e.preventDefault(); + e.cancelBubble = true; + const newPath = path.resolve(this.path, name); + if (newPath !== this.path) { + const entryIdString = e.target.getAttribute(FileListingClient.PROPERTY_ENTRY_ID); + let entry: Entry | undefined; + let anchor: HTMLElement | undefined; + if (entryIdString) { + const entryId = parseInt(entryIdString, 10); + if (!isNaN(entryId) && this.entries[entryId]) { + entry = this.entries[entryId]; + anchor = e.target; + } + } + this.loadContent(newPath, entry, anchor); + } + }); + + if (this.ws instanceof Multiplexer) { + this.filePushHandler = new FilePushHandler(this.parent, new AdbkitFilePushStream(this.ws, this)); + this.filePushHandler.addEventListener(this); + } + } + this.parent.appendChild(fragment); + } + + public onDragEnter(): boolean { + if (this.enterCount === 0) { + this.addForeground(Foreground.Drop); + } + this.enterCount++; + return true; + } + + public onDragLeave(): boolean { + this.enterCount--; + if (this.enterCount < 0) { + this.enterCount = 0; + } + if (this.enterCount === 0) { + this.removeForeground(Foreground.Drop); + } + return true; + } + + public onDrop(): boolean { + this.enterCount = 0; + this.removeForeground(Foreground.Drop); + return true; + } + + private findOrCreateEntryRow(fileName: string): HTMLElement { + const row = document.getElementById(`entry-${fileName}`); + if (row) { + return row; + } + return this.addRow(true, fileName, 'file'); + } + + public onFilePushUpdate(data: PushUpdateParams): void { + const { fileName, progress, error, message, finished } = data; + let upload = this.uploads.get(fileName); + if (!upload || document.getElementById(upload.anchor.id) !== upload.anchor) { + const row = this.findOrCreateEntryRow(fileName); + const anchor = row.getElementsByTagName('a')[0]; + if (!anchor.id) { + anchor.id = `upload_${fileName}`; + } + const progressEl = this.appendProgressElement(anchor); + upload = { row, progressEl, anchor, timeout: null }; + this.uploads.set(fileName, upload); + } + const { row, progressEl, anchor } = upload; + if (error) { + this.uploads.delete(fileName); + progressEl.style.width = `100%`; + progressEl.classList.add('error'); + if (!anchor.classList.contains('error')) { + anchor.classList.add('error'); + anchor.innerText = `${fileName}. ${message}`; + } + if (!upload.timeout) { + upload.timeout = window.setTimeout(() => { + const parent = row.parentElement; + if (parent) { + parent.removeChild(row); + this.reload(); + } + }, FileListingClient.REMOVE_ROW_TIMEOUT); + } + } else { + anchor.innerText = `${fileName}. ${message}`; + progressEl.style.width = `${progress}%`; + } + if (finished && !error) { + this.uploads.delete(fileName); + this.reload(); + } + } + public onError(error: string | Error): void { + console.error(this.name, 'FIXME: implement', error); + } + + private addForeground(type: Foreground): void { + const fragment = html`
+
${Message[type]}
+
`.content; + this.parent.appendChild(fragment); + } + + private removeForeground(type: Foreground): void { + const els = this.parent.getElementsByClassName(type); + Array.from(els).forEach((el) => { + this.parent.removeChild(el); + }); + } + + public static parseParameters(params: URLSearchParams): ParamsFileListing { + const typedParams = super.parseParameters(params); + const { action } = typedParams; + if (action !== ACTION.FILE_LISTING) { + throw Error('Incorrect action'); + } + const pathParam = params.get('path'); + const path = pathParam || '/data/local/tmp'; + return { ...typedParams, action, udid: Util.parseString(params, 'udid', true), path }; + } + + protected buildDirectWebSocketUrl(): URL { + const localUrl = super.buildDirectWebSocketUrl(); + localUrl.searchParams.set('action', ACTION.MULTIPLEX); + return localUrl; + } + + protected onSocketClose(event: CloseEvent): void { + if (this.filePushHandler) { + this.filePushHandler.release(); + } + console.error(this.name, 'socket closed', event.reason); + this.addForeground(Foreground.Connect); + } + + protected onSocketMessage(_e: MessageEvent): void { + // We create separate channel for each request + // Don't expect any messages on this level + console.log("接收到的参数234", _e.data) + + } + + protected onSocketOpen(): void { + this.loadContent(this.path); + } + + protected loadContent(path: string, entry?: Entry, anchor?: HTMLElement, pathToLoadAfter = ''): void { + if (!this.ws || this.ws.readyState !== this.ws.OPEN || !(this.ws instanceof Multiplexer)) { + return; + } + if (!entry && (this.channels.size || this.uploads.size)) { + return; + } + this.requireClean = true; + this.requestedPath = path; + let cmd: string; + if (!entry) { + cmd = Protocol.STAT; + } else if (entry.isFile()) { + cmd = Protocol.RECV; + } else { + cmd = Protocol.LIST; + } + const len = Buffer.byteLength(path, 'utf-8'); + const payload = Buffer.alloc(cmd.length + 4 + len); + let pos = payload.write(cmd, 0); + pos = payload.writeUInt32LE(len, pos); + payload.write(path, pos); + const channel = this.ws.createChannel(payload); + this.channels.add(channel); + const download: Download = { + receivedBytes: 0, + path, + entry, + anchor, + chunks: [], + pathToLoadAfter, + }; + this.downloads.set(channel, download); + const onMessage = (event: MessageEvent): void => { + this.handleReply(channel, event); + }; + const onClose = (): void => { + this.channels.delete(channel); + this.downloads.delete(channel); + channel.removeEventListener('message', onMessage); + channel.removeEventListener('close', onClose); + }; + channel.addEventListener('message', onMessage); + channel.addEventListener('close', onClose); + } + + protected clean(): void { + this.tableBody.innerHTML = ''; + const header = document.getElementById('header'); + if (header) { + header.innerText = `Content ${this.path}`; + } + this.toggleQuickLinks(this.path); + + // FIXME: should do over way around: load content on hash change + const hash = location.hash.replace(/#!/, ''); + const params = new URLSearchParams(hash); + if (params.get('action') === ACTION.FILE_LISTING) { + params.set('path', this.path); + location.hash = `#!${params.toString()}`; + } + } + + protected toggleQuickLinks(path: string): void { + const isRoot = path === rootPath; + const parentEl = document.getElementById(parentDirLinkBox); + if (parentEl) { + parentEl.classList.toggle('hidden', isRoot); + } + const rootEl = document.getElementById(rootDirLinkBox); + if (rootEl) { + rootEl.classList.toggle('hidden', isRoot); + } + const isTemp = path === tempPath; + const tempEl = document.getElementById(tempDirLinkBox); + if (tempEl) { + tempEl.classList.toggle('hidden', isTemp); + } + const isStorage = path === storagePath; + const storageEl = document.getElementById(storageDirLinkBox); + if (storageEl) { + storageEl.classList.toggle('hidden', isStorage); + } + } + + protected handleReply(channel: Multiplexer, e: MessageEvent): void { + const data = Buffer.from(e.data); + const reply = data.slice(0, 4).toString('ascii'); + switch (reply) { + case Protocol.DENT: + const stat = data.slice(4); + const mode = stat.readUInt32LE(0); + const size = stat.readUInt32LE(4); + const mtime = stat.readUInt32LE(8); + const namelen = stat.readUInt32LE(12); + const name = Util.utf8ByteArrayToString(stat.slice(16, 16 + namelen)); + this.addEntry(new Entry(name, mode, size, mtime)); + return; + case Protocol.DONE: + this.finishDownload(channel); + return; + case Protocol.STAT: { + const download = this.downloads.get(channel); + if (!download) { + return; + } + const stat = data.slice(4); + const mode = stat.readUInt32LE(0); + const size = stat.readUInt32LE(4); + const mtime = stat.readUInt32LE(8); + const nameString = path.basename(download.path); + if (mode === 0) { + console.error('FIXME: show error in UI'); + console.error(`Error: no entity "${download.path}"`); + this.channels.delete(channel); + this.loadContent(tempPath); + return; + } + const entry = new Entry(nameString, mode, size, mtime); + let anchor: HTMLElement | undefined; + let nextPath = ''; + if (!entry.isDirectory()) { + nextPath = this.requestedPath = path.dirname(download.path); + const row = this.addEntry(entry); + anchor = row ? row.getElementsByTagName('a')[0] : undefined; + } + this.loadContent(download.path, entry, anchor, nextPath); + break; + } + case Protocol.FAIL: + const length = data.readUInt32LE(4); + const message = Util.utf8ByteArrayToString(data.slice(8, 8 + length)); + console.error(TAG, `FAIL: ${message}`); + return; + case Protocol.DATA: + const download = this.downloads.get(channel); + if (!download) { + return; + } + download.chunks.push(data.slice(4)); + download.receivedBytes += data.length - 4; + if (download.anchor) { + let progressElement = download.progressEl; + if (!progressElement) { + progressElement = this.appendProgressElement(download.anchor); + download.progressEl = progressElement; + } + if (download.entry) { + const { size } = download.entry; + const percent = (download.receivedBytes * 100) / size; + progressElement.style.width = `${percent}%`; + } + } + return; + default: + console.error(`Unexpected "${reply}"`); + } + } + + protected appendProgressElement(anchor: HTMLElement): HTMLElement { + const progressElement = document.createElement('span'); + progressElement.className = 'background-progress'; + const parent = anchor.parentElement; + if (parent) { + parent.appendChild(progressElement); + } + return progressElement; + } + + protected addEntry(entry: Entry): HTMLElement | undefined { + if (this.requireClean) { + this.path = this.requestedPath; + this.requestedPath = ''; + this.clean(); + this.requireClean = false; + this.entries.length = 0; + } + this.entries.push(entry); + const entryId = (this.entries.length - 1).toString(); + if (entry.name === '.') { + return; + } + if (entry.name === FileListingClient.PARENT_DIR) { + const el = document.getElementById(parentDirLinkBox); + if (el) { + const a = el.children[0]; + if (a) { + a.setAttribute(FileListingClient.PROPERTY_ENTRY_ID, entryId); + } + } + return; + } + const type = entry.isDirectory() ? 'dir' : entry.isSymbolicLink() ? 'link' : entry.isFile() ? 'file' : 'else'; + const date = entry.mtime.toLocaleString(); + return this.addRow(false, entry.name, type, entry.size.toString(), date, entryId); + } + + protected addRow(push: boolean, name: string, typeClass: string, size = '', date = '', entryId = ''): HTMLElement { + const row = document.createElement('tr'); + row.id = `entry-${name}`; + row.classList.add('entry-row'); + const nameTd = document.createElement('td'); + nameTd.classList.add('entry-name'); + const link = document.createElement('a'); + link.classList.add('icon', typeClass); + link.setAttribute(FileListingClient.PROPERTY_NAME, name); + if (entryId) { + link.setAttribute(FileListingClient.PROPERTY_ENTRY_ID, entryId); + } + link.innerText = name; + nameTd.appendChild(link); + row.appendChild(nameTd); + if (push) { + nameTd.colSpan = 3; + link.classList.add('push'); + } else { + const href = new URL(location.href); + const hash = new URLSearchParams(href.hash.replace(/^#!/, '')); + hash.set('path', path.join(this.path, name)); + href.hash = `#!${hash.toString()}`; + link.href = href.toString(); + const sizeTd = document.createElement('td'); + sizeTd.classList.add('entry-size'); + sizeTd.innerText = size; + row.appendChild(sizeTd); + const mtimeTd = document.createElement('td'); + mtimeTd.classList.add('entry-time'); + mtimeTd.innerText = date; + row.appendChild(mtimeTd); + } + if (push || !this.tableBody.children.length) { + this.tableBody.insertBefore(row, this.tableBody.firstChild); + } else { + this.tableBody.appendChild(row); + } + return row; + } + + protected finishDownload(channel: Multiplexer): void { + const download = this.downloads.get(channel); + if (!download) { + return; + } + this.downloads.delete(channel); + const el = download.progressEl; + if (el) { + this.cleanProgress(el); + } + let name: string; + if (download.entry && download.entry.isFile()) { + name = download.entry.name; + } else { + // we always should have `download.entry` and never be here + name = path.basename(this.path); + } + if (download.pathToLoadAfter) { + this.channels.delete(channel); + this.loadContent(download.pathToLoadAfter); + } + const file = new File(download.chunks, name, { type: 'application/octet-stream' }); + const a = document.createElement('a'); + a.href = URL.createObjectURL(file); + a.download = `${name}`; + a.click(); + } + + protected cleanProgress(el: HTMLElement): void { + el.classList.add('finished'); + setTimeout(() => { + const parent = el.parentElement; + if (parent) { + parent.removeChild(el); + } + }); + } + + public getPath(): string { + return this.path; + } + + public reload(): void { + this.loadContent(this.path); + } + + protected supportMultiplexing(): boolean { + return true; + } + + protected getChannelInitData(): Buffer { + const serial = Util.stringToUtf8ByteArray(this.serial); + const buffer = Buffer.alloc(4 + 4 + serial.byteLength); + buffer.write(ChannelCode.FSLS, 'ascii'); + buffer.writeUInt32LE(serial.length, 4); + buffer.set(serial, 8); + return buffer; + } +} diff --git a/src/app/googDevice/client/ShellClient.ts b/src/app/googDevice/client/ShellClient.ts new file mode 100644 index 0000000..e78538b --- /dev/null +++ b/src/app/googDevice/client/ShellClient.ts @@ -0,0 +1,147 @@ +import 'xterm/css/xterm.css'; +import { ManagerClient } from '../../client/ManagerClient'; +import { Terminal } from 'xterm'; +import { AttachAddon } from 'xterm-addon-attach'; +import { FitAddon } from 'xterm-addon-fit'; +import { MessageXtermClient } from '../../../types/MessageXtermClient'; +import { ACTION } from '../../../common/Action'; +import { ParamsShell } from '../../../types/ParamsShell'; +import GoogDeviceDescriptor from '../../../types/GoogDeviceDescriptor'; +import { BaseDeviceTracker } from '../../client/BaseDeviceTracker'; +import Util from '../../Util'; +import { ParamsDeviceTracker } from '../../../types/ParamsDeviceTracker'; +import { ChannelCode } from '../../../common/ChannelCode'; + +const TAG = '[ShellClient]'; + +export class ShellClient extends ManagerClient { + public static ACTION = ACTION.SHELL; + public static start(params: ParamsShell): ShellClient { + return new ShellClient(params); + } + + private readonly term: Terminal; + private readonly fitAddon: FitAddon; + private readonly escapedUdid: string; + private readonly udid: string; + + constructor(params: ParamsShell) { + super(params); + this.udid = params.udid; + this.openNewConnection(); + this.setTitle(`Shell ${this.udid}`); + this.setBodyClass('shell'); + if (!this.ws) { + throw Error('No WebSocket'); + } + this.term = new Terminal(); + this.term.loadAddon(new AttachAddon(this.ws)); + this.fitAddon = new FitAddon(); + this.term.loadAddon(this.fitAddon); + this.escapedUdid = Util.escapeUdid(this.udid); + this.term.open(ShellClient.getOrCreateContainer(this.escapedUdid)); + this.updateTerminalSize(); + this.term.focus(); + } + + protected supportMultiplexing(): boolean { + return true; + } + + public static parseParameters(params: URLSearchParams): ParamsShell { + const typedParams = super.parseParameters(params); + const { action } = typedParams; + if (action !== ACTION.SHELL) { + throw Error('Incorrect action'); + } + return { ...typedParams, action, udid: Util.parseString(params, 'udid', true) }; + } + + protected onSocketOpen = (): void => { + this.startShell(this.udid); + }; + + protected onSocketClose(event: CloseEvent): void { + console.log(TAG, `Connection closed: ${event.reason}`); + this.term.dispose(); + } + + protected onSocketMessage(): void { + // messages are processed by Attach Addon + } + + public startShell(udid: string): void { + if (!udid || !this.ws || this.ws.readyState !== this.ws.OPEN) { + return; + } + const { rows, cols } = this.fitAddon.proposeDimensions(); + const message: MessageXtermClient = { + id: 1, + type: 'shell', + data: { + type: 'start', + rows, + cols, + udid, + }, + }; + console.log('发送的参数5'); + + this.ws.send(JSON.stringify(message)); + } + + private static getOrCreateContainer(udid: string): HTMLElement { + let container = document.getElementById(udid); + if (!container) { + container = document.createElement('div'); + container.className = 'terminal-container'; + container.id = udid; + document.body.appendChild(container); + } + return container; + } + + private updateTerminalSize(): void { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const term: any = this.term; + const terminalContainer: HTMLElement = ShellClient.getOrCreateContainer(this.escapedUdid); + const { rows, cols } = this.fitAddon.proposeDimensions(); + const width = + (cols * term._core._renderService.dimensions.actualCellWidth + term._core.viewport.scrollBarWidth).toFixed( + 2, + ) + 'px'; + const height = (rows * term._core._renderService.dimensions.actualCellHeight).toFixed(2) + 'px'; + terminalContainer.style.width = width; + terminalContainer.style.height = height; + this.fitAddon.fit(); + } + + public static createEntryForDeviceList( + descriptor: GoogDeviceDescriptor, + blockClass: string, + params: ParamsDeviceTracker, + ): HTMLElement | DocumentFragment | undefined { + if (descriptor.state !== 'device') { + return; + } + const entry = document.createElement('div'); + entry.classList.add('shell', blockClass); + entry.appendChild( + BaseDeviceTracker.buildLink( + { + action: ACTION.SHELL, + udid: descriptor.udid, + }, + 'shell', + params, + ), + ); + return entry; + } + + protected getChannelInitData(): Buffer { + const buffer = Buffer.alloc(4); + buffer.write(ChannelCode.SHEL, 'ascii'); + return buffer; + } +} diff --git a/src/app/googDevice/client/StreamClientScrcpy.ts b/src/app/googDevice/client/StreamClientScrcpy.ts new file mode 100644 index 0000000..6a22e14 --- /dev/null +++ b/src/app/googDevice/client/StreamClientScrcpy.ts @@ -0,0 +1,506 @@ +import { BaseClient } from '../../client/BaseClient'; +import { ParamsStreamScrcpy } from '../../../types/ParamsStreamScrcpy'; +import { GoogMoreBox } from '../toolbox/GoogMoreBox'; +import { GoogToolBox } from '../toolbox/GoogToolBox'; +import VideoSettings from '../../VideoSettings'; +import Size from '../../Size'; +import { ControlMessage } from '../../controlMessage/ControlMessage'; +import { ClientsStats, DisplayCombinedInfo } from '../../client/StreamReceiver'; +import { CommandControlMessage } from '../../controlMessage/CommandControlMessage'; +import Util from '../../Util'; +import FilePushHandler from '../filePush/FilePushHandler'; +import DragAndPushLogger from '../DragAndPushLogger'; +import { KeyEventListener, KeyInputHandler } from '../KeyInputHandler'; +import { KeyCodeControlMessage } from '../../controlMessage/KeyCodeControlMessage'; +import { BasePlayer, PlayerClass } from '../../player/BasePlayer'; +import GoogDeviceDescriptor from '../../../types/GoogDeviceDescriptor'; +import { ConfigureScrcpy } from './ConfigureScrcpy'; +import { DeviceTracker } from './DeviceTracker'; +import { ControlCenterCommand } from '../../../common/ControlCenterCommand'; +import { html } from '../../ui/HtmlTag'; +import { + FeaturedInteractionHandler, + InteractionHandlerListener, +} from '../../interactionHandler/FeaturedInteractionHandler'; +import DeviceMessage from '../DeviceMessage'; +import { DisplayInfo } from '../../DisplayInfo'; +import { Attribute } from '../../Attribute'; +import { HostTracker } from '../../client/HostTracker'; +import { ACTION } from '../../../common/Action'; +import { StreamReceiverScrcpy } from './StreamReceiverScrcpy'; +import { ParamsDeviceTracker } from '../../../types/ParamsDeviceTracker'; +import { ScrcpyFilePushStream } from '../filePush/ScrcpyFilePushStream'; + +type StartParams = { + udid: string; + playerName?: string; + player?: BasePlayer; + fitToScreen?: boolean; + videoSettings?: VideoSettings; +}; + +const TAG = '[StreamClientScrcpy]'; + +export class StreamClientScrcpy + extends BaseClient + implements KeyEventListener, InteractionHandlerListener { + public static ACTION = 'stream'; + private static players: Map = new Map(); + + private controlButtons?: HTMLElement; + private deviceName = ''; + private clientId = -1; + private clientsCount = -1; + private joinedStream = false; + private requestedVideoSettings?: VideoSettings; + private touchHandler?: FeaturedInteractionHandler; + private moreBox?: GoogMoreBox; + private player?: BasePlayer; + private filePushHandler?: FilePushHandler; + private fitToScreen?: boolean; + private readonly streamReceiver: StreamReceiverScrcpy; + + public static registerPlayer(playerClass: PlayerClass): void { + if (playerClass.isSupported()) { + this.players.set(playerClass.playerFullName, playerClass); + } + } + + public static getPlayers(): PlayerClass[] { + return Array.from(this.players.values()); + } + + private static getPlayerClass(playerName: string): PlayerClass | undefined { + let playerClass: PlayerClass | undefined; + for (const value of StreamClientScrcpy.players.values()) { + if (value.playerFullName === playerName || value.playerCodeName === playerName) { + playerClass = value; + } + } + return playerClass; + } + + public static createPlayer(playerName: string, udid: string, displayInfo?: DisplayInfo): BasePlayer | undefined { + const playerClass = this.getPlayerClass(playerName); + if (!playerClass) { + return; + } + return new playerClass(udid, displayInfo); + } + + public static getFitToScreen(playerName: string, udid: string, displayInfo?: DisplayInfo): boolean { + const playerClass = this.getPlayerClass(playerName); + if (!playerClass) { + return false; + } + return playerClass.getFitToScreenStatus(udid, displayInfo); + } + + public static start( + query: URLSearchParams | ParamsStreamScrcpy, + streamReceiver?: StreamReceiverScrcpy, + player?: BasePlayer, + fitToScreen?: boolean, + videoSettings?: VideoSettings, + ): StreamClientScrcpy { + if (query instanceof URLSearchParams) { + const params = StreamClientScrcpy.parseParameters(query); + return new StreamClientScrcpy(params, streamReceiver, player, fitToScreen, videoSettings); + } else { + return new StreamClientScrcpy(query, streamReceiver, player, fitToScreen, videoSettings); + } + } + + private static createVideoSettingsWithBounds(old: VideoSettings, newBounds: Size): VideoSettings { + return new VideoSettings({ + crop: old.crop, + bitrate: old.bitrate, + bounds: newBounds, + maxFps: old.maxFps, + iFrameInterval: old.iFrameInterval, + sendFrameMeta: old.sendFrameMeta, + lockedVideoOrientation: old.lockedVideoOrientation, + displayId: old.displayId, + codecOptions: old.codecOptions, + encoderName: old.encoderName, + }); + } + + protected constructor( + params: ParamsStreamScrcpy, + streamReceiver?: StreamReceiverScrcpy, + player?: BasePlayer, + fitToScreen?: boolean, + videoSettings?: VideoSettings, + ) { + super(params); + if (streamReceiver) { + this.streamReceiver = streamReceiver; + } else { + this.streamReceiver = new StreamReceiverScrcpy(this.params); + } + + const { udid, player: playerName } = this.params; + this.startStream({ udid, player, playerName, fitToScreen, videoSettings }); + this.setBodyClass('stream'); + } + + public static parseParameters(params: URLSearchParams): ParamsStreamScrcpy { + const typedParams = super.parseParameters(params); + const { action } = typedParams; + if (action !== ACTION.STREAM_SCRCPY) { + throw Error('Incorrect action'); + } + return { + ...typedParams, + action, + player: Util.parseString(params, 'player', true), + udid: Util.parseString(params, 'udid', true), + ws: Util.parseString(params, 'ws', true), + }; + } + + public OnDeviceMessage = (message: DeviceMessage): void => { + if (this.moreBox) { + console.log('message', message) + this.moreBox.OnDeviceMessage(message); + } + }; + + public onVideo = (data: ArrayBuffer): void => { + + if (!this.player) { + return; + } + const STATE = BasePlayer.STATE; + if (this.player.getState() === STATE.PAUSED) { + this.player.play(); + } + if (this.player.getState() === STATE.PLAYING) { + this.player.pushFrame(new Uint8Array(data)); + } + }; + + public onClientsStats = (stats: ClientsStats): void => { + this.deviceName = stats.deviceName; + this.clientId = stats.clientId; + this.setTitle(`Stream ${this.deviceName}`); + }; + + public onDisplayInfo = (infoArray: DisplayCombinedInfo[]): void => { + if (!this.player) { + return; + } + let currentSettings = this.player.getVideoSettings(); + const displayId = currentSettings.displayId; + const info = infoArray.find((value) => { + return value.displayInfo.displayId === displayId; + }); + if (!info) { + return; + } + if (this.player.getState() === BasePlayer.STATE.PAUSED) { + this.player.play(); + } + const { videoSettings, screenInfo } = info; + this.player.setDisplayInfo(info.displayInfo); + if (typeof this.fitToScreen !== 'boolean') { + this.fitToScreen = this.player.getFitToScreenStatus(); + } + if (this.fitToScreen) { + const newBounds = this.getMaxSize(); + if (newBounds) { + currentSettings = StreamClientScrcpy.createVideoSettingsWithBounds(currentSettings, newBounds); + this.player.setVideoSettings(currentSettings, this.fitToScreen, false); + } + } + if (!videoSettings || !screenInfo) { + this.joinedStream = true; + this.sendMessage(CommandControlMessage.createSetVideoSettingsCommand(currentSettings)); + return; + } + + this.clientsCount = info.connectionCount; + let min = VideoSettings.copy(videoSettings); + const oldInfo = this.player.getScreenInfo(); + if (!screenInfo.equals(oldInfo)) { + this.player.setScreenInfo(screenInfo); + } + + if (!videoSettings.equals(currentSettings)) { + this.applyNewVideoSettings(videoSettings, videoSettings.equals(this.requestedVideoSettings)); + } + if (!oldInfo) { + const bounds = currentSettings.bounds; + const videoSize: Size = screenInfo.videoSize; + const onlyOneClient = this.clientsCount === 0; + const smallerThenCurrent = bounds && (bounds.width < videoSize.width || bounds.height < videoSize.height); + if (onlyOneClient || smallerThenCurrent) { + min = currentSettings; + } + const minBounds = currentSettings.bounds?.intersect(min.bounds); + if (minBounds && !minBounds.equals(min.bounds)) { + min = StreamClientScrcpy.createVideoSettingsWithBounds(min, minBounds); + } + } + if (!min.equals(videoSettings) || !this.joinedStream) { + this.joinedStream = true; + this.sendMessage(CommandControlMessage.createSetVideoSettingsCommand(min)); + } + }; + + public onDisconnected = (): void => { + this.streamReceiver.off('deviceMessage', this.OnDeviceMessage); + this.streamReceiver.off('video', this.onVideo); + this.streamReceiver.off('clientsStats', this.onClientsStats); + this.streamReceiver.off('displayInfo', this.onDisplayInfo); + this.streamReceiver.off('disconnected', this.onDisconnected); + + this.filePushHandler?.release(); + this.filePushHandler = undefined; + this.touchHandler?.release(); + this.touchHandler = undefined; + }; + + public startStream({ udid, player, playerName, videoSettings, fitToScreen }: StartParams): void { + if (!udid) { + throw Error(`Invalid udid value: "${udid}"`); + } + + this.fitToScreen = fitToScreen; + if (!player) { + if (typeof playerName !== 'string') { + throw Error('Must provide BasePlayer instance or playerName'); + } + let displayInfo: DisplayInfo | undefined; + if (this.streamReceiver && videoSettings) { + displayInfo = this.streamReceiver.getDisplayInfo(videoSettings.displayId); + } + const p = StreamClientScrcpy.createPlayer(playerName, udid, displayInfo); + if (!p) { + throw Error(`Unsupported player: "${playerName}"`); + } + if (typeof fitToScreen !== 'boolean') { + fitToScreen = StreamClientScrcpy.getFitToScreen(playerName, udid, displayInfo); + } + player = p; + } + this.player = player; + this.setTouchListeners(player); + + if (!videoSettings) { + videoSettings = player.getVideoSettings(); + } + + const deviceView = document.createElement('div'); + deviceView.className = 'device-view'; + const stop = (ev?: string | Event) => { + if (ev && ev instanceof Event && ev.type === 'error') { + console.error(TAG, ev); + } + let parent; + parent = deviceView.parentElement; + if (parent) { + parent.removeChild(deviceView); + } + parent = moreBox.parentElement; + if (parent) { + parent.removeChild(moreBox); + } + this.streamReceiver.stop(); + if (this.player) { + this.player.stop(); + } + }; + + const googMoreBox = (this.moreBox = new GoogMoreBox(udid, player, this)); + const moreBox = googMoreBox.getHolderElement(); + googMoreBox.setOnStop(stop); + const googToolBox = GoogToolBox.createToolBox(udid, player, this, moreBox); + this.controlButtons = googToolBox.getHolderElement(); + deviceView.appendChild(this.controlButtons); + const video = document.createElement('div'); + video.className = 'video'; + deviceView.appendChild(video); + deviceView.appendChild(moreBox); + player.setParent(video); + player.pause(); + + document.body.appendChild(deviceView); + if (fitToScreen) { + const newBounds = this.getMaxSize(); + if (newBounds) { + videoSettings = StreamClientScrcpy.createVideoSettingsWithBounds(videoSettings, newBounds); + } + } + this.applyNewVideoSettings(videoSettings, false); + const element = player.getTouchableElement(); + const logger = new DragAndPushLogger(element); + this.filePushHandler = new FilePushHandler(element, new ScrcpyFilePushStream(this.streamReceiver)); + this.filePushHandler.addEventListener(logger); + + const streamReceiver = this.streamReceiver; + streamReceiver.on('deviceMessage', this.OnDeviceMessage); + streamReceiver.on('video', this.onVideo); + streamReceiver.on('clientsStats', this.onClientsStats); + streamReceiver.on('displayInfo', this.onDisplayInfo); + streamReceiver.on('disconnected', this.onDisconnected); + console.log(TAG, player.getName(), udid); + } + + public sendMessage(message: ControlMessage): void { + console.log("发送消息", message) + this.streamReceiver.sendEvent(message); + } + + public getDeviceName(): string { + return this.deviceName; + } + + public setHandleKeyboardEvents(enabled: boolean): void { + if (enabled) { + KeyInputHandler.addEventListener(this); + } else { + KeyInputHandler.removeEventListener(this); + } + } + + public onKeyEvent(event: KeyCodeControlMessage): void { + this.sendMessage(event); + } + + public sendNewVideoSetting(videoSettings: VideoSettings): void { + this.requestedVideoSettings = videoSettings; + this.sendMessage(CommandControlMessage.createSetVideoSettingsCommand(videoSettings)); + } + + public getClientId(): number { + return this.clientId; + } + + public getClientsCount(): number { + return this.clientsCount; + } + + public getMaxSize(): Size | undefined { + if (!this.controlButtons) { + return; + } + const body = document.body; + const width = (body.clientWidth - this.controlButtons.clientWidth) & ~15; + const height = body.clientHeight & ~15; + return new Size(width, height); + } + + private setTouchListeners(player: BasePlayer): void { + if (this.touchHandler) { + return; + } + this.touchHandler = new FeaturedInteractionHandler(player, this); + } + + private applyNewVideoSettings(videoSettings: VideoSettings, saveToStorage: boolean): void { + let fitToScreen = false; + + // TODO: create control (switch/checkbox) instead + if (videoSettings.bounds && videoSettings.bounds.equals(this.getMaxSize())) { + fitToScreen = true; + } + if (this.player) { + this.player.setVideoSettings(videoSettings, fitToScreen, saveToStorage); + } + } + + public static createEntryForDeviceList( + descriptor: GoogDeviceDescriptor, + blockClass: string, + fullName: string, + params: ParamsDeviceTracker, + ): HTMLElement | DocumentFragment | undefined { + const hasPid = descriptor.pid !== -1; + if (hasPid) { + const configureButtonId = `configure_${Util.escapeUdid(descriptor.udid)}`; + const e = html`
+ +
`; + const a = e.content.getElementById(configureButtonId); + a && (a.onclick = this.onConfigureStreamClick); + return e.content; + } + return; + } + + private static onConfigureStreamClick = (event: MouseEvent): void => { + const button = event.currentTarget as HTMLAnchorElement; + const udid = Util.parseStringEnv(button.getAttribute(Attribute.UDID) || ''); + const fullName = button.getAttribute(Attribute.FULL_NAME); + const secure = Util.parseBooleanEnv(button.getAttribute(Attribute.SECURE) || undefined) || false; + const hostname = Util.parseStringEnv(button.getAttribute(Attribute.HOSTNAME) || undefined) || ''; + const port = Util.parseIntEnv(button.getAttribute(Attribute.PORT) || undefined); + const pathname = Util.parseStringEnv(button.getAttribute(Attribute.PATHNAME) || undefined) || ''; + const useProxy = Util.parseBooleanEnv(button.getAttribute(Attribute.USE_PROXY) || undefined); + if (!udid) { + throw Error(`Invalid udid value: "${udid}"`); + } + if (typeof port !== 'number') { + throw Error(`Invalid port type: ${typeof port}`); + } + const tracker = DeviceTracker.getInstance({ + type: 'android', + secure, + hostname, + port, + pathname, + useProxy, + }); + const descriptor = tracker.getDescriptorByUdid(udid); + if (!descriptor) { + return; + } + event.preventDefault(); + const elements = document.getElementsByName(`${DeviceTracker.AttributePrefixInterfaceSelectFor}${fullName}`); + if (!elements || !elements.length) { + return; + } + const select = elements[0] as HTMLSelectElement; + const optionElement = select.options[select.selectedIndex]; + const ws = optionElement.getAttribute(Attribute.URL); + const name = optionElement.getAttribute(Attribute.NAME); + if (!ws || !name) { + return; + } + const options: ParamsStreamScrcpy = { + udid, + ws, + player: '', + action: ACTION.STREAM_SCRCPY, + secure, + hostname, + port, + pathname, + useProxy, + }; + const dialog = new ConfigureScrcpy(tracker, descriptor, options); + dialog.on('closed', StreamClientScrcpy.onConfigureDialogClosed); + }; + + private static onConfigureDialogClosed = (event: { dialog: ConfigureScrcpy; result: boolean }): void => { + event.dialog.off('closed', StreamClientScrcpy.onConfigureDialogClosed); + if (event.result) { + HostTracker.getInstance().destroy(); + } + }; +} diff --git a/src/app/googDevice/client/StreamReceiverScrcpy.ts b/src/app/googDevice/client/StreamReceiverScrcpy.ts new file mode 100644 index 0000000..f0a114f --- /dev/null +++ b/src/app/googDevice/client/StreamReceiverScrcpy.ts @@ -0,0 +1,24 @@ +import { StreamReceiver } from '../../client/StreamReceiver'; +import { ParamsStreamScrcpy } from '../../../types/ParamsStreamScrcpy'; +import { ACTION } from '../../../common/Action'; +import Util from '../../Util'; + +export class StreamReceiverScrcpy extends StreamReceiver { + public static parseParameters(params: URLSearchParams): ParamsStreamScrcpy { + const typedParams = super.parseParameters(params); + const { action } = typedParams; + if (action !== ACTION.STREAM_SCRCPY) { + throw Error('Incorrect action'); + } + return { + ...typedParams, + action, + udid: Util.parseString(params, 'udid', true), + ws: Util.parseString(params, 'ws', true), + player: Util.parseString(params, 'player', true), + }; + } + protected buildDirectWebSocketUrl(): URL { + return new URL((this.params as ParamsStreamScrcpy).ws); + } +} diff --git a/src/app/googDevice/filePush/AdbkitFilePushStream.ts b/src/app/googDevice/filePush/AdbkitFilePushStream.ts new file mode 100644 index 0000000..f5fa211 --- /dev/null +++ b/src/app/googDevice/filePush/AdbkitFilePushStream.ts @@ -0,0 +1,110 @@ +import { FilePushStream } from './FilePushStream'; +import { CommandControlMessage, FilePushState } from '../../controlMessage/CommandControlMessage'; +import { Multiplexer } from '../../../packages/multiplexer/Multiplexer'; +import { FilePushResponseStatus } from './FilePushResponseStatus'; +import Protocol from '@dead50f7/adbkit/lib/adb/protocol'; +import { FileListingClient } from '../client/FileListingClient'; +import * as path from 'path'; +import FilePushHandler from './FilePushHandler'; + +export class AdbkitFilePushStream extends FilePushStream { + private channels: Map = new Map(); + constructor(private readonly socket: Multiplexer, private readonly fileListingClient: FileListingClient) { + super(); + } + public hasConnection(): boolean { + return this.socket.readyState == this.socket.OPEN; + } + + public isAllowedFile(): boolean { + return true; + } + + public getChannel(id: number): Multiplexer | undefined { + const channel = this.channels.get(id); + let code: FilePushResponseStatus = FilePushResponseStatus.NO_ERROR; + if (!channel) { + code = FilePushResponseStatus.ERROR_UNKNOWN_ID; + } + if (code) { + this.emit('response', { id, code }); + return; + } + return channel; + } + + public sendEventAppend({ id, chunk }: { id: number; chunk: Uint8Array }): void { + const appendParams = { id, chunk, state: FilePushState.APPEND }; + const channel = this.getChannel(id); + if (!channel) { + return; + } + console.log('发送的参数6'); + + channel.send(CommandControlMessage.createPushFileCommand(appendParams).toBuffer()); + } + + public sendEventFinish({ id }: { id: number }): void { + const finishParams = { id, state: FilePushState.FINISH }; + const channel = this.getChannel(id); + if (!channel) { + return; + } + console.log('发送的参数7'); + + channel.send(CommandControlMessage.createPushFileCommand(finishParams).toBuffer()); + } + + public sendEventNew({ id }: { id: number }): void { + let pushId = id; + const newParams = { id, state: FilePushState.NEW }; + const channel = this.socket.createChannel(Buffer.from(Protocol.SEND)); + const onMessage = (event: MessageEvent): void => { + let offset = 0; + const buffer = Buffer.from(event.data); + const id = buffer.readInt16BE(offset); + offset += 2; + const code = buffer.readInt8(offset); + if (code === FilePushResponseStatus.NEW_PUSH_ID) { + this.channels.set(id, channel); + pushId = id; + } + this.emit('response', { id, code }); + }; + const onClose = (event: CloseEvent): void => { + if (!event.wasClean) { + const code = 4000 - event.code; + // this.emit('response', { id: pushId, code }); + this.emit('error', { + id: pushId, + error: new Error(FilePushHandler.getErrorMessage(code, event.reason)), + }); + } + channel.removeEventListener('message', onMessage); + channel.removeEventListener('close', onClose); + }; + channel.addEventListener('message', onMessage); + channel.addEventListener('close', onClose); + console.log('发送的参数7'); + + channel.send(CommandControlMessage.createPushFileCommand(newParams).toBuffer()); + } + + public sendEventStart({ id, fileName, fileSize }: { id: number; fileName: string; fileSize: number }): void { + const filePath = path.join(this.fileListingClient.getPath(), fileName); + const startParams = { id, fileName: filePath, fileSize, state: FilePushState.START }; + const channel = this.getChannel(id); + if (!channel) { + return; + } + console.log('发送的参数8'); + + channel.send(CommandControlMessage.createPushFileCommand(startParams).toBuffer()); + } + + public release(): void { + this.channels.forEach((channel) => { + channel.close(); + }); + } +} diff --git a/src/app/googDevice/filePush/FilePushHandler.ts b/src/app/googDevice/filePush/FilePushHandler.ts new file mode 100644 index 0000000..45a508c --- /dev/null +++ b/src/app/googDevice/filePush/FilePushHandler.ts @@ -0,0 +1,256 @@ +import { DragAndDropHandler, DragEventListener } from '../DragAndDropHandler'; +import { FilePushStream, PushResponse } from './FilePushStream'; +import { FilePushResponseStatus } from './FilePushResponseStatus'; + +type Resolve = (response: PushResponse) => void; + +export type PushUpdateParams = { + pushId: number; + fileName: string; + message: string; + progress: number; + error: boolean; + finished: boolean; +}; + +export interface DragAndPushListener { + onDragEnter: () => boolean; + onDragLeave: () => boolean; + onDrop: () => boolean; + onFilePushUpdate: (data: PushUpdateParams) => void; + onError: (error: Error | string) => void; +} + +const TAG = '[FilePushHandler]'; + +export default class FilePushHandler implements DragEventListener { + public static readonly REQUEST_NEW_PUSH_ID = 0; // ignored on server, when state is `NEW_PUSH_ID` + + private responseWaiter: Map = new Map(); + private listeners: Set = new Set(); + private pushIdFileNameMap: Map = new Map(); + + constructor(private readonly element: HTMLElement, private readonly filePushStream: FilePushStream) { + DragAndDropHandler.addEventListener(this); + filePushStream.on('response', this.onStreamResponse); + filePushStream.on('error', this.onStreamError); + } + + private sendUpdate(params: PushUpdateParams): void { + if (params.error) { + this.pushIdFileNameMap.delete(params.pushId); + } + this.listeners.forEach((listener) => { + listener.onFilePushUpdate(params); + }); + } + + private logError(pushId: number, fileName: string, code: number): void { + const msg = RESPONSE_CODES.get(code) || `Unknown error (${code})`; + this.sendUpdate({ pushId, fileName, message: `error: "${msg}"`, progress: -1, error: true, finished: true }); + } + + private static async getStreamReader(file: File): Promise<{ + reader: ReadableStreamDefaultReader; + result: ReadableStreamReadResult; + }> { + const blob = await new Response(file).blob(); + const reader = blob.stream().getReader() as ReadableStreamDefaultReader; + const result = await reader.read(); + return { reader, result }; + } + + private async pushFile(file: File): Promise { + const start = Date.now(); + const { name: fileName, size: fileSize } = file; + if (!this.filePushStream.hasConnection()) { + this.listeners.forEach((listener) => { + listener.onError('WebSocket is not ready'); + }); + return; + } + const id = FilePushHandler.REQUEST_NEW_PUSH_ID; + this.sendUpdate({ pushId: id, fileName, message: 'begins...', progress: 0, error: false, finished: false }); + this.filePushStream.sendEventNew({ id }); + const { code: pushId } = await this.waitForResponse(id); + if (pushId <= 0) { + return this.logError(pushId, fileName, pushId); + } + + this.pushIdFileNameMap.set(pushId, fileName); + const waitPromise = this.waitForResponse(pushId); + this.filePushStream.sendEventStart({ id: pushId, fileName, fileSize }); + const [{ code: startResponseCode }, { reader, result }] = await Promise.all([ + waitPromise, + FilePushHandler.getStreamReader(file), + ]); + if (startResponseCode !== FilePushResponseStatus.NO_ERROR) { + this.logError(pushId, fileName, startResponseCode); + return; + } + let receivedBytes = 0; + + const processData = async ({ done, value }: { done: boolean; value?: Uint8Array }): Promise => { + if (done || !value) { + this.filePushStream.sendEventFinish({ id: pushId }); + const { code: finishResponseCode } = await this.waitForResponse(pushId); + if (finishResponseCode !== 0) { + this.logError(pushId, fileName, finishResponseCode); + } else { + this.sendUpdate({ + pushId, + fileName, + message: 'success!', + progress: 100, + error: false, + finished: true, + }); + } + console.log(TAG, `File "${fileName}" uploaded in ${Date.now() - start}ms`); + return; + } + + receivedBytes += value.length; + this.filePushStream.sendEventAppend({ id: pushId, chunk: value }); + + const [{ code: appendResponseCode }, result] = await Promise.all([ + this.waitForResponse(pushId), + reader.read(), + ]); + if (appendResponseCode !== 0) { + this.logError(pushId, fileName, appendResponseCode); + return; + } + const progress = (receivedBytes * 100) / fileSize; + const message = `${progress.toFixed(2)}%`; + this.sendUpdate({ pushId, fileName, message, progress, error: false, finished: false }); + return processData(result); + }; + return processData(result); + } + + private waitForResponse(pushId: number): Promise { + return new Promise((resolve) => { + const stored = this.responseWaiter.get(pushId); + if (Array.isArray(stored)) { + stored.push(resolve); + } else if (stored) { + const arr: Resolve[] = [stored]; + arr.push(resolve); + this.responseWaiter.set(pushId, arr); + } else { + this.responseWaiter.set(pushId, resolve); + } + }); + } + + onStreamError = ({ id: pushId, error }: { id: number; error: Error }): void => { + const fileName = this.pushIdFileNameMap.get(pushId) || 'Unknown file'; + this.sendUpdate({ pushId, fileName, message: error.message, progress: -1, error: true, finished: true }); + }; + + onStreamResponse = (response: PushResponse): void => { + let func: Resolve; + let value: PushResponse; + const { code, id: idInResponse } = response; + const id = code === FilePushResponseStatus.NEW_PUSH_ID ? FilePushHandler.REQUEST_NEW_PUSH_ID : response.id; + const resolve = this.responseWaiter.get(id); + if (!resolve) { + console.warn(TAG, `Unexpected push id: "${id}", ${JSON.stringify(response)}`); + return; + } + if (Array.isArray(resolve)) { + func = resolve.shift() as Resolve; + if (!resolve.length) { + this.responseWaiter.delete(id); + } + } else { + func = resolve; + this.responseWaiter.delete(id); + } + if (code === FilePushResponseStatus.NEW_PUSH_ID) { + value = { id, code: idInResponse }; + } else { + value = { id, code: code }; + } + func(value); + }; + public onFilesDrop(files: File[]): boolean { + this.listeners.forEach((listener) => { + listener.onDrop(); + }); + files.forEach((file: File) => { + const { type, name } = file; + if (this.filePushStream.isAllowedFile(file)) { + this.pushFile(file); + } else { + const errorParams: PushUpdateParams = { + pushId: FilePushHandler.REQUEST_NEW_PUSH_ID, + fileName: name, + message: `Unsupported type "${type}"`, + progress: -1, + error: true, + finished: true, + }; + this.sendUpdate(errorParams); + } + }); + return true; + } + + public static getErrorMessage(code: number, message?: string): string { + return message || RESPONSE_CODES.get(code) || 'Unknown error'; + } + + public onDragEnter(): boolean { + let handled = false; + this.listeners.forEach((listener) => { + handled = handled || listener.onDragEnter(); + }); + return handled; + } + + public onDragLeave(): boolean { + let handled = false; + this.listeners.forEach((listener) => { + handled = handled || listener.onDragLeave(); + }); + return handled; + } + + public getElement(): HTMLElement { + return this.element; + } + + public release(): void { + this.filePushStream.off('response', this.onStreamResponse); + this.filePushStream.off('error', this.onStreamError); + this.filePushStream.release(); + DragAndDropHandler.removeEventListener(this); + this.listeners.clear(); + } + + public addEventListener(listener: DragAndPushListener): void { + this.listeners.add(listener); + } + public removeEventListener(listener: DragAndPushListener): void { + this.listeners.delete(listener); + } +} + +const RESPONSE_CODES = new Map([ + [FilePushResponseStatus.NEW_PUSH_ID, 'New push id'], + [FilePushResponseStatus.NO_ERROR, 'No error'], + + [FilePushResponseStatus.ERROR_INVALID_NAME, 'Invalid name'], + [FilePushResponseStatus.ERROR_NO_SPACE, 'No space'], + [FilePushResponseStatus.ERROR_FAILED_TO_DELETE, 'Failed to delete existing'], + [FilePushResponseStatus.ERROR_FAILED_TO_CREATE, 'Failed to create new file'], + [FilePushResponseStatus.ERROR_FILE_NOT_FOUND, 'File not found'], + [FilePushResponseStatus.ERROR_FAILED_TO_WRITE, 'Failed to write to file'], + [FilePushResponseStatus.ERROR_FILE_IS_BUSY, 'File is busy'], + [FilePushResponseStatus.ERROR_INVALID_STATE, 'Invalid state'], + [FilePushResponseStatus.ERROR_UNKNOWN_ID, 'Unknown id'], + [FilePushResponseStatus.ERROR_NO_FREE_ID, 'No free id'], + [FilePushResponseStatus.ERROR_INCORRECT_SIZE, 'Incorrect size'], +]); diff --git a/src/app/googDevice/filePush/FilePushResponseStatus.ts b/src/app/googDevice/filePush/FilePushResponseStatus.ts new file mode 100644 index 0000000..02e655e --- /dev/null +++ b/src/app/googDevice/filePush/FilePushResponseStatus.ts @@ -0,0 +1,16 @@ +export enum FilePushResponseStatus { + NEW_PUSH_ID = 1, + NO_ERROR = 0, + ERROR_INVALID_NAME = -1, + ERROR_NO_SPACE = -2, + ERROR_FAILED_TO_DELETE = -3, + ERROR_FAILED_TO_CREATE = -4, + ERROR_FILE_NOT_FOUND = -5, + ERROR_FAILED_TO_WRITE = -6, + ERROR_FILE_IS_BUSY = -7, + ERROR_INVALID_STATE = -8, + ERROR_UNKNOWN_ID = -9, + ERROR_NO_FREE_ID = -10, + ERROR_INCORRECT_SIZE = -11, + ERROR_OTHER = -12, +} diff --git a/src/app/googDevice/filePush/FilePushStream.ts b/src/app/googDevice/filePush/FilePushStream.ts new file mode 100644 index 0000000..62d7dd4 --- /dev/null +++ b/src/app/googDevice/filePush/FilePushStream.ts @@ -0,0 +1,18 @@ +import { TypedEmitter } from '../../../common/TypedEmitter'; + +export type PushResponse = { id: number; code: number }; + +interface FilePushStreamEvents { + response: PushResponse; + error: { id: number; error: Error }; +} + +export abstract class FilePushStream extends TypedEmitter { + public abstract hasConnection(): boolean; + public abstract isAllowedFile(file: File): boolean; + public abstract sendEventNew(params: { id: number }): void; + public abstract sendEventStart(params: { id: number; fileName: string; fileSize: number }): void; + public abstract sendEventFinish(params: { id: number }): void; + public abstract sendEventAppend(params: { id: number; chunk: Uint8Array }): void; + public abstract release(): void; +} diff --git a/src/app/googDevice/filePush/ScrcpyFilePushStream.ts b/src/app/googDevice/filePush/ScrcpyFilePushStream.ts new file mode 100644 index 0000000..2f3fdc5 --- /dev/null +++ b/src/app/googDevice/filePush/ScrcpyFilePushStream.ts @@ -0,0 +1,54 @@ +import { FilePushStream } from './FilePushStream'; +import { StreamReceiverScrcpy } from '../client/StreamReceiverScrcpy'; +import DeviceMessage from '../DeviceMessage'; +import { CommandControlMessage, FilePushState } from '../../controlMessage/CommandControlMessage'; + +const ALLOWED_TYPES = ['application/vnd.android.package-archive']; +const ALLOWED_NAME_RE = /\.apk$/i; + +export class ScrcpyFilePushStream extends FilePushStream { + constructor(private readonly streamReceiver: StreamReceiverScrcpy) { + super(); + streamReceiver.on('deviceMessage', this.onDeviceMessage); + } + public hasConnection(): boolean { + return this.streamReceiver.hasConnection(); + } + + public isAllowedFile(file: File): boolean { + const { type, name } = file; + return (type && ALLOWED_TYPES.includes(type)) || (!type && ALLOWED_NAME_RE.test(name)); + } + + public sendEventAppend({ id, chunk }: { id: number; chunk: Uint8Array }): void { + const appendParams = { id, chunk, state: FilePushState.APPEND }; + this.streamReceiver.sendEvent(CommandControlMessage.createPushFileCommand(appendParams)); + } + + public sendEventFinish({ id }: { id: number }): void { + const finishParams = { id, state: FilePushState.FINISH }; + this.streamReceiver.sendEvent(CommandControlMessage.createPushFileCommand(finishParams)); + } + + public sendEventNew({ id }: { id: number }): void { + const newParams = { id, state: FilePushState.NEW }; + this.streamReceiver.sendEvent(CommandControlMessage.createPushFileCommand(newParams)); + } + + public sendEventStart({ id, fileName, fileSize }: { id: number; fileName: string; fileSize: number }): void { + const startParams = { id, fileName, fileSize, state: FilePushState.START }; + this.streamReceiver.sendEvent(CommandControlMessage.createPushFileCommand(startParams)); + } + + public release(): void { + this.streamReceiver.off('deviceMessage', this.onDeviceMessage); + } + + onDeviceMessage = (ev: DeviceMessage): void => { + if (ev.type !== DeviceMessage.TYPE_PUSH_RESPONSE) { + return; + } + const stats = ev.getPushStats(); + this.emit('response', stats); + }; +} diff --git a/src/app/googDevice/toolbox/GoogMoreBox.ts b/src/app/googDevice/toolbox/GoogMoreBox.ts new file mode 100644 index 0000000..ccc520c --- /dev/null +++ b/src/app/googDevice/toolbox/GoogMoreBox.ts @@ -0,0 +1,319 @@ +import '../../../style/morebox.css'; +import { BasePlayer } from '../../player/BasePlayer'; +import { TextControlMessage } from '../../controlMessage/TextControlMessage'; +import { CommandControlMessage } from '../../controlMessage/CommandControlMessage'; +import { ControlMessage } from '../../controlMessage/ControlMessage'; +import Size from '../../Size'; +import DeviceMessage from '../DeviceMessage'; +import VideoSettings from '../../VideoSettings'; +import { StreamClientScrcpy } from '../client/StreamClientScrcpy'; + +const TAG = '[GoogMoreBox]'; + +export class GoogMoreBox { + private static defaultSize = new Size(480, 480); + private onStop?: () => void; + private readonly holder: HTMLElement; + private readonly input: HTMLTextAreaElement; + private readonly bitrateInput?: HTMLInputElement; + private readonly maxFpsInput?: HTMLInputElement; + private readonly iFrameIntervalInput?: HTMLInputElement; + private readonly maxWidthInput?: HTMLInputElement; + private readonly maxHeightInput?: HTMLInputElement; + + constructor(udid: string, private player: BasePlayer, private client: StreamClientScrcpy) { + const playerName = player.getName(); + const videoSettings = player.getVideoSettings(); + const { displayId } = videoSettings; + const preferredSettings = player.getPreferredVideoSetting(); + const moreBox = document.createElement('div'); + moreBox.className = 'more-box'; + const nameBox = document.createElement('p'); + nameBox.innerText = `${udid} (${playerName})`; + nameBox.className = 'text-with-shadow'; + moreBox.appendChild(nameBox); + const input = (this.input = document.createElement('textarea')); + input.classList.add('text-area'); + const sendButton = document.createElement('button'); + sendButton.innerText = 'Send as keys'; + + const inputWrapper = GoogMoreBox.wrap('p', [input, sendButton], moreBox); + sendButton.onclick = () => { + if (input.value) { + client.sendMessage(new TextControlMessage(input.value)); + } + }; + + const commands: HTMLElement[] = []; + const codes = CommandControlMessage.Commands; + for (const [action, command] of codes.entries()) { + const btn = document.createElement('button'); + let bitrateInput: HTMLInputElement; + let maxFpsInput: HTMLInputElement; + let iFrameIntervalInput: HTMLInputElement; + let maxWidthInput: HTMLInputElement; + let maxHeightInput: HTMLInputElement; + if (action === ControlMessage.TYPE_CHANGE_STREAM_PARAMETERS) { + const spoiler = document.createElement('div'); + const spoilerLabel = document.createElement('label'); + const spoilerCheck = document.createElement('input'); + + const innerDiv = document.createElement('div'); + const id = `spoiler_video_${udid}_${playerName}_${displayId}_${action}`; + + spoiler.className = 'spoiler'; + spoilerCheck.type = 'checkbox'; + spoilerCheck.id = id; + spoilerLabel.htmlFor = id; + spoilerLabel.innerText = command; + innerDiv.className = 'box'; + spoiler.appendChild(spoilerCheck); + spoiler.appendChild(spoilerLabel); + spoiler.appendChild(innerDiv); + + const bitrateLabel = document.createElement('label'); + bitrateLabel.innerText = 'Bitrate:'; + bitrateInput = document.createElement('input'); + bitrateInput.placeholder = `${preferredSettings.bitrate} bps`; + bitrateInput.value = videoSettings.bitrate.toString(); + GoogMoreBox.wrap('div', [bitrateLabel, bitrateInput], innerDiv); + this.bitrateInput = bitrateInput; + + const maxFpsLabel = document.createElement('label'); + maxFpsLabel.innerText = 'Max fps:'; + maxFpsInput = document.createElement('input'); + maxFpsInput.placeholder = `${preferredSettings.maxFps} fps`; + maxFpsInput.value = videoSettings.maxFps.toString(); + GoogMoreBox.wrap('div', [maxFpsLabel, maxFpsInput], innerDiv); + this.maxFpsInput = maxFpsInput; + + const iFrameIntervalLabel = document.createElement('label'); + iFrameIntervalLabel.innerText = 'I-Frame Interval:'; + iFrameIntervalInput = document.createElement('input'); + iFrameIntervalInput.placeholder = `${preferredSettings.iFrameInterval} seconds`; + iFrameIntervalInput.value = videoSettings.iFrameInterval.toString(); + GoogMoreBox.wrap('div', [iFrameIntervalLabel, iFrameIntervalInput], innerDiv); + this.iFrameIntervalInput = iFrameIntervalInput; + + const { width, height } = videoSettings.bounds || client.getMaxSize() || GoogMoreBox.defaultSize; + const pWidth = preferredSettings.bounds?.width || width; + const pHeight = preferredSettings.bounds?.height || height; + + const maxWidthLabel = document.createElement('label'); + maxWidthLabel.innerText = 'Max width:'; + maxWidthInput = document.createElement('input'); + maxWidthInput.placeholder = `${pWidth} px`; + maxWidthInput.value = width.toString(); + GoogMoreBox.wrap('div', [maxWidthLabel, maxWidthInput], innerDiv); + this.maxWidthInput = maxWidthInput; + + const maxHeightLabel = document.createElement('label'); + maxHeightLabel.innerText = 'Max height:'; + maxHeightInput = document.createElement('input'); + maxHeightInput.placeholder = `${pHeight} px`; + maxHeightInput.value = height.toString(); + GoogMoreBox.wrap('div', [maxHeightLabel, maxHeightInput], innerDiv); + this.maxHeightInput = maxHeightInput; + + innerDiv.appendChild(btn); + const fitButton = document.createElement('button'); + fitButton.innerText = 'Fit'; + fitButton.onclick = this.fit; + innerDiv.insertBefore(fitButton, innerDiv.firstChild); + const resetButton = document.createElement('button'); + resetButton.innerText = 'Reset'; + resetButton.onclick = this.reset; + innerDiv.insertBefore(resetButton, innerDiv.firstChild); + commands.push(spoiler); + } else { + if ( + action === CommandControlMessage.TYPE_SET_CLIPBOARD || + action === CommandControlMessage.TYPE_GET_CLIPBOARD + ) { + inputWrapper.appendChild(btn); + } else { + commands.push(btn); + } + } + btn.innerText = command; + if (action === ControlMessage.TYPE_CHANGE_STREAM_PARAMETERS) { + btn.onclick = () => { + const bitrate = parseInt(bitrateInput.value, 10); + const maxFps = parseInt(maxFpsInput.value, 10); + const iFrameInterval = parseInt(iFrameIntervalInput.value, 10); + if (isNaN(bitrate) || isNaN(maxFps)) { + return; + } + const width = parseInt(maxWidthInput.value, 10) & ~15; + const height = parseInt(maxHeightInput.value, 10) & ~15; + const bounds = new Size(width, height); + const current = player.getVideoSettings(); + const { lockedVideoOrientation, sendFrameMeta, displayId, codecOptions, encoderName } = current; + const videoSettings = new VideoSettings({ + bounds, + bitrate, + maxFps, + iFrameInterval, + lockedVideoOrientation, + sendFrameMeta, + displayId, + codecOptions, + encoderName, + }); + client.sendNewVideoSetting(videoSettings); + }; + } else if (action === CommandControlMessage.TYPE_SET_CLIPBOARD) { + btn.onclick = () => { + const text = input.value; + if (text) { + console.log(`粘贴的内容${text}`); + client.sendMessage(CommandControlMessage.createSetClipboardCommand(text)); + } + }; + } else { + btn.onclick = () => { + client.sendMessage(new CommandControlMessage(action)); + }; + } + } + GoogMoreBox.wrap('p', commands, moreBox); + + const screenPowerModeId = `screen_power_mode_${udid}_${playerName}_${displayId}`; + const screenPowerModeLabel = document.createElement('label'); + screenPowerModeLabel.style.display = 'none'; + const labelTextPrefix = 'Mode'; + const buttonTextPrefix = 'Set screen power mode'; + const screenPowerModeCheck = document.createElement('input'); + screenPowerModeCheck.type = 'checkbox'; + let mode = (screenPowerModeCheck.checked = false) ? 'ON' : 'OFF'; + screenPowerModeCheck.id = screenPowerModeLabel.htmlFor = screenPowerModeId; + screenPowerModeLabel.innerText = `${labelTextPrefix} ${mode}`; + screenPowerModeCheck.onchange = () => { + mode = screenPowerModeCheck.checked ? 'ON' : 'OFF'; + screenPowerModeLabel.innerText = `${labelTextPrefix} ${mode}`; + sendScreenPowerModeButton.innerText = `${buttonTextPrefix} ${mode}`; + }; + const sendScreenPowerModeButton = document.createElement('button'); + sendScreenPowerModeButton.innerText = `${buttonTextPrefix} ${mode}`; + sendScreenPowerModeButton.onclick = () => { + const message = CommandControlMessage.createSetScreenPowerModeCommand(screenPowerModeCheck.checked); + client.sendMessage(message); + }; + GoogMoreBox.wrap('p', [screenPowerModeCheck, screenPowerModeLabel, sendScreenPowerModeButton], moreBox, [ + 'flex-center', + ]); + + const qualityId = `show_video_quality_${udid}_${playerName}_${displayId}`; + const qualityLabel = document.createElement('label'); + const qualityCheck = document.createElement('input'); + qualityCheck.type = 'checkbox'; + qualityCheck.checked = BasePlayer.DEFAULT_SHOW_QUALITY_STATS; + qualityCheck.id = qualityId; + qualityLabel.htmlFor = qualityId; + qualityLabel.innerText = 'Show quality stats'; + GoogMoreBox.wrap('p', [qualityCheck, qualityLabel], moreBox, ['flex-center']); + qualityCheck.onchange = () => { + player.setShowQualityStats(qualityCheck.checked); + }; + + const stop = (ev?: string | Event) => { + if (ev && ev instanceof Event && ev.type === 'error') { + console.error(TAG, ev); + } + const parent = moreBox.parentElement; + if (parent) { + parent.removeChild(moreBox); + } + player.off('video-view-resize', this.onViewVideoResize); + if (this.onStop) { + this.onStop(); + delete this.onStop; + } + }; + + const stopBtn = document.createElement('button') as HTMLButtonElement; + stopBtn.innerText = `Disconnect`; + stopBtn.onclick = stop; + + GoogMoreBox.wrap('p', [stopBtn], moreBox); + player.on('video-view-resize', this.onViewVideoResize); + player.on('video-settings', this.onVideoSettings); + this.holder = moreBox; + } + + private onViewVideoResize = (size: Size): void => { + // padding: 10px + this.holder.style.width = `${size.width - 2 * 10}px`; + }; + + private onVideoSettings = (videoSettings: VideoSettings): void => { + if (this.bitrateInput) { + this.bitrateInput.value = videoSettings.bitrate.toString(); + } + if (this.maxFpsInput) { + this.maxFpsInput.value = videoSettings.maxFps.toString(); + } + if (this.iFrameIntervalInput) { + this.iFrameIntervalInput.value = videoSettings.iFrameInterval.toString(); + } + if (videoSettings.bounds) { + const { width, height } = videoSettings.bounds; + if (this.maxWidthInput) { + this.maxWidthInput.value = width.toString(); + } + if (this.maxHeightInput) { + this.maxHeightInput.value = height.toString(); + } + } + }; + + private fit = (): void => { + const { width, height } = this.client.getMaxSize() || GoogMoreBox.defaultSize; + if (this.maxWidthInput) { + this.maxWidthInput.value = width.toString(); + } + if (this.maxHeightInput) { + this.maxHeightInput.value = height.toString(); + } + }; + + private reset = (): void => { + const preferredSettings = this.player.getPreferredVideoSetting(); + this.onVideoSettings(preferredSettings); + }; + + public OnDeviceMessage(ev: DeviceMessage): void { + if (ev.type !== DeviceMessage.TYPE_CLIPBOARD) { + return; + } + this.input.value = ev.getText(); + console.log("this.input.value", this.input.value) + this.input.select(); + document.execCommand('copy'); + } + + private static wrap( + tagName: string, + elements: HTMLElement[], + parent: HTMLElement, + opt_classes?: string[], + ): HTMLElement { + const wrap = document.createElement(tagName); + if (opt_classes) { + wrap.classList.add(...opt_classes); + } + elements.forEach((e) => { + wrap.appendChild(e); + }); + parent.appendChild(wrap); + return wrap; + } + + public getHolderElement(): HTMLElement { + return this.holder; + } + + public setOnStop(listener: () => void): void { + this.onStop = listener; + } +} diff --git a/src/app/googDevice/toolbox/GoogToolBox.ts b/src/app/googDevice/toolbox/GoogToolBox.ts new file mode 100644 index 0000000..e27c6a4 --- /dev/null +++ b/src/app/googDevice/toolbox/GoogToolBox.ts @@ -0,0 +1,122 @@ +import { ToolBox } from '../../toolbox/ToolBox'; +import KeyEvent from '../android/KeyEvent'; +import SvgImage from '../../ui/SvgImage'; +import { KeyCodeControlMessage } from '../../controlMessage/KeyCodeControlMessage'; +import { ToolBoxButton } from '../../toolbox/ToolBoxButton'; +import { ToolBoxElement } from '../../toolbox/ToolBoxElement'; +import { ToolBoxCheckbox } from '../../toolbox/ToolBoxCheckbox'; +import { StreamClientScrcpy } from '../client/StreamClientScrcpy'; +import { BasePlayer } from '../../player/BasePlayer'; + +const BUTTONS = [ + { + title: 'Power', + code: KeyEvent.KEYCODE_POWER, + icon: SvgImage.Icon.POWER, + }, + { + title: 'Volume up', + code: KeyEvent.KEYCODE_VOLUME_UP, + icon: SvgImage.Icon.VOLUME_UP, + }, + { + title: 'Volume down', + code: KeyEvent.KEYCODE_VOLUME_DOWN, + icon: SvgImage.Icon.VOLUME_DOWN, + }, + { + title: 'Back', + code: KeyEvent.KEYCODE_BACK, + icon: SvgImage.Icon.BACK, + }, + { + title: 'Home', + code: KeyEvent.KEYCODE_HOME, + icon: SvgImage.Icon.HOME, + }, + { + title: 'Overview', + code: KeyEvent.KEYCODE_APP_SWITCH, + icon: SvgImage.Icon.OVERVIEW, + }, + { + title: '打开抖音', + code: -1, // 用特殊值标记,后续处理 + icon: SvgImage.Icon.OVERVIEW, + action: 'launch_app', // 自定义字段,标记特殊动作 + packageName: 'com.ss.android.ugc.aweme', // 抖音包名 + }, +]; + +export class GoogToolBox extends ToolBox { + protected constructor(list: ToolBoxElement[]) { + super(list); + } + + public static createToolBox( + udid: string, + player: BasePlayer, + client: StreamClientScrcpy, + moreBox?: HTMLElement, + ): GoogToolBox { + const playerName = player.getName(); + const list = BUTTONS.slice(); + console.log("list", list) + const handler = ( + type: K, + element: ToolBoxElement, + ) => { + const { code } = element.optional || {}; + + // 处理普通按键事件 + if (code && code !== -1) { + const actionType = type === 'mousedown' ? KeyEvent.ACTION_DOWN : KeyEvent.ACTION_UP; + const event = new KeyCodeControlMessage(actionType, code, 0, 0); + client.sendMessage(event); + // 处理启动抖音应用 + } else if (code == -1) { + console.log("启动抖音") + + } + + }; + const elements: ToolBoxElement[] = list.map((item) => { + const button = new ToolBoxButton(item.title, item.icon, { + code: item.code, + }); + button.addEventListener('mousedown', handler); + button.addEventListener('mouseup', handler); + return button; + }); + if (player.supportsScreenshot) { + const screenshot = new ToolBoxButton('Take screenshot', SvgImage.Icon.CAMERA); + screenshot.addEventListener('click', () => { + player.createScreenshot(client.getDeviceName()); + }); + elements.push(screenshot); + } + + const keyboard = new ToolBoxCheckbox( + 'Capture keyboard', + SvgImage.Icon.KEYBOARD, + `capture_keyboard_${udid}_${playerName}`, + ); + keyboard.addEventListener('click', (_, el) => { + const element = el.getElement(); + client.setHandleKeyboardEvents(element.checked); + }); + elements.push(keyboard); + + if (moreBox) { + const displayId = player.getVideoSettings().displayId; + const id = `show_more_${udid}_${playerName}_${displayId}`; + const more = new ToolBoxCheckbox('More', SvgImage.Icon.MORE, id); + more.addEventListener('click', (_, el) => { + const element = el.getElement(); + moreBox.style.display = element.checked ? 'block' : 'none'; + }); + elements.unshift(more); + } + return new GoogToolBox(elements); + } +} diff --git a/src/app/index.ts b/src/app/index.ts new file mode 100644 index 0000000..cbc7a28 --- /dev/null +++ b/src/app/index.ts @@ -0,0 +1,112 @@ +import '../style/app.css'; +import { StreamClientScrcpy } from './googDevice/client/StreamClientScrcpy'; +import { HostTracker } from './client/HostTracker'; +import { Tool } from './client/Tool'; + +window.onload = async function (): Promise { + const hash = location.hash.replace(/^#!/, ''); + const parsedQuery = new URLSearchParams(hash); + const action = parsedQuery.get('action'); + + /// #if USE_BROADWAY + const { BroadwayPlayer } = await import('./player/BroadwayPlayer'); + StreamClientScrcpy.registerPlayer(BroadwayPlayer); + /// #endif + + /// #if USE_H264_CONVERTER + const { MsePlayer } = await import('./player/MsePlayer'); + StreamClientScrcpy.registerPlayer(MsePlayer); + /// #endif + + /// #if USE_TINY_H264 + const { TinyH264Player } = await import('./player/TinyH264Player'); + StreamClientScrcpy.registerPlayer(TinyH264Player); + /// #endif + + /// #if USE_WEBCODECS + const { WebCodecsPlayer } = await import('./player/WebCodecsPlayer'); + StreamClientScrcpy.registerPlayer(WebCodecsPlayer); + /// #endif + + if (action === StreamClientScrcpy.ACTION && typeof parsedQuery.get('udid') === 'string') { + StreamClientScrcpy.start(parsedQuery); + return; + } + + /// #if INCLUDE_APPL + { + const { DeviceTracker } = await import('./applDevice/client/DeviceTracker'); + + /// #if USE_QVH_SERVER + const { StreamClientQVHack } = await import('./applDevice/client/StreamClientQVHack'); + + DeviceTracker.registerTool(StreamClientQVHack); + + /// #if USE_WEBCODECS + const { WebCodecsPlayer } = await import('./player/WebCodecsPlayer'); + StreamClientQVHack.registerPlayer(WebCodecsPlayer); + /// #endif + + /// #if USE_H264_CONVERTER + const { MsePlayerForQVHack } = await import('./player/MsePlayerForQVHack'); + StreamClientQVHack.registerPlayer(MsePlayerForQVHack); + /// #endif + + if (action === StreamClientQVHack.ACTION && typeof parsedQuery.get('udid') === 'string') { + StreamClientQVHack.start(StreamClientQVHack.parseParameters(parsedQuery)); + return; + } + /// #endif + + /// #if USE_WDA_MJPEG_SERVER + const { StreamClientMJPEG } = await import('./applDevice/client/StreamClientMJPEG'); + DeviceTracker.registerTool(StreamClientMJPEG); + + const { MjpegPlayer } = await import('./player/MjpegPlayer'); + StreamClientMJPEG.registerPlayer(MjpegPlayer); + + if (action === StreamClientMJPEG.ACTION && typeof parsedQuery.get('udid') === 'string') { + StreamClientMJPEG.start(StreamClientMJPEG.parseParameters(parsedQuery)); + return; + } + /// #endif + } + /// #endif + + const tools: Tool[] = []; + + /// #if INCLUDE_ADB_SHELL + const { ShellClient } = await import('./googDevice/client/ShellClient'); + if (action === ShellClient.ACTION && typeof parsedQuery.get('udid') === 'string') { + ShellClient.start(ShellClient.parseParameters(parsedQuery)); + return; + } + tools.push(ShellClient); + /// #endif + + /// #if INCLUDE_DEV_TOOLS + const { DevtoolsClient } = await import('./googDevice/client/DevtoolsClient'); + if (action === DevtoolsClient.ACTION) { + DevtoolsClient.start(DevtoolsClient.parseParameters(parsedQuery)); + return; + } + tools.push(DevtoolsClient); + /// #endif + + /// #if INCLUDE_FILE_LISTING + const { FileListingClient } = await import('./googDevice/client/FileListingClient'); + if (action === FileListingClient.ACTION) { + FileListingClient.start(FileListingClient.parseParameters(parsedQuery)); + return; + } + tools.push(FileListingClient); + /// #endif + + if (tools.length) { + const { DeviceTracker } = await import('./googDevice/client/DeviceTracker'); + tools.forEach((tool) => { + DeviceTracker.registerTool(tool); + }); + } + HostTracker.start(); +}; diff --git a/src/app/interactionHandler/FeaturedInteractionHandler.ts b/src/app/interactionHandler/FeaturedInteractionHandler.ts new file mode 100644 index 0000000..d4b1a84 --- /dev/null +++ b/src/app/interactionHandler/FeaturedInteractionHandler.ts @@ -0,0 +1,132 @@ +import { InteractionEvents, KeyEventNames, InteractionHandler } from './InteractionHandler'; +import { BasePlayer } from '../player/BasePlayer'; +import { ControlMessage } from '../controlMessage/ControlMessage'; +import { TouchControlMessage } from '../controlMessage/TouchControlMessage'; +import MotionEvent from '../MotionEvent'; +import ScreenInfo from '../ScreenInfo'; +import { ScrollControlMessage } from '../controlMessage/ScrollControlMessage'; + +const TAG = '[FeaturedTouchHandler]'; + +export interface InteractionHandlerListener { + sendMessage: (message: ControlMessage) => void; +} + +export class FeaturedInteractionHandler extends InteractionHandler { + private static readonly touchEventsNames: InteractionEvents[] = [ + 'touchstart', + 'touchend', + 'touchmove', + 'touchcancel', + 'mousedown', + 'mouseup', + 'mousemove', + 'wheel', + ]; + private static readonly keyEventsNames: KeyEventNames[] = ['keydown', 'keyup']; + public static SCROLL_EVENT_THROTTLING_TIME = 30; // one event per 50ms + private readonly storedFromMouseEvent = new Map(); + private readonly storedFromTouchEvent = new Map(); + private lastScrollEvent?: { time: number; hScroll: number; vScroll: number }; + + constructor(player: BasePlayer, public readonly listener: InteractionHandlerListener) { + super(player, FeaturedInteractionHandler.touchEventsNames, FeaturedInteractionHandler.keyEventsNames); + this.tag.addEventListener('mouseleave', this.onMouseLeave); + this.tag.addEventListener('mouseenter', this.onMouseEnter); + } + + public buildScrollEvent(event: WheelEvent, screenInfo: ScreenInfo): ScrollControlMessage[] { + const messages: ScrollControlMessage[] = []; + const touchOnClient = InteractionHandler.buildTouchOnClient(event, screenInfo); + if (touchOnClient) { + const hScroll = event.deltaX > 0 ? -1 : event.deltaX < -0 ? 1 : 0; + const vScroll = event.deltaY > 0 ? -1 : event.deltaY < -0 ? 1 : 0; + const time = Date.now(); + if ( + !this.lastScrollEvent || + time - this.lastScrollEvent.time > FeaturedInteractionHandler.SCROLL_EVENT_THROTTLING_TIME || + this.lastScrollEvent.vScroll !== vScroll || + this.lastScrollEvent.hScroll !== hScroll + ) { + this.lastScrollEvent = { time, hScroll, vScroll }; + messages.push(new ScrollControlMessage(touchOnClient.touch.position, hScroll, vScroll)); + } + } + return messages; + } + + protected onInteraction(event: MouseEvent | TouchEvent): void { + const screenInfo = this.player.getScreenInfo(); + if (!screenInfo) { + return; + } + let messages: ControlMessage[]; + let storage: Map; + if (event instanceof MouseEvent) { + if (event.target !== this.tag) { + return; + } + if (window['WheelEvent'] && event instanceof WheelEvent) { + messages = this.buildScrollEvent(event, screenInfo); + } else { + storage = this.storedFromMouseEvent; + messages = this.buildTouchEvent(event, screenInfo, storage); + } + if (this.over) { + this.lastPosition = event; + } + } else if (window['TouchEvent'] && event instanceof TouchEvent) { + // TODO: Research drag from out of the target inside it + if (event.target !== this.tag) { + return; + } + storage = this.storedFromTouchEvent; + messages = this.formatTouchEvent(event, screenInfo, storage); + } else { + console.error(TAG, 'Unsupported event', event); + return; + } + if (event.cancelable) { + event.preventDefault(); + } + event.stopPropagation(); + messages.forEach((message) => { + this.listener.sendMessage(message); + }); + } + + protected onKey(event: KeyboardEvent): void { + if (!this.lastPosition) { + return; + } + const screenInfo = this.player.getScreenInfo(); + if (!screenInfo) { + return; + } + const { ctrlKey, shiftKey } = event; + const { target, button, buttons, clientY, clientX } = this.lastPosition; + const type = InteractionHandler.SIMULATE_MULTI_TOUCH; + const props = { ctrlKey, shiftKey, type, target, button, buttons, clientX, clientY }; + this.buildTouchEvent(props, screenInfo, new Map()); + } + + private onMouseEnter = (): void => { + this.over = true; + }; + private onMouseLeave = (): void => { + this.lastPosition = undefined; + this.over = false; + this.storedFromMouseEvent.forEach((message) => { + this.listener.sendMessage(InteractionHandler.createEmulatedMessage(MotionEvent.ACTION_UP, message)); + }); + this.storedFromMouseEvent.clear(); + this.clearCanvas(); + }; + + public release(): void { + super.release(); + this.tag.removeEventListener('mouseleave', this.onMouseLeave); + this.tag.removeEventListener('mouseenter', this.onMouseEnter); + this.storedFromMouseEvent.clear(); + } +} diff --git a/src/app/interactionHandler/InteractionHandler.ts b/src/app/interactionHandler/InteractionHandler.ts new file mode 100644 index 0000000..8a739b2 --- /dev/null +++ b/src/app/interactionHandler/InteractionHandler.ts @@ -0,0 +1,562 @@ +import MotionEvent from '../MotionEvent'; +import ScreenInfo from '../ScreenInfo'; +import { TouchControlMessage } from '../controlMessage/TouchControlMessage'; +import Size from '../Size'; +import Point from '../Point'; +import Position from '../Position'; +import TouchPointPNG from '../../public/images/multitouch/touch_point.png'; +import CenterPointPNG from '../../public/images/multitouch/center_point.png'; +import Util from '../Util'; +import { BasePlayer } from '../player/BasePlayer'; + +interface Touch { + action: number; + position: Position; + buttons: number; + invalid: boolean; +} + +interface TouchOnClient { + client: { + width: number; + height: number; + }; + touch: Touch; +} + +interface CommonTouchAndMouse { + clientX: number; + clientY: number; + type: string; + target: EventTarget | null; + buttons: number; +} + +interface MiniMouseEvent extends CommonTouchAndMouse { + ctrlKey: boolean; + shiftKey: boolean; + buttons: number; +} + +const TAG = '[TouchHandler]'; + +export type TouchEventNames = + | 'touchstart' + | 'touchend' + | 'touchmove' + | 'touchcancel' + | 'mousedown' + | 'mouseup' + | 'mousemove'; +export type WheelEventNames = 'wheel'; +export type InteractionEvents = TouchEventNames | WheelEventNames; +export type KeyEventNames = 'keydown' | 'keyup'; + +export abstract class InteractionHandler { + protected static readonly SIMULATE_MULTI_TOUCH = 'SIMULATE_MULTI_TOUCH'; + protected static readonly STROKE_STYLE: string = '#00BEA4'; + protected static EVENT_ACTION_MAP: Record = { + touchstart: MotionEvent.ACTION_DOWN, + touchend: MotionEvent.ACTION_UP, + touchmove: MotionEvent.ACTION_MOVE, + touchcancel: MotionEvent.ACTION_UP, + mousedown: MotionEvent.ACTION_DOWN, + mousemove: MotionEvent.ACTION_MOVE, + mouseup: MotionEvent.ACTION_UP, + [InteractionHandler.SIMULATE_MULTI_TOUCH]: -1, + }; + private static options = Util.supportsPassive() ? { passive: false } : false; + private static idToPointerMap: Map = new Map(); + private static pointerToIdMap: Map = new Map(); + private static touchPointRadius = 10; + private static centerPointRadius = 5; + private static touchPointImage?: HTMLImageElement; + private static centerPointImage?: HTMLImageElement; + private static pointImagesLoaded = false; + private static eventListeners: Map> = new Map(); + private multiTouchActive = false; + private multiTouchCenter?: Point; + private multiTouchShift = false; + private dirtyPlace: Point[] = []; + protected readonly ctx: CanvasRenderingContext2D | null; + protected readonly tag: HTMLCanvasElement; + protected over = false; + protected lastPosition?: MouseEvent; + + protected constructor( + public readonly player: BasePlayer, + public readonly touchEventsNames: InteractionEvents[], + public readonly keyEventsNames: KeyEventNames[], + ) { + this.tag = player.getTouchableElement(); + this.ctx = this.tag.getContext('2d'); + InteractionHandler.loadImages(); + InteractionHandler.bindGlobalListeners(this); + } + + protected abstract onInteraction(event: MouseEvent | TouchEvent): void; + protected abstract onKey(event: KeyboardEvent): void; + + protected static bindGlobalListeners(interactionHandler: InteractionHandler): void { + interactionHandler.touchEventsNames.forEach((eventName) => { + let set: Set | undefined = InteractionHandler.eventListeners.get(eventName); + if (!set) { + set = new Set(); + document.body.addEventListener(eventName, this.onInteractionEvent, InteractionHandler.options); + this.eventListeners.set(eventName, set); + } + set.add(interactionHandler); + }); + interactionHandler.keyEventsNames.forEach((eventName) => { + let set = InteractionHandler.eventListeners.get(eventName); + if (!set) { + set = new Set(); + document.body.addEventListener(eventName, this.onKeyEvent); + this.eventListeners.set(eventName, set); + } + set.add(interactionHandler); + }); + } + + protected static unbindListeners(touchHandler: InteractionHandler): void { + touchHandler.touchEventsNames.forEach((eventName) => { + const set = InteractionHandler.eventListeners.get(eventName); + if (!set) { + return; + } + set.delete(touchHandler); + if (set.size <= 0) { + this.eventListeners.delete(eventName); + document.body.removeEventListener(eventName, this.onInteractionEvent); + } + }); + touchHandler.keyEventsNames.forEach((eventName) => { + const set = InteractionHandler.eventListeners.get(eventName); + if (!set) { + return; + } + set.delete(touchHandler); + if (set.size <= 0) { + this.eventListeners.delete(eventName); + document.body.removeEventListener(eventName, this.onKeyEvent); + } + }); + } + + protected static onInteractionEvent = (event: MouseEvent | TouchEvent): void => { + const set = InteractionHandler.eventListeners.get(event.type as TouchEventNames); + if (!set) { + return; + } + set.forEach((instance) => { + instance.onInteraction(event); + }); + }; + + protected static onKeyEvent = (event: KeyboardEvent): void => { + const set = InteractionHandler.eventListeners.get(event.type as KeyEventNames); + if (!set) { + return; + } + set.forEach((instance) => { + instance.onKey(event); + }); + }; + + protected static loadImages(): void { + if (this.pointImagesLoaded) { + return; + } + const total = 2; + let current = 0; + + const onload = (event: Event) => { + if (++current === total) { + this.pointImagesLoaded = true; + } + if (event.target === this.touchPointImage) { + this.touchPointRadius = this.touchPointImage.width / 2; + } else if (event.target === this.centerPointImage) { + this.centerPointRadius = this.centerPointImage.width / 2; + } + }; + const touch = (this.touchPointImage = new Image()); + touch.src = TouchPointPNG; + touch.onload = onload; + const center = (this.centerPointImage = new Image()); + center.src = CenterPointPNG; + center.onload = onload; + } + + protected static getPointerId(type: string, identifier: number): number { + if (this.idToPointerMap.has(identifier)) { + const pointerId = this.idToPointerMap.get(identifier) as number; + if (type === 'touchend' || type === 'touchcancel') { + this.idToPointerMap.delete(identifier); + this.pointerToIdMap.delete(pointerId); + } + return pointerId; + } + let pointerId = 0; + while (this.pointerToIdMap.has(pointerId)) { + pointerId++; + } + this.idToPointerMap.set(identifier, pointerId); + this.pointerToIdMap.set(pointerId, identifier); + return pointerId; + } + + protected static buildTouchOnClient(event: CommonTouchAndMouse, screenInfo: ScreenInfo): TouchOnClient | null { + const action = this.mapTypeToAction(event.type); + const { width, height } = screenInfo.videoSize; + const target: HTMLElement = event.target as HTMLElement; + const rect = target.getBoundingClientRect(); + let { clientWidth, clientHeight } = target; + let touchX = event.clientX - rect.left; + let touchY = event.clientY - rect.top; + let invalid = false; + if (touchX < 0 || touchX > clientWidth || touchY < 0 || touchY > clientHeight) { + invalid = true; + } + const eps = 1e5; + const ratio = width / height; + const shouldBe = Math.round(eps * ratio); + const haveNow = Math.round((eps * clientWidth) / clientHeight); + if (shouldBe > haveNow) { + const realHeight = Math.ceil(clientWidth / ratio); + const top = (clientHeight - realHeight) / 2; + if (touchY < top || touchY > top + realHeight) { + invalid = true; + } + touchY -= top; + clientHeight = realHeight; + } else if (shouldBe < haveNow) { + const realWidth = Math.ceil(clientHeight * ratio); + const left = (clientWidth - realWidth) / 2; + if (touchX < left || touchX > left + realWidth) { + invalid = true; + } + touchX -= left; + clientWidth = realWidth; + } + const x = (touchX * width) / clientWidth; + const y = (touchY * height) / clientHeight; + const size = new Size(width, height); + const point = new Point(x, y); + const position = new Position(point, size); + if (x < 0 || y < 0 || x > width || y > height) { + invalid = true; + } + return { + client: { + width: clientWidth, + height: clientHeight, + }, + touch: { + invalid, + action, + position, + buttons: event.buttons, + }, + }; + } + + private static validateMessage( + originalEvent: MiniMouseEvent | TouchEvent, + message: TouchControlMessage, + storage: Map, + logPrefix: string, + ): TouchControlMessage[] { + const messages: TouchControlMessage[] = []; + const { action, pointerId } = message; + const previous = storage.get(pointerId); + if (action === MotionEvent.ACTION_UP) { + if (!previous) { + console.warn(logPrefix, 'Received ACTION_UP while there are no DOWN stored'); + } else { + storage.delete(pointerId); + messages.push(message); + } + } else if (action === MotionEvent.ACTION_DOWN) { + if (previous) { + console.warn(logPrefix, 'Received ACTION_DOWN while already has one stored'); + } else { + storage.set(pointerId, message); + messages.push(message); + } + } else if (action === MotionEvent.ACTION_MOVE) { + if (!previous) { + if ( + (originalEvent instanceof MouseEvent && originalEvent.buttons) || + (window['TouchEvent'] && originalEvent instanceof TouchEvent) + ) { + console.warn(logPrefix, 'Received ACTION_MOVE while there are no DOWN stored'); + const emulated = InteractionHandler.createEmulatedMessage(MotionEvent.ACTION_DOWN, message); + messages.push(emulated); + storage.set(pointerId, emulated); + } + } else { + messages.push(message); + storage.set(pointerId, message); + } + } + return messages; + } + + protected static createEmulatedMessage(action: number, event: TouchControlMessage): TouchControlMessage { + const { pointerId, position, buttons } = event; + let pressure = event.pressure; + if (action === MotionEvent.ACTION_UP) { + pressure = 0; + } + return new TouchControlMessage(action, pointerId, position, pressure, buttons); + } + + public static mapTypeToAction(type: string): number { + return this.EVENT_ACTION_MAP[type]; + } + + protected getTouch( + e: CommonTouchAndMouse, + screenInfo: ScreenInfo, + ctrlKey: boolean, + shiftKey: boolean, + ): Touch[] | null { + const touchOnClient = InteractionHandler.buildTouchOnClient(e, screenInfo); + if (!touchOnClient) { + return null; + } + const { client, touch } = touchOnClient; + const result: Touch[] = [touch]; + if (!ctrlKey) { + this.multiTouchActive = false; + this.multiTouchCenter = undefined; + this.multiTouchShift = false; + this.clearCanvas(); + return result; + } + const { position, action, buttons } = touch; + const { point, screenSize } = position; + const { width, height } = screenSize; + const { x, y } = point; + if (!this.multiTouchActive) { + if (shiftKey) { + this.multiTouchCenter = point; + this.multiTouchShift = true; + } else { + this.multiTouchCenter = new Point(client.width / 2, client.height / 2); + } + } + this.multiTouchActive = true; + let opposite: Point | undefined; + let invalid = false; + if (this.multiTouchShift && this.multiTouchCenter) { + const oppoX = 2 * this.multiTouchCenter.x - x; + const oppoY = 2 * this.multiTouchCenter.y - y; + opposite = new Point(oppoX, oppoY); + if (!(oppoX <= width && oppoX >= 0 && oppoY <= height && oppoY >= 0)) { + invalid = true; + } + } else { + opposite = new Point(client.width - x, client.height - y); + invalid = touch.invalid; + } + if (opposite) { + result.push({ + invalid, + action, + buttons, + position: new Position(opposite, screenSize), + }); + } + return result; + } + + protected drawCircle(ctx: CanvasRenderingContext2D, point: Point, radius: number): void { + ctx.beginPath(); + ctx.arc(point.x, point.y, radius, 0, Math.PI * 2, true); + ctx.stroke(); + } + + public drawLine(point1: Point, point2: Point): void { + if (!this.ctx) { + return; + } + this.ctx.save(); + this.ctx.strokeStyle = InteractionHandler.STROKE_STYLE; + this.ctx.beginPath(); + this.ctx.moveTo(point1.x, point1.y); + this.ctx.lineTo(point2.x, point2.y); + this.ctx.stroke(); + this.ctx.restore(); + } + + protected drawPoint(point: Point, radius: number, image?: HTMLImageElement): void { + if (!this.ctx) { + return; + } + let { lineWidth } = this.ctx; + if (InteractionHandler.pointImagesLoaded && image) { + radius = image.width / 2; + lineWidth = 0; + this.ctx.drawImage(image, point.x - radius, point.y - radius); + } else { + this.drawCircle(this.ctx, point, radius); + } + + const topLeft = new Point(point.x - radius - lineWidth, point.y - radius - lineWidth); + const bottomRight = new Point(point.x + radius + lineWidth, point.y + radius + lineWidth); + this.updateDirty(topLeft, bottomRight); + } + + public drawPointer(point: Point): void { + this.drawPoint(point, InteractionHandler.touchPointRadius, InteractionHandler.touchPointImage); + if (this.multiTouchCenter) { + this.drawLine(this.multiTouchCenter, point); + } + } + + public drawCenter(point: Point): void { + this.drawPoint(point, InteractionHandler.centerPointRadius, InteractionHandler.centerPointImage); + } + + protected updateDirty(topLeft: Point, bottomRight: Point): void { + if (!this.dirtyPlace.length) { + this.dirtyPlace.push(topLeft, bottomRight); + return; + } + const currentTopLeft = this.dirtyPlace[0]; + const currentBottomRight = this.dirtyPlace[1]; + const newTopLeft = new Point(Math.min(currentTopLeft.x, topLeft.x), Math.min(currentTopLeft.y, topLeft.y)); + const newBottomRight = new Point( + Math.max(currentBottomRight.x, bottomRight.x), + Math.max(currentBottomRight.y, bottomRight.y), + ); + this.dirtyPlace.length = 0; + this.dirtyPlace.push(newTopLeft, newBottomRight); + } + + public clearCanvas(): void { + const { clientWidth, clientHeight } = this.tag; + const ctx = this.ctx; + if (ctx && this.dirtyPlace.length) { + const topLeft = this.dirtyPlace[0]; + const bottomRight = this.dirtyPlace[1]; + this.dirtyPlace.length = 0; + const x = Math.max(topLeft.x, 0); + const y = Math.max(topLeft.y, 0); + const w = Math.min(clientWidth, bottomRight.x - x); + const h = Math.min(clientHeight, bottomRight.y - y); + ctx.clearRect(x, y, w, h); + ctx.strokeStyle = InteractionHandler.STROKE_STYLE; + } + } + + public formatTouchEvent( + e: TouchEvent, + screenInfo: ScreenInfo, + storage: Map, + ): TouchControlMessage[] { + const logPrefix = `${TAG}[formatTouchEvent]`; + const messages: TouchControlMessage[] = []; + const touches = e.changedTouches; + if (touches && touches.length) { + for (let i = 0, l = touches.length; i < l; i++) { + const touch = touches[i]; + const pointerId = InteractionHandler.getPointerId(e.type, touch.identifier); + if (touch.target !== this.tag) { + continue; + } + const previous = storage.get(pointerId); + const item: CommonTouchAndMouse = { + clientX: touch.clientX, + clientY: touch.clientY, + type: e.type, + buttons: MotionEvent.BUTTON_PRIMARY, + target: e.target, + }; + const event = InteractionHandler.buildTouchOnClient(item, screenInfo); + if (event) { + const { action, buttons, position, invalid } = event.touch; + let pressure = 1; + if (action === MotionEvent.ACTION_UP) { + pressure = 0; + } else if (typeof touch.force === 'number') { + pressure = touch.force; + } + if (!invalid) { + const message = new TouchControlMessage(action, pointerId, position, pressure, buttons); + messages.push( + ...InteractionHandler.validateMessage(e, message, storage, `${logPrefix}[validate]`), + ); + } else { + if (previous) { + messages.push(InteractionHandler.createEmulatedMessage(MotionEvent.ACTION_UP, previous)); + storage.delete(pointerId); + } + } + } else { + console.error(logPrefix, `Failed to format touch`, touch); + } + } + } else { + console.error(logPrefix, 'No "touches"', e); + } + return messages; + } + + public buildTouchEvent( + e: MiniMouseEvent, + screenInfo: ScreenInfo, + storage: Map, + ): TouchControlMessage[] { + const logPrefix = `${TAG}[buildTouchEvent]`; + const touches = this.getTouch(e, screenInfo, e.ctrlKey, e.shiftKey); + if (!touches) { + return []; + } + const messages: TouchControlMessage[] = []; + const points: Point[] = []; + this.clearCanvas(); + touches.forEach((touch: Touch, pointerId: number) => { + const { action, buttons, position } = touch; + const previous = storage.get(pointerId); + if (!touch.invalid) { + let pressure = 1.0; + if (action === MotionEvent.ACTION_UP) { + pressure = 0; + } + const message = new TouchControlMessage(action, pointerId, position, pressure, buttons); + messages.push(...InteractionHandler.validateMessage(e, message, storage, `${logPrefix}[validate]`)); + points.push(touch.position.point); + } else { + if (previous) { + points.push(previous.position.point); + } + } + }); + if (this.multiTouchActive) { + if (this.multiTouchCenter) { + this.drawCenter(this.multiTouchCenter); + } + points.forEach((point) => { + this.drawPointer(point); + }); + } + const hasActionUp = messages.find((message) => { + return message.action === MotionEvent.ACTION_UP; + }); + if (hasActionUp && storage.size) { + console.warn(logPrefix, 'Looks like one of Multi-touch pointers was not raised up'); + storage.forEach((message) => { + messages.push(InteractionHandler.createEmulatedMessage(MotionEvent.ACTION_UP, message)); + }); + storage.clear(); + } + return messages; + } + + public release(): void { + InteractionHandler.unbindListeners(this); + } +} diff --git a/src/app/interactionHandler/SimpleInteractionHandler.ts b/src/app/interactionHandler/SimpleInteractionHandler.ts new file mode 100644 index 0000000..7065061 --- /dev/null +++ b/src/app/interactionHandler/SimpleInteractionHandler.ts @@ -0,0 +1,86 @@ +import { InteractionEvents, InteractionHandler } from './InteractionHandler'; +import { BasePlayer } from '../player/BasePlayer'; +import ScreenInfo from '../ScreenInfo'; +import Position from '../Position'; + +export interface TouchHandlerListener { + performClick: (position: Position) => void; + performScroll: (from: Position, to: Position) => void; +} + +const TAG = '[SimpleTouchHandler]'; + +export class SimpleInteractionHandler extends InteractionHandler { + private startPosition?: Position; + private endPosition?: Position; + private static readonly touchEventsNames: InteractionEvents[] = ['mousedown', 'mouseup', 'mousemove']; + private storage = new Map(); + + constructor(player: BasePlayer, private readonly listener: TouchHandlerListener) { + super(player, SimpleInteractionHandler.touchEventsNames, []); + } + + protected onInteraction(event: MouseEvent | TouchEvent): void { + let handled = false; + if (!(event instanceof MouseEvent)) { + return; + } + if (event.target === this.tag) { + const screenInfo: ScreenInfo = this.player.getScreenInfo() as ScreenInfo; + if (!screenInfo) { + return; + } + const events = this.buildTouchEvent(event, screenInfo, this.storage); + if (events.length > 1) { + console.warn(TAG, 'Too many events', events); + return; + } + const downEventName = 'mousedown'; + if (events.length === 1) { + handled = true; + if (event.type === downEventName) { + this.startPosition = events[0].position; + } else { + if (this.startPosition) { + this.endPosition = events[0].position; + } else { + console.warn(TAG, `Received "${event.type}" before "${downEventName}"`); + } + } + if (this.startPosition) { + this.drawPointer(this.startPosition.point); + } + if (this.endPosition) { + this.drawPointer(this.endPosition.point); + if (this.startPosition) { + this.drawLine(this.startPosition.point, this.endPosition.point); + } + } + if (event.type === 'mouseup') { + if (this.startPosition && this.endPosition) { + this.clearCanvas(); + if (this.startPosition.point.distance(this.endPosition.point) < 10) { + this.listener.performClick(this.endPosition); + } else { + this.listener.performScroll(this.startPosition, this.endPosition); + } + } + } + } + if (handled) { + if (event.cancelable) { + event.preventDefault(); + } + event.stopPropagation(); + } + } + if (event.type === 'mouseup') { + this.startPosition = undefined; + this.endPosition = undefined; + } + } + + protected onKey(): void { + throw Error(`${TAG} Unsupported`); + } +} diff --git a/src/app/player/BaseCanvasBasedPlayer.ts b/src/app/player/BaseCanvasBasedPlayer.ts new file mode 100644 index 0000000..613ada3 --- /dev/null +++ b/src/app/player/BaseCanvasBasedPlayer.ts @@ -0,0 +1,236 @@ +import { BasePlayer, PlaybackQuality } from './BasePlayer'; +import ScreenInfo from '../ScreenInfo'; +import VideoSettings from '../VideoSettings'; +import { DisplayInfo } from '../DisplayInfo'; + +type DecodedFrame = { + width: number; + height: number; + frame: any; +}; + +interface CanvasDecoder { + decode(buffer: Uint8Array, width: number, height: number): void; +} + +export abstract class BaseCanvasBasedPlayer extends BasePlayer { + protected framesList: Uint8Array[] = []; + protected decodedFrames: DecodedFrame[] = []; + protected videoStats: PlaybackQuality[] = []; + protected animationFrameId?: number; + protected canvas?: CanvasDecoder; + + public static hasWebGLSupport(): boolean { + // For some reason if I use here `this.tag` image on canvas will be flattened + const testCanvas: HTMLCanvasElement = document.createElement('canvas'); + const validContextNames = ['webgl', 'experimental-webgl', 'moz-webgl', 'webkit-3d']; + let index = 0; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let gl: any = null; + while (!gl && index++ < validContextNames.length) { + try { + gl = testCanvas.getContext(validContextNames[index]); + } catch (error: any) { + gl = null; + } + } + return !!gl; + } + + public static createElement(id?: string): HTMLCanvasElement { + const tag = document.createElement('canvas') as HTMLCanvasElement; + if (typeof id === 'string') { + tag.id = id; + } + tag.className = 'video-layer'; + return tag; + } + + constructor( + udid: string, + displayInfo?: DisplayInfo, + name = 'Canvas', + storageKeyPrefix = 'DummyCanvas', + protected tag: HTMLCanvasElement = BaseCanvasBasedPlayer.createElement(), + ) { + super(udid, displayInfo, name, storageKeyPrefix, tag); + } + + protected abstract decode(data: Uint8Array): void; + public abstract getPreferredVideoSetting(): VideoSettings; + + protected drawDecoded = (): void => { + if (!this.canvas) { + return; + } + if (this.receivedFirstFrame) { + const data = this.decodedFrames.shift(); + if (data) { + const { frame, width, height } = data; + this.canvas.decode(frame, width, height); + } + } + if (this.decodedFrames.length) { + this.animationFrameId = requestAnimationFrame(this.drawDecoded); + } else { + this.animationFrameId = undefined; + } + }; + + protected onFrameDecoded(width: number, height: number, frame: any): void { + if (!this.receivedFirstFrame) { + // decoded frame with previous video settings + return; + } + let dropped = 0; + const maxStored = this.videoSettings.maxFps / 10; // for 100ms + + while (this.decodedFrames.length > maxStored) { + const data = this.decodedFrames.shift(); + if (data) { + this.dropFrame(data.frame); + dropped++; + } + } + this.decodedFrames.push({ width, height, frame }); + this.videoStats.push({ + decodedFrames: 1, + droppedFrames: dropped, + inputBytes: 0, + inputFrames: 0, + timestamp: Date.now(), + }); + if (!this.animationFrameId) { + this.animationFrameId = requestAnimationFrame(this.drawDecoded); + } + } + + protected dropFrame(_frame: any): void { + // dispose frame if required + } + + private shiftFrame(): void { + if (this.getState() !== BasePlayer.STATE.PLAYING) { + return; + } + const first = this.framesList.shift(); + if (first) { + this.decode(first); + } + } + + protected calculateMomentumStats(): void { + const timestamp = Date.now(); + const oneSecondBefore = timestamp - 1000; + + while (this.videoStats.length && this.videoStats[0].timestamp < oneSecondBefore) { + this.videoStats.shift(); + } + while (this.inputBytes.length && this.inputBytes[0].timestamp < oneSecondBefore) { + this.inputBytes.shift(); + } + let decodedFrames = 0; + let droppedFrames = 0; + let inputBytes = 0; + this.videoStats.forEach((item) => { + decodedFrames += item.decodedFrames; + droppedFrames += item.droppedFrames; + }); + this.inputBytes.forEach((item) => { + inputBytes += item.bytes; + }); + this.momentumQualityStats = { + decodedFrames, + droppedFrames, + inputFrames: this.inputBytes.length, + inputBytes, + timestamp, + }; + } + + protected resetStats(): void { + super.resetStats(); + this.videoStats = []; + } + + public getImageDataURL(): string { + return this.tag.toDataURL(); + } + + protected initCanvas(width: number, height: number): void { + if (this.canvas) { + const parent = this.tag.parentNode; + if (parent) { + const tag = BaseCanvasBasedPlayer.createElement(this.tag.id); + tag.className = this.tag.className; + parent.replaceChild(tag, this.tag); + parent.appendChild(this.touchableCanvas); + this.tag = tag; + } + } + this.tag.onerror = (event: Event | string): void => { + console.error(`[${this.name}]`, event); + }; + this.tag.oncontextmenu = (event: MouseEvent): void => { + event.preventDefault(); + }; + this.tag.width = Math.round(width); + this.tag.height = Math.round(height); + } + + public play(): void { + super.play(); + if (this.getState() !== BasePlayer.STATE.PLAYING || !this.screenInfo) { + return; + } + if (!this.canvas) { + const { width, height } = this.screenInfo.videoSize; + this.initCanvas(width, height); + this.resetStats(); + } + this.shiftFrame(); + } + + public stop(): void { + super.stop(); + this.clearState(); + } + + public setScreenInfo(screenInfo: ScreenInfo): void { + super.setScreenInfo(screenInfo); + this.clearState(); + const { width, height } = screenInfo.videoSize; + this.initCanvas(width, height); + this.framesList = []; + if (this.animationFrameId) { + cancelAnimationFrame(this.animationFrameId); + this.animationFrameId = undefined; + } + } + + public pushFrame(frame: Uint8Array): void { + super.pushFrame(frame); + if (BasePlayer.isIFrame(frame)) { + if (this.videoSettings) { + const { maxFps } = this.videoSettings; + if (this.framesList.length > maxFps / 2) { + const dropped = this.framesList.length; + this.framesList = []; + this.videoStats.push({ + decodedFrames: 0, + droppedFrames: dropped, + inputBytes: 0, + inputFrames: 0, + timestamp: Date.now(), + }); + } + } + } + this.framesList.push(frame); + this.shiftFrame(); + } + + protected clearState(): void { + this.framesList = []; + } +} diff --git a/src/app/player/BasePlayer.ts b/src/app/player/BasePlayer.ts new file mode 100644 index 0000000..fb1768f --- /dev/null +++ b/src/app/player/BasePlayer.ts @@ -0,0 +1,577 @@ +import VideoSettings from '../VideoSettings'; +import ScreenInfo from '../ScreenInfo'; +import Rect from '../Rect'; +import Size from '../Size'; +import Util from '../Util'; +import { TypedEmitter } from '../../common/TypedEmitter'; +import { DisplayInfo } from '../DisplayInfo'; + +interface BitrateStat { + timestamp: number; + bytes: number; +} + +interface FramesPerSecondStats { + avgInput: number; + avgDecoded: number; + avgDropped: number; + avgSize: number; +} + +export interface PlaybackQuality { + decodedFrames: number; + droppedFrames: number; + inputFrames: number; + inputBytes: number; + timestamp: number; +} + +export interface PlayerEvents { + 'video-view-resize': Size; + 'input-video-resize': ScreenInfo; + 'video-settings': VideoSettings; +} + +export interface PlayerClass { + playerFullName: string; + playerCodeName: string; + storageKeyPrefix: string; + isSupported(): boolean; + getPreferredVideoSetting(): VideoSettings; + getFitToScreenStatus(deviceName: string, displayInfo?: DisplayInfo): boolean; + loadVideoSettings(deviceName: string, displayInfo?: DisplayInfo): VideoSettings; + saveVideoSettings( + deviceName: string, + videoSettings: VideoSettings, + fitToScreen: boolean, + displayInfo?: DisplayInfo, + ): void; + new(udid: string, displayInfo?: DisplayInfo): BasePlayer; +} + +export abstract class BasePlayer extends TypedEmitter { + private static readonly STAT_BACKGROUND: string = 'rgba(0, 0, 0, 0.5)'; + private static readonly STAT_TEXT_COLOR: string = 'hsl(24, 85%, 50%)'; + public static readonly DEFAULT_SHOW_QUALITY_STATS = false; + public static STATE: Record = { + PLAYING: 1, + PAUSED: 2, + STOPPED: 3, + }; + private static STATS_HEIGHT = 12; + protected screenInfo?: ScreenInfo; + protected videoSettings: VideoSettings; + protected parentElement?: HTMLElement; + protected touchableCanvas: HTMLCanvasElement; + protected inputBytes: BitrateStat[] = []; + protected perSecondQualityStats?: FramesPerSecondStats; + protected momentumQualityStats?: PlaybackQuality; + protected bounds: Size | null = null; + private totalStats: PlaybackQuality = { + decodedFrames: 0, + droppedFrames: 0, + inputFrames: 0, + inputBytes: 0, + timestamp: 0, + }; + private totalStatsCounter = 0; + private dirtyStatsWidth = 0; + private state: number = BasePlayer.STATE.STOPPED; + private qualityAnimationId?: number; + private showQualityStats = BasePlayer.DEFAULT_SHOW_QUALITY_STATS; + protected receivedFirstFrame = false; + private statLines: string[] = []; + public readonly supportsScreenshot: boolean = false; + public readonly resizeVideoToBounds: boolean = false; + protected videoHeight = -1; + protected videoWidth = -1; + + public static storageKeyPrefix = 'BaseDecoder'; + public static playerFullName = 'BasePlayer'; + public static playerCodeName = 'baseplayer'; + public static preferredVideoSettings: VideoSettings = new VideoSettings({ + lockedVideoOrientation: -1, + bitrate: 524288, + maxFps: 24, + iFrameInterval: 5, + bounds: new Size(480, 480), + sendFrameMeta: false, + }); + + public static isSupported(): boolean { + // Implement the check in a child class + return false; + } + + constructor( + public readonly udid: string, + protected displayInfo?: DisplayInfo, + protected name: string = 'BasePlayer', + protected storageKeyPrefix: string = 'Dummy', + protected tag: HTMLElement = document.createElement('div'), + ) { + super(); + this.touchableCanvas = document.createElement('canvas'); + this.touchableCanvas.className = 'touch-layer'; + this.touchableCanvas.oncontextmenu = function (event: MouseEvent): void { + event.preventDefault(); + }; + const preferred = this.getPreferredVideoSetting(); + this.videoSettings = BasePlayer.getVideoSettingFromStorage(preferred, this.storageKeyPrefix, udid, displayInfo); + } + + protected calculateScreenInfoForBounds(videoWidth: number, videoHeight: number): void { + this.videoWidth = videoWidth; + this.videoHeight = videoHeight; + if (this.resizeVideoToBounds) { + let w = videoWidth; + let h = videoHeight; + if (this.bounds) { + let { w: boundsWidth, h: boundsHeight } = this.bounds; + if (w > boundsWidth || h > boundsHeight) { + let scaledHeight; + let scaledWidth; + if (boundsWidth > w) { + scaledHeight = h; + } else { + scaledHeight = (boundsWidth * h) / w; + } + if (boundsHeight > scaledHeight) { + boundsHeight = scaledHeight; + } + if (boundsHeight == h) { + scaledWidth = w; + } else { + scaledWidth = (boundsHeight * w) / h; + } + if (boundsWidth > scaledWidth) { + boundsWidth = scaledWidth; + } + w = boundsWidth | 0; + h = boundsHeight | 0; + this.tag.style.maxWidth = `${w}px`; + this.tag.style.maxHeight = `${h}px`; + } + } + const realScreen = new ScreenInfo(new Rect(0, 0, videoWidth, videoHeight), new Size(w, h), 0); + this.emit('input-video-resize', realScreen); + this.setScreenInfo(new ScreenInfo(new Rect(0, 0, w, h), new Size(w, h), 0)); + } + } + + protected static isIFrame(frame: Uint8Array): boolean { + // last 5 bits === 5: Coded slice of an IDR picture + + // https://www.ietf.org/rfc/rfc3984.txt + // 1.3. Network Abstraction Layer Unit Types + // https://www.itu.int/rec/T-REC-H.264-201906-I/en + // Table 7-1 – NAL unit type codes, syntax element categories, and NAL unit type classes + return frame && frame.length > 4 && (frame[4] & 31) === 5; + } + + private static getStorageKey(storageKeyPrefix: string, udid: string): string { + const { innerHeight, innerWidth } = window; + return `${storageKeyPrefix}:${udid}:${innerWidth}x${innerHeight}`; + } + + private static getFullStorageKey(storageKeyPrefix: string, udid: string, displayInfo?: DisplayInfo): string { + const { innerHeight, innerWidth } = window; + let base = `${storageKeyPrefix}:${udid}:${innerWidth}x${innerHeight}`; + if (displayInfo) { + const { displayId, size } = displayInfo; + base = `${base}:${displayId}:${size.width}x${size.height}`; + } + return base; + } + + public static getFromStorageCompat(prefix: string, udid: string, displayInfo?: DisplayInfo): string | null { + const shortKey = this.getStorageKey(prefix, udid); + const savedInShort = window.localStorage.getItem(shortKey); + if (!displayInfo) { + return savedInShort; + } + const isDefaultDisplay = displayInfo.displayId === DisplayInfo.DEFAULT_DISPLAY; + const fullKey = this.getFullStorageKey(prefix, udid, displayInfo); + const savedInFull = window.localStorage.getItem(fullKey); + if (savedInFull) { + if (savedInShort && isDefaultDisplay) { + window.localStorage.removeItem(shortKey); + } + return savedInFull; + } + if (isDefaultDisplay) { + return savedInShort; + } + return null; + } + + public static getFitToScreenFromStorage( + storageKeyPrefix: string, + udid: string, + displayInfo?: DisplayInfo, + ): boolean { + if (!window.localStorage) { + return false; + } + let parsedValue = false; + const key = `${this.getFullStorageKey(storageKeyPrefix, udid, displayInfo)}:fit`; + const saved = window.localStorage.getItem(key); + if (!saved) { + return false; + } + try { + parsedValue = JSON.parse(saved); + } catch (error: any) { + console.error(`[${this.name}]`, 'Failed to parse', saved); + } + return parsedValue; + } + + public static getVideoSettingFromStorage( + preferred: VideoSettings, + storageKeyPrefix: string, + udid: string, + displayInfo?: DisplayInfo, + ): VideoSettings { + if (!window.localStorage) { + return preferred; + } + const saved = this.getFromStorageCompat(storageKeyPrefix, udid, displayInfo); + if (!saved) { + return preferred; + } + const parsed = JSON.parse(saved); + const { + displayId, + crop, + bitrate, + iFrameInterval, + sendFrameMeta, + lockedVideoOrientation, + codecOptions, + encoderName, + } = parsed; + + // REMOVE `frameRate` + const maxFps = isNaN(parsed.maxFps) ? parsed.frameRate : parsed.maxFps; + // REMOVE `maxSize` + let bounds: Size | null = null; + if (typeof parsed.bounds !== 'object' || isNaN(parsed.bounds.width) || isNaN(parsed.bounds.height)) { + if (!isNaN(parsed.maxSize)) { + bounds = new Size(parsed.maxSize, parsed.maxSize); + } + } else { + bounds = new Size(parsed.bounds.width, parsed.bounds.height); + } + return new VideoSettings({ + displayId: typeof displayId === 'number' ? displayId : 0, + crop: crop ? new Rect(crop.left, crop.top, crop.right, crop.bottom) : preferred.crop, + bitrate: !isNaN(bitrate) ? bitrate : preferred.bitrate, + bounds: bounds !== null ? bounds : preferred.bounds, + maxFps: !isNaN(maxFps) ? maxFps : preferred.maxFps, + iFrameInterval: !isNaN(iFrameInterval) ? iFrameInterval : preferred.iFrameInterval, + sendFrameMeta: typeof sendFrameMeta === 'boolean' ? sendFrameMeta : preferred.sendFrameMeta, + lockedVideoOrientation: !isNaN(lockedVideoOrientation) + ? lockedVideoOrientation + : preferred.lockedVideoOrientation, + codecOptions, + encoderName, + }); + } + + protected static putVideoSettingsToStorage( + storageKeyPrefix: string, + udid: string, + videoSettings: VideoSettings, + fitToScreen: boolean, + displayInfo?: DisplayInfo, + ): void { + if (!window.localStorage) { + return; + } + const key = this.getFullStorageKey(storageKeyPrefix, udid, displayInfo); + window.localStorage.setItem(key, JSON.stringify(videoSettings)); + const fitKey = `${key}:fit`; + window.localStorage.setItem(fitKey, JSON.stringify(fitToScreen)); + } + + public abstract getImageDataURL(): string; + + public createScreenshot(deviceName: string): void { + const a = document.createElement('a'); + a.href = this.getImageDataURL(); + a.download = `${deviceName} ${new Date().toLocaleString()}.png`; + a.click(); + } + + public play(): void { + if (this.needScreenInfoBeforePlay() && !this.screenInfo) { + return; + } + this.state = BasePlayer.STATE.PLAYING; + } + + public pause(): void { + this.state = BasePlayer.STATE.PAUSED; + } + + public stop(): void { + this.state = BasePlayer.STATE.STOPPED; + } + + public getState(): number { + return this.state; + } + + public pushFrame(frame: Uint8Array): void { + if (!this.receivedFirstFrame) { + this.receivedFirstFrame = true; + if (typeof this.qualityAnimationId !== 'number') { + this.qualityAnimationId = requestAnimationFrame(this.updateQualityStats); + } + } + this.inputBytes.push({ + timestamp: Date.now(), + bytes: frame.byteLength, + }); + } + + public abstract getPreferredVideoSetting(): VideoSettings; + protected abstract calculateMomentumStats(): void; + + public getTouchableElement(): HTMLCanvasElement { + return this.touchableCanvas; + } + + public setParent(parent: HTMLElement): void { + this.parentElement = parent; + parent.appendChild(this.tag); + parent.appendChild(this.touchableCanvas); + } + + protected needScreenInfoBeforePlay(): boolean { + return true; + } + + public getVideoSettings(): VideoSettings { + return this.videoSettings; + } + + public setVideoSettings(videoSettings: VideoSettings, fitToScreen: boolean, saveToStorage: boolean): void { + this.videoSettings = videoSettings; + if (saveToStorage) { + BasePlayer.putVideoSettingsToStorage( + this.storageKeyPrefix, + this.udid, + videoSettings, + fitToScreen, + this.displayInfo, + ); + } + this.resetStats(); + this.emit('video-settings', VideoSettings.copy(videoSettings)); + } + + public getScreenInfo(): ScreenInfo | undefined { + return this.screenInfo; + } + + public setScreenInfo(screenInfo: ScreenInfo): void { + if (this.needScreenInfoBeforePlay()) { + this.pause(); + } + this.receivedFirstFrame = false; + this.screenInfo = screenInfo; + const { width, height } = screenInfo.videoSize; + this.touchableCanvas.width = width; + this.touchableCanvas.height = height; + if (this.parentElement) { + this.parentElement.style.height = `${height}px`; + this.parentElement.style.width = `${width}px`; + } + const size = new Size(width, height); + this.emit('video-view-resize', size); + } + + public getName(): string { + return this.name; + } + + protected resetStats(): void { + this.receivedFirstFrame = false; + this.totalStatsCounter = 0; + this.totalStats = { + droppedFrames: 0, + decodedFrames: 0, + inputFrames: 0, + inputBytes: 0, + timestamp: 0, + }; + this.perSecondQualityStats = { + avgDecoded: 0, + avgDropped: 0, + avgInput: 0, + avgSize: 0, + }; + } + + private updateQualityStats = (): void => { + const now = Date.now(); + const oneSecondBefore = now - 1000; + this.calculateMomentumStats(); + if (!this.momentumQualityStats) { + return; + } + if (this.totalStats.timestamp < oneSecondBefore) { + this.totalStats = { + timestamp: now, + decodedFrames: this.totalStats.decodedFrames + this.momentumQualityStats.decodedFrames, + droppedFrames: this.totalStats.droppedFrames + this.momentumQualityStats.droppedFrames, + inputFrames: this.totalStats.inputFrames + this.momentumQualityStats.inputFrames, + inputBytes: this.totalStats.inputBytes + this.momentumQualityStats.inputBytes, + }; + + if (this.totalStatsCounter !== 0) { + this.perSecondQualityStats = { + avgDecoded: this.totalStats.decodedFrames / this.totalStatsCounter, + avgDropped: this.totalStats.droppedFrames / this.totalStatsCounter, + avgInput: this.totalStats.inputFrames / this.totalStatsCounter, + avgSize: this.totalStats.inputBytes / this.totalStatsCounter, + }; + } + this.totalStatsCounter++; + } + this.drawStats(); + if (this.state !== BasePlayer.STATE.STOPPED) { + this.qualityAnimationId = requestAnimationFrame(this.updateQualityStats); + } + }; + + private drawStats(): void { + if (!this.showQualityStats) { + return; + } + const ctx = this.touchableCanvas.getContext('2d'); + if (!ctx) { + return; + } + const newStats = []; + if (this.perSecondQualityStats && this.momentumQualityStats) { + const { decodedFrames, droppedFrames, inputBytes, inputFrames } = this.momentumQualityStats; + const { avgDecoded, avgDropped, avgSize, avgInput } = this.perSecondQualityStats; + const padInput = inputFrames.toString().padStart(3, ' '); + const padDecoded = decodedFrames.toString().padStart(3, ' '); + const padDropped = droppedFrames.toString().padStart(3, ' '); + const padAvgDecoded = avgDecoded.toFixed(1).padStart(5, ' '); + const padAvgDropped = avgDropped.toFixed(1).padStart(5, ' '); + const padAvgInput = avgInput.toFixed(1).padStart(5, ' '); + const prettyBytes = Util.prettyBytes(inputBytes).padStart(8, ' '); + const prettyAvgBytes = Util.prettyBytes(avgSize).padStart(8, ' '); + + newStats.push(`Input bytes: ${prettyBytes} (avg: ${prettyAvgBytes}/s)`); + newStats.push(`Input FPS: ${padInput} (avg: ${padAvgInput})`); + newStats.push(`Dropped FPS: ${padDropped} (avg: ${padAvgDropped})`); + newStats.push(`Decoded FPS: ${padDecoded} (avg: ${padAvgDecoded})`); + } else { + newStats.push(`Not supported`); + } + let changed = this.statLines.length !== newStats.length; + let i = 0; + while (!changed && i++ < newStats.length) { + if (newStats[i] !== this.statLines[i]) { + changed = true; + } + } + + if (changed) { + this.statLines = newStats; + this.updateCanvas(false); + } + } + + private updateCanvas(onlyClear: boolean): void { + const ctx = this.touchableCanvas.getContext('2d'); + if (!ctx) { + return; + } + console.log("123") + const y = this.touchableCanvas.height; + const height = BasePlayer.STATS_HEIGHT; + const lines = this.statLines.length; + const spaces = lines + 1; + const p = height / 2; + const d = p * 2; + const totalHeight = height * lines + p * spaces; + + ctx.clearRect(0, y - totalHeight, this.dirtyStatsWidth + d, totalHeight); + this.dirtyStatsWidth = 0; + + if (onlyClear) { + return; + } + ctx.save(); + ctx.font = `${height}px monospace`; + this.statLines.forEach((text) => { + const textMetrics = ctx.measureText(text); + const dirty = Math.abs(textMetrics.actualBoundingBoxLeft) + Math.abs(textMetrics.actualBoundingBoxRight); + this.dirtyStatsWidth = Math.max(dirty, this.dirtyStatsWidth); + }); + ctx.fillStyle = BasePlayer.STAT_BACKGROUND; + ctx.fillRect(0, y - totalHeight, this.dirtyStatsWidth + d, totalHeight); + ctx.fillStyle = BasePlayer.STAT_TEXT_COLOR; + this.statLines.forEach((text, line) => { + ctx.fillText(text, p, y - p - line * (height + p)); + }); + ctx.restore(); + } + + public setShowQualityStats(value: boolean): void { + this.showQualityStats = value; + if (!value) { + this.updateCanvas(true); + } else { + this.drawStats(); + } + } + + public getShowQualityStats(): boolean { + return this.showQualityStats; + } + + public setBounds(bounds: Size): void { + this.bounds = Size.copy(bounds); + } + + public getDisplayInfo(): DisplayInfo | undefined { + return this.displayInfo; + } + + public setDisplayInfo(displayInfo: DisplayInfo): void { + this.displayInfo = displayInfo; + } + + public abstract getFitToScreenStatus(): boolean; + + public abstract loadVideoSettings(): VideoSettings; + + public static loadVideoSettings(udid: string, displayInfo?: DisplayInfo): VideoSettings { + return this.getVideoSettingFromStorage(this.preferredVideoSettings, this.storageKeyPrefix, udid, displayInfo); + } + + public static getFitToScreenStatus(udid: string, displayInfo?: DisplayInfo): boolean { + return this.getFitToScreenFromStorage(this.storageKeyPrefix, udid, displayInfo); + } + + public static getPreferredVideoSetting(): VideoSettings { + return this.preferredVideoSettings; + } + + public static saveVideoSettings( + udid: string, + videoSettings: VideoSettings, + fitToScreen: boolean, + displayInfo?: DisplayInfo, + ): void { + this.putVideoSettingsToStorage(this.storageKeyPrefix, udid, videoSettings, fitToScreen, displayInfo); + } +} diff --git a/src/app/player/BroadwayPlayer.ts b/src/app/player/BroadwayPlayer.ts new file mode 100644 index 0000000..abe24d6 --- /dev/null +++ b/src/app/player/BroadwayPlayer.ts @@ -0,0 +1,69 @@ +import '../../../vendor/Broadway/avc.wasm.asset'; +import { BaseCanvasBasedPlayer } from './BaseCanvasBasedPlayer'; +import Size from '../Size'; +import YUVCanvas from '../../../vendor/h264-live-player/YUVCanvas'; +import YUVWebGLCanvas from '../../../vendor/h264-live-player/YUVWebGLCanvas'; +import Avc from '../../../vendor/Broadway/Decoder'; +import VideoSettings from '../VideoSettings'; +import Canvas from '../../../vendor/h264-live-player/Canvas'; +import { DisplayInfo } from '../DisplayInfo'; + +export class BroadwayPlayer extends BaseCanvasBasedPlayer { + public static readonly storageKeyPrefix = 'BroadwayDecoder'; + public static readonly playerFullName = 'Broadway.js'; + public static readonly playerCodeName = 'broadway'; + public static readonly preferredVideoSettings: VideoSettings = new VideoSettings({ + lockedVideoOrientation: -1, + bitrate: 524288, + maxFps: 24, + iFrameInterval: 5, + bounds: new Size(480, 480), + sendFrameMeta: false, + }); + + protected canvas?: Canvas; + private avc?: Avc; + public readonly supportsScreenshot: boolean = true; + + public static isSupported(): boolean { + return typeof WebAssembly === 'object' && typeof WebAssembly.instantiate === 'function'; + } + + constructor(udid: string, displayInfo?: DisplayInfo, name = BroadwayPlayer.playerFullName) { + super(udid, displayInfo, name, BroadwayPlayer.storageKeyPrefix); + } + + protected initCanvas(width: number, height: number): void { + super.initCanvas(width, height); + if (BaseCanvasBasedPlayer.hasWebGLSupport()) { + this.canvas = new YUVWebGLCanvas(this.tag, new Size(width, height)); + } else { + this.canvas = new YUVCanvas(this.tag, new Size(width, height)); + } + if (!this.avc) { + this.avc = new Avc(); + } + this.avc.onPictureDecoded = (buffer: Uint8Array, width: number, height: number) => { + this.onFrameDecoded(width, height, buffer); + }; + } + + protected decode(data: Uint8Array): void { + if (!this.avc) { + return; + } + this.avc.decode(data); + } + + public getPreferredVideoSetting(): VideoSettings { + return BroadwayPlayer.preferredVideoSettings; + } + + public getFitToScreenStatus(): boolean { + return BroadwayPlayer.getFitToScreenStatus(this.udid, this.displayInfo); + } + + public loadVideoSettings(): VideoSettings { + return BroadwayPlayer.loadVideoSettings(this.udid, this.displayInfo); + } +} diff --git a/src/app/player/MjpegPlayer.ts b/src/app/player/MjpegPlayer.ts new file mode 100644 index 0000000..ae8d7cf --- /dev/null +++ b/src/app/player/MjpegPlayer.ts @@ -0,0 +1,93 @@ +import { BasePlayer } from './BasePlayer'; +import VideoSettings from '../VideoSettings'; +import { DisplayInfo } from '../DisplayInfo'; + +export class MjpegPlayer extends BasePlayer { + private static dummyVideoSettings = new VideoSettings(); + public static storageKeyPrefix = 'MjpegDecoder'; + public static playerFullName = 'Mjpeg Http Player'; + public static playerCodeName = 'mjpeghttp'; + + public static createElement(id?: string): HTMLImageElement { + const tag = document.createElement('img') as HTMLImageElement; + if (typeof id === 'string') { + tag.id = id; + } + tag.className = 'video-layer'; + return tag; + } + + public static isSupported(): boolean { + // I guess everything supports MJPEG? + return true; + } + + public readonly supportsScreenshot = true; + public readonly resizeVideoToBounds: boolean = true; + + constructor( + udid: string, + displayInfo?: DisplayInfo, + name = 'MJPEG_Player', + storageKeyPrefix = 'MJPEG', + protected tag: HTMLImageElement = MjpegPlayer.createElement(), + ) { + super(udid, displayInfo, name, storageKeyPrefix, tag); + this.tag.onload = () => { + this.checkVideoResize(); + }; + } + + public play(): void { + super.play(); + this.tag.setAttribute('src', `${location.protocol}//${location.host}/mjpeg/${this.udid}`); + } + + public pause(): void { + super.pause(); + this.tag.removeAttribute('src'); + } + + public stop(): void { + super.stop(); + this.tag.removeAttribute('src'); + } + + protected needScreenInfoBeforePlay(): boolean { + return false; + } + + protected calculateMomentumStats(): void { + // not implemented + } + + getFitToScreenStatus(): boolean { + return false; + } + + getImageDataURL(): string { + const canvas = document.createElement('canvas'); + canvas.width = this.videoWidth; + canvas.height = this.videoHeight; + canvas.getContext('2d')?.drawImage(this.tag, 0, 0); + return canvas.toDataURL('image/png'); + } + + getPreferredVideoSetting(): VideoSettings { + return MjpegPlayer.dummyVideoSettings; + } + + loadVideoSettings(): VideoSettings { + return MjpegPlayer.dummyVideoSettings; + } + + checkVideoResize = (): void => { + if (!this.tag) { + return; + } + const { height, width } = this.tag; + if (this.videoHeight !== height || this.videoWidth !== width) { + this.calculateScreenInfoForBounds(width, height); + } + }; +} diff --git a/src/app/player/MsePlayer.ts b/src/app/player/MsePlayer.ts new file mode 100644 index 0000000..3f645ed --- /dev/null +++ b/src/app/player/MsePlayer.ts @@ -0,0 +1,450 @@ +import { BasePlayer } from './BasePlayer'; +import VideoConverter, { setLogger, mimeType } from 'h264-converter'; +import VideoSettings from '../VideoSettings'; +import Size from '../Size'; +import { DisplayInfo } from '../DisplayInfo'; + +interface QualityStats { + timestamp: number; + decodedFrames: number; + droppedFrames: number; +} + +type Block = { + start: number; + end: number; +}; + +export class MsePlayer extends BasePlayer { + public static readonly storageKeyPrefix = 'MseDecoder'; + public static readonly playerFullName = 'H264 Converter'; + public static readonly playerCodeName = 'mse'; + public static readonly preferredVideoSettings: VideoSettings = new VideoSettings({ + lockedVideoOrientation: -1, + bitrate: 7340032, + maxFps: 60, + iFrameInterval: 10, + bounds: new Size(720, 720), + sendFrameMeta: false, + }); + private static DEFAULT_FRAMES_PER_FRAGMENT = 1; + private static DEFAULT_FRAMES_PER_SECOND = 60; + + public static createElement(id?: string): HTMLVideoElement { + const tag = document.createElement('video') as HTMLVideoElement; + tag.muted = true; + tag.autoplay = true; + tag.setAttribute('muted', 'muted'); + tag.setAttribute('autoplay', 'autoplay'); + if (typeof id === 'string') { + tag.id = id; + } + tag.className = 'video-layer'; + return tag; + } + + private converter?: VideoConverter; + private videoStats: QualityStats[] = []; + private noDecodedFramesSince = -1; + private currentTimeNotChangedSince = -1; + private bigBufferSince = -1; + private aheadOfBufferSince = -1; + public fpf: number = MsePlayer.DEFAULT_FRAMES_PER_FRAGMENT; + public readonly supportsScreenshot = true; + private sourceBuffer?: SourceBuffer; + private waitUntilSegmentRemoved = false; + private blocks: Block[] = []; + private frames: Uint8Array[] = []; + private jumpEnd = -1; + private lastTime = -1; + protected canPlay = false; + private seekingSince = -1; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + protected readonly isSafari = !!(window as unknown as any)['safari']; + protected readonly isChrome = navigator.userAgent.includes('Chrome'); + protected readonly isMac = navigator.platform.startsWith('Mac'); + private MAX_TIME_TO_RECOVER = 200; // ms + private MAX_BUFFER = this.isSafari ? 2 : this.isChrome && this.isMac ? 0.9 : 0.2; + private MAX_AHEAD = -0.2; + + public static isSupported(): boolean { + return typeof MediaSource !== 'undefined' && MediaSource.isTypeSupported(mimeType); + } + + constructor( + udid: string, + displayInfo?: DisplayInfo, + name = MsePlayer.playerFullName, + protected tag: HTMLVideoElement = MsePlayer.createElement(), + ) { + super(udid, displayInfo, name, MsePlayer.storageKeyPrefix, tag); + tag.oncontextmenu = function (event: MouseEvent): boolean { + event.preventDefault(); + return false; + }; + tag.addEventListener('error', this.onVideoError); + tag.addEventListener('canplay', this.onVideoCanPlay); + // eslint-disable-next-line @typescript-eslint/no-empty-function + setLogger(() => {}, console.error); + } + + onVideoError = (event: Event): void => { + console.error(`[${this.name}]`, event); + }; + + onVideoCanPlay = (): void => { + this.onCanPlayHandler(); + }; + + private static createConverter( + tag: HTMLVideoElement, + fps: number = MsePlayer.DEFAULT_FRAMES_PER_SECOND, + fpf: number = MsePlayer.DEFAULT_FRAMES_PER_FRAGMENT, + ): VideoConverter { + return new VideoConverter(tag, fps, fpf); + } + + private getVideoPlaybackQuality(): QualityStats | null { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const video = this.tag as any; + if (typeof video.mozDecodedFrames !== 'undefined') { + return null; + } + const now = Date.now(); + if (typeof this.tag.getVideoPlaybackQuality == 'function') { + const temp = this.tag.getVideoPlaybackQuality(); + return { + timestamp: now, + decodedFrames: temp.totalVideoFrames, + droppedFrames: temp.droppedVideoFrames, + }; + } + + // Webkit-specific properties + if (typeof video.webkitDecodedFrameCount !== 'undefined') { + return { + timestamp: now, + decodedFrames: video.webkitDecodedFrameCount, + droppedFrames: video.webkitDroppedFrameCount, + }; + } + return null; + } + + protected onCanPlayHandler(): void { + this.canPlay = true; + this.tag.play(); + this.tag.removeEventListener('canplay', this.onVideoCanPlay); + this.checkVideoResize(); + } + + protected calculateMomentumStats(): void { + const stat = this.getVideoPlaybackQuality(); + if (!stat) { + return; + } + + const timestamp = Date.now(); + const oneSecondBefore = timestamp - 1000; + this.videoStats.push(stat); + + while (this.videoStats.length && this.videoStats[0].timestamp < oneSecondBefore) { + this.videoStats.shift(); + } + while (this.inputBytes.length && this.inputBytes[0].timestamp < oneSecondBefore) { + this.inputBytes.shift(); + } + let inputBytes = 0; + this.inputBytes.forEach((item) => { + inputBytes += item.bytes; + }); + const inputFrames = this.inputBytes.length; + if (this.videoStats.length) { + const oldest = this.videoStats[0]; + const decodedFrames = stat.decodedFrames - oldest.decodedFrames; + const droppedFrames = stat.droppedFrames - oldest.droppedFrames; + // const droppedFrames = inputFrames - decodedFrames; + this.momentumQualityStats = { + decodedFrames, + droppedFrames, + inputBytes, + inputFrames, + timestamp, + }; + } + } + + protected resetStats(): void { + super.resetStats(); + this.videoStats = []; + } + + public getImageDataURL(): string { + const canvas = document.createElement('canvas'); + canvas.width = this.tag.clientWidth; + canvas.height = this.tag.clientHeight; + const ctx = canvas.getContext('2d'); + if (ctx) { + ctx.drawImage(this.tag, 0, 0, canvas.width, canvas.height); + } + + return canvas.toDataURL(); + } + + public play(): void { + super.play(); + if (this.getState() !== BasePlayer.STATE.PLAYING) { + return; + } + if (!this.converter) { + let fps = MsePlayer.DEFAULT_FRAMES_PER_SECOND; + if (this.videoSettings) { + fps = this.videoSettings.maxFps; + } + this.converter = MsePlayer.createConverter(this.tag, fps, this.fpf); + this.canPlay = false; + this.resetStats(); + } + this.converter.play(); + } + + public pause(): void { + super.pause(); + this.stopConverter(); + } + + public stop(): void { + super.stop(); + this.stopConverter(); + } + + public setVideoSettings(videoSettings: VideoSettings, fitToScreen: boolean, saveToStorage: boolean): void { + if (this.videoSettings && this.videoSettings.maxFps !== videoSettings.maxFps) { + const state = this.getState(); + if (this.converter) { + this.stop(); + this.converter = MsePlayer.createConverter(this.tag, videoSettings.maxFps, this.fpf); + this.canPlay = false; + } + if (state === BasePlayer.STATE.PLAYING) { + this.play(); + } + } + super.setVideoSettings(videoSettings, fitToScreen, saveToStorage); + } + + public getPreferredVideoSetting(): VideoSettings { + return MsePlayer.preferredVideoSettings; + } + + checkVideoResize = (): void => { + if (!this.tag) { + return; + } + const { videoHeight, videoWidth } = this.tag; + if (this.videoHeight !== videoHeight || this.videoWidth !== videoWidth) { + this.calculateScreenInfoForBounds(videoWidth, videoHeight); + } + }; + cleanSourceBuffer = (): void => { + if (!this.sourceBuffer) { + return; + } + if (this.sourceBuffer.updating) { + return; + } + if (this.blocks.length < 10) { + return; + } + try { + this.sourceBuffer.removeEventListener('updateend', this.cleanSourceBuffer); + this.waitUntilSegmentRemoved = false; + const removeStart = this.blocks[0].start; + const removeEnd = this.blocks[4].end; + this.blocks = this.blocks.slice(5); + this.sourceBuffer.remove(removeStart, removeEnd); + let frame = this.frames.shift(); + while (frame) { + if (!this.checkForIFrame(frame)) { + this.frames.unshift(frame); + break; + } + frame = this.frames.shift(); + } + } catch (error: any) { + console.error(`[${this.name}]`, 'Failed to clean source buffer'); + } + }; + + jumpToEnd = (): void => { + if (!this.sourceBuffer) { + return; + } + if (this.sourceBuffer.updating) { + return; + } + if (!this.tag.buffered.length) { + return; + } + const end = this.tag.buffered.end(this.tag.seekable.length - 1); + console.log(`[${this.name}]`, `Jumping to the end (${this.jumpEnd}, ${end - this.jumpEnd}).`); + this.tag.currentTime = end; + this.jumpEnd = -1; + this.sourceBuffer.removeEventListener('updateend', this.jumpToEnd); + }; + + public pushFrame(frame: Uint8Array): void { + super.pushFrame(frame); + if (!this.checkForIFrame(frame)) { + this.frames.push(frame); + } else { + this.checkForBadState(); + } + } + + protected checkForBadState(): void { + // Workaround for stalled playback (`stalled` event is not fired, but the image freezes) + const { currentTime } = this.tag; + const now = Date.now(); + // let reasonToJump = ''; + let hasReasonToJump = false; + if (this.momentumQualityStats) { + if (this.momentumQualityStats.decodedFrames === 0 && this.momentumQualityStats.inputFrames > 0) { + if (this.noDecodedFramesSince === -1) { + this.noDecodedFramesSince = now; + } else { + const time = now - this.noDecodedFramesSince; + if (time > this.MAX_TIME_TO_RECOVER) { + // reasonToJump = `No frames decoded for ${time} ms.`; + hasReasonToJump = true; + } + } + } else { + this.noDecodedFramesSince = -1; + } + } + if (currentTime === this.lastTime && this.currentTimeNotChangedSince === -1) { + this.currentTimeNotChangedSince = now; + } else { + this.currentTimeNotChangedSince = -1; + } + this.lastTime = currentTime; + if (this.tag.buffered.length) { + const end = this.tag.buffered.end(0); + const buffered = end - currentTime; + + if ((end | 0) - currentTime > this.MAX_BUFFER) { + if (this.bigBufferSince === -1) { + this.bigBufferSince = now; + } else { + const time = now - this.bigBufferSince; + if (time > this.MAX_TIME_TO_RECOVER) { + // reasonToJump = `Buffer is bigger then ${this.MAX_BUFFER} (${buffered.toFixed( + // 3, + // )}) for ${time} ms.`; + hasReasonToJump = true; + } + } + } else { + this.bigBufferSince = -1; + } + if (buffered < this.MAX_AHEAD) { + if (this.aheadOfBufferSince === -1) { + this.aheadOfBufferSince = now; + } else { + const time = now - this.aheadOfBufferSince; + if (time > this.MAX_TIME_TO_RECOVER) { + // reasonToJump = `Current time is ahead of end (${buffered}) for ${time} ms.`; + hasReasonToJump = true; + } + } + } else { + this.aheadOfBufferSince = -1; + } + if (this.currentTimeNotChangedSince !== -1) { + const time = now - this.currentTimeNotChangedSince; + if (time > this.MAX_TIME_TO_RECOVER) { + // reasonToJump = `Current time not changed for ${time} ms.`; + hasReasonToJump = true; + } + } + if (!hasReasonToJump) { + return; + } + let waitingForSeekEnd = 0; + if (this.seekingSince !== -1) { + waitingForSeekEnd = now - this.seekingSince; + if (waitingForSeekEnd < 1500) { + return; + } + } + // console.info(`${reasonToJump} Jumping to the end. ${waitingForSeekEnd}`); + + const onSeekEnd = () => { + this.seekingSince = -1; + this.tag.removeEventListener('seeked', onSeekEnd); + this.tag.play(); + }; + if (this.seekingSince !== -1) { + console.warn(`[${this.name}]`, `Attempt to seek while already seeking! ${waitingForSeekEnd}`); + } + this.seekingSince = now; + this.tag.addEventListener('seeked', onSeekEnd); + this.tag.currentTime = this.tag.buffered.end(0); + } + } + + protected checkForIFrame(frame: Uint8Array): boolean { + if (!this.converter) { + return false; + } + this.sourceBuffer = this.converter.sourceBuffer; + if (BasePlayer.isIFrame(frame)) { + let start = 0; + let end = 0; + if (this.tag.buffered && this.tag.buffered.length) { + start = this.tag.buffered.start(0); + end = this.tag.buffered.end(0); + } + if (end !== 0 && start < end) { + const block: Block = { + start, + end, + }; + this.blocks.push(block); + if (this.blocks.length > 10) { + this.waitUntilSegmentRemoved = true; + + this.sourceBuffer.addEventListener('updateend', this.cleanSourceBuffer); + this.converter.appendRawData(frame); + return true; + } + } + if (this.sourceBuffer) { + this.sourceBuffer.onupdateend = this.checkVideoResize; + } + } + if (this.waitUntilSegmentRemoved) { + return false; + } + + this.converter.appendRawData(frame); + return true; + } + + private stopConverter(): void { + if (this.converter) { + this.converter.appendRawData(new Uint8Array([])); + this.converter.pause(); + delete this.converter; + } + } + + public getFitToScreenStatus(): boolean { + return MsePlayer.getFitToScreenStatus(this.udid, this.displayInfo); + } + + public loadVideoSettings(): VideoSettings { + return MsePlayer.loadVideoSettings(this.udid, this.displayInfo); + } +} diff --git a/src/app/player/MsePlayerForQVHack.ts b/src/app/player/MsePlayerForQVHack.ts new file mode 100644 index 0000000..9bfd9e0 --- /dev/null +++ b/src/app/player/MsePlayerForQVHack.ts @@ -0,0 +1,37 @@ +import { MsePlayer } from './MsePlayer'; +import Size from '../Size'; +import VideoSettings from '../VideoSettings'; +import { DisplayInfo } from '../DisplayInfo'; + +export class MsePlayerForQVHack extends MsePlayer { + public static readonly preferredVideoSettings: VideoSettings = new VideoSettings({ + lockedVideoOrientation: -1, + bitrate: 8000000, + maxFps: 30, + iFrameInterval: 10, + bounds: new Size(720, 720), + sendFrameMeta: false, + }); + + public readonly resizeVideoToBounds: boolean = true; + constructor( + udid: string, + displayInfo?: DisplayInfo, + name = 'MSE_Player_For_QVHack', + tag = MsePlayerForQVHack.createElement(), + ) { + super(udid, displayInfo, name, tag); + } + + protected needScreenInfoBeforePlay(): boolean { + return false; + } + + public getPreferredVideoSetting(): VideoSettings { + return MsePlayerForQVHack.preferredVideoSettings; + } + + public setVideoSettings(): void { + return; + } +} diff --git a/src/app/player/TinyH264Player.ts b/src/app/player/TinyH264Player.ts new file mode 100644 index 0000000..79fab94 --- /dev/null +++ b/src/app/player/TinyH264Player.ts @@ -0,0 +1,126 @@ +import { BaseCanvasBasedPlayer } from './BaseCanvasBasedPlayer'; +import TinyH264Worker from 'worker-loader!./../../../vendor/tinyh264/H264NALDecoder.worker'; +import VideoSettings from '../VideoSettings'; +import YUVWebGLCanvas from '../../../vendor/tinyh264/YUVWebGLCanvas'; +import YUVCanvas from '../../../vendor/tinyh264/YUVCanvas'; +import Size from '../Size'; +import { DisplayInfo } from '../DisplayInfo'; + +type WorkerMessage = { + type: string; + width: number; + height: number; + data: ArrayBuffer; + renderStateId: number; +}; + +export class TinyH264Player extends BaseCanvasBasedPlayer { + public static readonly storageKeyPrefix = 'Tinyh264Decoder'; + public static readonly playerFullName = 'Tiny H264'; + public static readonly playerCodeName = 'tinyh264'; + private static videoStreamId = 1; + public static readonly preferredVideoSettings: VideoSettings = new VideoSettings({ + lockedVideoOrientation: -1, + bitrate: 524288, + maxFps: 24, + iFrameInterval: 5, + bounds: new Size(480, 480), + sendFrameMeta: false, + }); + + private worker?: TinyH264Worker; + private isDecoderReady = false; + protected canvas?: YUVWebGLCanvas | YUVCanvas; + public readonly supportsScreenshot: boolean = true; + + public static isSupported(): boolean { + return typeof WebAssembly === 'object' && typeof WebAssembly.instantiate === 'function'; + } + + constructor(udid: string, displayInfo?: DisplayInfo, name = TinyH264Player.playerFullName) { + super(udid, displayInfo, name, TinyH264Player.storageKeyPrefix); + } + + private onWorkerMessage = (event: MessageEvent): void => { + const message: WorkerMessage = event.data; + switch (message.type) { + case 'pictureReady': + const { width, height, data } = message; + this.onFrameDecoded(width, height, new Uint8Array(data)); + break; + case 'decoderReady': + this.isDecoderReady = true; + break; + default: + console.error(`[${this.name}]`, Error(`Wrong message type "${message.type}"`)); + } + }; + + private initWorker(): void { + this.worker = new TinyH264Worker(); + this.worker.addEventListener('message', this.onWorkerMessage); + } + + protected initCanvas(width: number, height: number): void { + super.initCanvas(width, height); + + if (BaseCanvasBasedPlayer.hasWebGLSupport()) { + this.canvas = new YUVWebGLCanvas(this.tag); + } else { + this.canvas = new YUVCanvas(this.tag); + } + } + + protected decode(data: Uint8Array): void { + if (!this.worker || !this.isDecoderReady) { + return; + } + + this.worker.postMessage( + { + type: 'decode', + data: data.buffer, + offset: data.byteOffset, + length: data.byteLength, + renderStateId: TinyH264Player.videoStreamId, + }, + [data.buffer], + ); + } + + public play(): void { + super.play(); + if (!this.worker) { + this.initWorker(); + } + } + + public stop(): void { + super.stop(); + if (this.worker) { + this.worker.removeEventListener('message', this.onWorkerMessage); + this.worker.postMessage({ type: 'release', renderStateId: TinyH264Player.videoStreamId }); + delete this.worker; + } + } + + public getPreferredVideoSetting(): VideoSettings { + return TinyH264Player.preferredVideoSettings; + } + + protected clearState(): void { + super.clearState(); + if (this.worker) { + this.worker.postMessage({ type: 'release', renderStateId: TinyH264Player.videoStreamId }); + TinyH264Player.videoStreamId++; + } + } + + public getFitToScreenStatus(): boolean { + return TinyH264Player.getFitToScreenStatus(this.udid, this.displayInfo); + } + + public loadVideoSettings(): VideoSettings { + return TinyH264Player.loadVideoSettings(this.udid, this.displayInfo); + } +} diff --git a/src/app/player/WebCodecsPlayer.ts b/src/app/player/WebCodecsPlayer.ts new file mode 100644 index 0000000..6e942cf --- /dev/null +++ b/src/app/player/WebCodecsPlayer.ts @@ -0,0 +1,223 @@ +import { BaseCanvasBasedPlayer } from './BaseCanvasBasedPlayer'; +import VideoSettings from '../VideoSettings'; +import Size from '../Size'; +import { DisplayInfo } from '../DisplayInfo'; +import H264Parser from 'h264-converter/dist/h264-parser'; +import NALU from 'h264-converter/dist/util/NALU'; +import ScreenInfo from '../ScreenInfo'; +import Rect from '../Rect'; + +type ParametersSubSet = { + codec: string; + width: number; + height: number; +}; + +function toHex(value: number) { + return value.toString(16).padStart(2, '0').toUpperCase(); +} + +export class WebCodecsPlayer extends BaseCanvasBasedPlayer { + public static readonly storageKeyPrefix = 'WebCodecsPlayer'; + public static readonly playerFullName = 'WebCodecs'; + public static readonly playerCodeName = 'webcodecs'; + + public static readonly preferredVideoSettings: VideoSettings = new VideoSettings({ + lockedVideoOrientation: -1, + bitrate: 524288, + maxFps: 24, + iFrameInterval: 5, + bounds: new Size(480, 480), + sendFrameMeta: false, + }); + + public static isSupported(): boolean { + if (typeof VideoDecoder !== 'function' || typeof VideoDecoder.isConfigSupported !== 'function') { + return false; + } + + // FIXME: verify support + // const result = await VideoDecoder.isConfigSupported(); + return true; + } + + private static parseSPS(data: Uint8Array): ParametersSubSet { + const { + profile_idc, + constraint_set_flags, + level_idc, + pic_width_in_mbs_minus1, + frame_crop_left_offset, + frame_crop_right_offset, + frame_mbs_only_flag, + pic_height_in_map_units_minus1, + frame_crop_top_offset, + frame_crop_bottom_offset, + sar, + } = H264Parser.parseSPS(data); + + const sarScale = sar[0] / sar[1]; + const codec = `avc1.${[profile_idc, constraint_set_flags, level_idc].map(toHex).join('')}`; + const width = Math.ceil( + ((pic_width_in_mbs_minus1 + 1) * 16 - frame_crop_left_offset * 2 - frame_crop_right_offset * 2) * sarScale, + ); + const height = + (2 - frame_mbs_only_flag) * (pic_height_in_map_units_minus1 + 1) * 16 - + (frame_mbs_only_flag ? 2 : 4) * (frame_crop_top_offset + frame_crop_bottom_offset); + return { codec, width, height }; + } + + public readonly supportsScreenshot = true; + private context: CanvasRenderingContext2D; + private decoder: VideoDecoder; + private buffer: ArrayBuffer | undefined; + private hadIDR = false; + private bufferedSPS = false; + private bufferedPPS = false; + + constructor(udid: string, displayInfo?: DisplayInfo, name = WebCodecsPlayer.playerFullName) { + super(udid, displayInfo, name, WebCodecsPlayer.storageKeyPrefix); + const context = this.tag.getContext('2d'); + if (!context) { + throw Error('Failed to get 2d context from canvas'); + } + this.context = context; + this.decoder = this.createDecoder(); + } + + private createDecoder(): VideoDecoder { + return new VideoDecoder({ + output: (frame) => { + this.onFrameDecoded(0, 0, frame); + }, + error: (error: DOMException) => { + console.error(error, `code: ${error.code}`); + this.stop(); + }, + }); + } + + protected addToBuffer(data: Uint8Array): Uint8Array { + let array: Uint8Array; + if (this.buffer) { + array = new Uint8Array(this.buffer.byteLength + data.byteLength); + array.set(new Uint8Array(this.buffer)); + array.set(new Uint8Array(data), this.buffer.byteLength); + } else { + array = data; + } + this.buffer = array.buffer; + return array; + } + + protected scaleCanvas(width: number, height: number): void { + const videoSize = new Size(width, height); + let scale = 1; + if (this.bounds && !this.bounds.intersect(videoSize).equals(videoSize)) { + scale = Math.min(this.bounds.w / width, this.bounds.h / height); + } + const w = width * scale; + const h = height * scale; + const screenInfo = new ScreenInfo(new Rect(0, 0, width, height), new Size(w, h), 0); + this.emit('input-video-resize', screenInfo); + this.setScreenInfo(screenInfo); + + // FIXME: canvas was initialized from `.setScreenInfo()` call above, but with wrong values + this.initCanvas(width, height); + if (scale !== 1) { + this.tag.style.transform = `scale(${scale.toFixed(4)})`; + } else { + this.tag.style.transform = ``; + } + this.tag.style.transformOrigin = 'top left'; + } + + protected decode(data: Uint8Array): void { + if (!data || data.length < 4) { + return; + } + const type = data[4] & 31; + const isIDR = type === NALU.IDR; + + if (type === NALU.SPS) { + const { codec, width, height } = WebCodecsPlayer.parseSPS(data.subarray(4)); + this.scaleCanvas(width, height); + const config: VideoDecoderConfig = { + codec, + optimizeForLatency: true, + } as VideoDecoderConfig; + this.decoder.configure(config); + this.bufferedSPS = true; + this.addToBuffer(data); + this.hadIDR = false; + return; + } else if (type === NALU.PPS) { + this.bufferedPPS = true; + this.addToBuffer(data); + return; + } else if (type === NALU.SEI) { + // Workaround for lonely SEI from ws-qvh + if (!this.bufferedSPS || !this.bufferedPPS) { + return; + } + } + const array = this.addToBuffer(data); + this.hadIDR = this.hadIDR || isIDR; + if (array && this.decoder.state === 'configured' && this.hadIDR) { + this.buffer = undefined; + this.bufferedPPS = false; + this.bufferedSPS = false; + this.decoder.decode( + new EncodedVideoChunk({ + type: 'key', + timestamp: 0, + data: array.buffer, + }), + ); + return; + } + } + + protected drawDecoded = (): void => { + if (this.receivedFirstFrame) { + const data = this.decodedFrames.shift(); + if (data) { + const frame: VideoFrame = data.frame; + this.context.drawImage(frame, 0, 0); + frame.close(); + } + } + if (this.decodedFrames.length) { + this.animationFrameId = requestAnimationFrame(this.drawDecoded); + } else { + this.animationFrameId = undefined; + } + }; + + protected dropFrame(frame: VideoFrame): void { + frame.close(); + } + + public getFitToScreenStatus(): boolean { + return false; + } + + public getPreferredVideoSetting(): VideoSettings { + return WebCodecsPlayer.preferredVideoSettings; + } + + public loadVideoSettings(): VideoSettings { + return WebCodecsPlayer.loadVideoSettings(this.udid, this.displayInfo); + } + + protected needScreenInfoBeforePlay(): boolean { + return false; + } + + public stop(): void { + super.stop(); + if (this.decoder.state === 'configured') { + this.decoder.close(); + } + } +} diff --git a/src/app/toolbox/ToolBox.ts b/src/app/toolbox/ToolBox.ts new file mode 100644 index 0000000..1749832 --- /dev/null +++ b/src/app/toolbox/ToolBox.ts @@ -0,0 +1,19 @@ +import { ToolBoxElement } from './ToolBoxElement'; + +export class ToolBox { + private readonly holder: HTMLElement; + + constructor(list: ToolBoxElement[]) { + this.holder = document.createElement('div'); + this.holder.classList.add('control-buttons-list', 'control-wrapper'); + list.forEach((item) => { + item.getAllElements().forEach((el) => { + this.holder.appendChild(el); + }); + }); + } + + public getHolderElement(): HTMLElement { + return this.holder; + } +} diff --git a/src/app/toolbox/ToolBoxButton.ts b/src/app/toolbox/ToolBoxButton.ts new file mode 100644 index 0000000..f8b6cd4 --- /dev/null +++ b/src/app/toolbox/ToolBoxButton.ts @@ -0,0 +1,21 @@ +import { Optional, ToolBoxElement } from './ToolBoxElement'; +import SvgImage, { Icon } from '../ui/SvgImage'; + +export class ToolBoxButton extends ToolBoxElement { + private readonly btn: HTMLButtonElement; + constructor(title: string, icon: Icon, optional?: Optional) { + super(title, optional); + const btn = document.createElement('button'); + btn.classList.add('control-button'); + btn.title = title; + btn.appendChild(SvgImage.create(icon)); + this.btn = btn; + } + + public getElement(): HTMLButtonElement { + return this.btn; + } + public getAllElements(): HTMLElement[] { + return [this.btn]; + } +} diff --git a/src/app/toolbox/ToolBoxCheckbox.ts b/src/app/toolbox/ToolBoxCheckbox.ts new file mode 100644 index 0000000..59acd2e --- /dev/null +++ b/src/app/toolbox/ToolBoxCheckbox.ts @@ -0,0 +1,51 @@ +import { Optional, ToolBoxElement } from './ToolBoxElement'; +import SvgImage, { Icon } from '../ui/SvgImage'; + +type Icons = { + on?: Icon; + off: Icon; +}; + +export class ToolBoxCheckbox extends ToolBoxElement { + private readonly input: HTMLInputElement; + private readonly label: HTMLLabelElement; + private readonly imageOn?: Element; + private readonly imageOff: Element; + constructor(title: string, icons: Icons | Icon, opt_id?: string, optional?: Optional) { + super(title, optional); + const input = document.createElement('input'); + input.type = 'checkbox'; + const label = document.createElement('label'); + label.title = title; + label.classList.add('control-button'); + let iconOff: Icon; + let iconOn: Icon | undefined; + if (typeof icons !== 'number') { + iconOff = icons.off; + iconOn = icons.on; + } else { + iconOff = icons; + } + this.imageOff = SvgImage.create(iconOff); + this.imageOff.classList.add('image', 'image-off'); + label.appendChild(this.imageOff); + if (iconOn) { + this.imageOn = SvgImage.create(iconOn); + this.imageOn.classList.add('image', 'image-on'); + label.appendChild(this.imageOn); + input.classList.add('two-images'); + } + const id = opt_id || title.toLowerCase().replace(' ', '_'); + label.htmlFor = input.id = `input_${id}`; + this.input = input; + this.label = label; + } + + public getElement(): HTMLInputElement { + return this.input; + } + + public getAllElements(): HTMLElement[] { + return [this.input, this.label]; + } +} diff --git a/src/app/toolbox/ToolBoxElement.ts b/src/app/toolbox/ToolBoxElement.ts new file mode 100644 index 0000000..30104dd --- /dev/null +++ b/src/app/toolbox/ToolBoxElement.ts @@ -0,0 +1,53 @@ +export type Optional = { + [index: string]: any; +}; + +// type Listener = (type: K, el: ToolBoxElement) => any; + +export abstract class ToolBoxElement { + private listeners: Map(type: K, el: ToolBoxElement) => any>> = + new Map(); + protected constructor(public readonly title: string, public readonly optional?: Optional) {} + + public abstract getElement(): T; + public abstract getAllElements(): HTMLElement[]; + + public addEventListener( + type: K, + listener: (type: K, el: ToolBoxElement) => any, + options?: boolean | AddEventListenerOptions, + ): void { + const set = this.listeners.get(type) || new Set(); + if (!set.size) { + const element = this.getElement(); + element.addEventListener(type, this.onEvent, options); + } + set.add(listener); + this.listeners.set(type, set); + } + public removeEventListener( + type: K, + listener: (type: K, el: ToolBoxElement) => any, + ): void { + const set = this.listeners.get(type); + if (!set) { + return; + } + set.delete(listener); + if (!set.size) { + this.listeners.delete(type); + const element = this.getElement(); + element.removeEventListener(type, this.onEvent); + } + } + onEvent = (ev: HTMLElementEventMap[K]): void => { + const set = this.listeners.get(ev.type); + if (!set) { + return; + } + const type = ev.type as K; + set.forEach((listener) => { + listener(type, this); + }); + }; +} diff --git a/src/app/ui/HtmlTag.ts b/src/app/ui/HtmlTag.ts new file mode 100644 index 0000000..ca2ff36 --- /dev/null +++ b/src/app/ui/HtmlTag.ts @@ -0,0 +1,23 @@ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type Value = any; + +function htmlValue(value: Value): string { + if (value instanceof HTMLTemplateElement) { + return value.innerHTML; + } + if (typeof value === 'undefined') { + return 'undefined'; + } + if (value === null) { + return 'null'; + } + const e = document.createElement('dummy'); + e.innerText = value.toString(); + return e.innerHTML; +} + +export const html = function html(strings: TemplateStringsArray, ...values: ReadonlyArray): HTMLTemplateElement { + const template = document.createElement('template') as HTMLTemplateElement; + template.innerHTML = values.reduce((acc, v, idx) => acc + htmlValue(v) + strings[idx + 1], strings[0]).toString(); + return template; +}; diff --git a/src/app/ui/SvgImage.ts b/src/app/ui/SvgImage.ts new file mode 100644 index 0000000..392470e --- /dev/null +++ b/src/app/ui/SvgImage.ts @@ -0,0 +1,91 @@ +import KeyboardSVG from '../../public/images/skin-light/ic_keyboard_678_48dp.svg'; +import MoreSVG from '../../public/images/skin-light/ic_more_horiz_678_48dp.svg'; +import CameraSVG from '../../public/images/skin-light/ic_photo_camera_678_48dp.svg'; +import PowerSVG from '../../public/images/skin-light/ic_power_settings_new_678_48px.svg'; +import VolumeDownSVG from '../../public/images/skin-light/ic_volume_down_678_48px.svg'; +import VolumeUpSVG from '../../public/images/skin-light/ic_volume_up_678_48px.svg'; +import BackSVG from '../../public/images/skin-light/System_Back_678.svg'; +import HomeSVG from '../../public/images/skin-light/System_Home_678.svg'; +import OverviewSVG from '../../public/images/skin-light/System_Overview_678.svg'; +import CancelSVG from '../../public/images/buttons/cancel.svg'; +import OfflineSVG from '../../public/images/buttons/offline.svg'; +import RefreshSVG from '../../public/images/buttons/refresh.svg'; +import SettingsSVG from '../../public/images/buttons/settings.svg'; +import MenuSVG from '../../public/images/buttons/menu.svg'; +import ArrowBackSVG from '../../public/images/buttons/arrow_back.svg'; +import ToggleOnSVG from '../../public/images/buttons/toggle_on.svg'; +import ToggleOffSVG from '../../public/images/buttons/toggle_off.svg'; + +export enum Icon { + BACK, + HOME, + OVERVIEW, + POWER, + VOLUME_UP, + VOLUME_DOWN, + MORE, + CAMERA, + KEYBOARD, + CANCEL, + OFFLINE, + REFRESH, + SETTINGS, + MENU, + ARROW_BACK, + TOGGLE_ON, + TOGGLE_OFF, +} + +export default class SvgImage { + static Icon = Icon; + private static getSvgString(type: Icon): string { + switch (type) { + case Icon.KEYBOARD: + return KeyboardSVG; + case Icon.MORE: + return MoreSVG; + case Icon.CAMERA: + return CameraSVG; + case Icon.POWER: + return PowerSVG; + case Icon.VOLUME_DOWN: + return VolumeDownSVG; + case Icon.VOLUME_UP: + return VolumeUpSVG; + case Icon.BACK: + return BackSVG; + case Icon.HOME: + return HomeSVG; + case Icon.OVERVIEW: + return OverviewSVG; + case Icon.CANCEL: + return CancelSVG; + case Icon.OFFLINE: + return OfflineSVG; + case Icon.REFRESH: + return RefreshSVG; + case Icon.SETTINGS: + return SettingsSVG; + case Icon.MENU: + return MenuSVG; + case Icon.ARROW_BACK: + return ArrowBackSVG; + case Icon.TOGGLE_ON: + return ToggleOnSVG; + case Icon.TOGGLE_OFF: + return ToggleOffSVG; + default: + return ''; + } + } + public static create(type: Icon): Element { + const dummy = document.createElement('div'); + dummy.innerHTML = this.getSvgString(type); + const svg = dummy.children[0]; + const titles = svg.getElementsByTagName('title'); + for (let i = 0, l = titles.length; i < l; i++) { + svg.removeChild(titles[i]); + } + return svg; + } +} diff --git a/src/common/Action.ts b/src/common/Action.ts new file mode 100644 index 0000000..150091a --- /dev/null +++ b/src/common/Action.ts @@ -0,0 +1,15 @@ +export enum ACTION { + LIST_HOSTS = 'list-hosts', + APPL_DEVICE_LIST = 'appl-device-list', + GOOG_DEVICE_LIST = 'goog-device-list', + MULTIPLEX = 'multiplex', + SHELL = 'shell', + PROXY_WS = 'proxy-ws', + PROXY_ADB = 'proxy-adb', + DEVTOOLS = 'devtools', + STREAM_SCRCPY = 'stream', + STREAM_WS_QVH = 'stream-qvh', + STREAM_MJPEG = 'stream-mjpeg', + PROXY_WDA = 'proxy-wda', + FILE_LISTING = 'list-files', +} diff --git a/src/common/ChannelCode.ts b/src/common/ChannelCode.ts new file mode 100644 index 0000000..3d60f01 --- /dev/null +++ b/src/common/ChannelCode.ts @@ -0,0 +1,9 @@ +export enum ChannelCode { + FSLS = 'FSLS', // File System LiSt + HSTS = 'HSTS', // HoSTS List + SHEL = 'SHEL', // SHELl + GTRC = 'GTRC', // Goog device TRaCer + ATRC = 'ATRC', // Appl device TRaCer + WDAP = 'WDAP', // WebDriverAgent Proxy + QVHS = 'QVHS', // Quicktime_Video_Hack Stream +} diff --git a/src/common/Constants.ts b/src/common/Constants.ts new file mode 100644 index 0000000..d804b3f --- /dev/null +++ b/src/common/Constants.ts @@ -0,0 +1,20 @@ +export const SERVER_PACKAGE = 'com.genymobile.scrcpy.Server'; +export const SERVER_PORT = 8886; +export const SERVER_VERSION = '1.19-ws6'; + +export const SERVER_TYPE = 'web'; + +export const LOG_LEVEL = 'ERROR'; + +let SCRCPY_LISTENS_ON_ALL_INTERFACES; +/// #if SCRCPY_LISTENS_ON_ALL_INTERFACES +SCRCPY_LISTENS_ON_ALL_INTERFACES = true; +/// #else +SCRCPY_LISTENS_ON_ALL_INTERFACES = false; +/// #endif + +const ARGUMENTS = [SERVER_VERSION, SERVER_TYPE, LOG_LEVEL, SERVER_PORT, SCRCPY_LISTENS_ON_ALL_INTERFACES]; + +export const SERVER_PROCESS_NAME = 'app_process'; + +export const ARGS_STRING = `/ ${SERVER_PACKAGE} ${ARGUMENTS.join(' ')} 2>&1 > /dev/null`; diff --git a/src/common/ControlCenterCommand.ts b/src/common/ControlCenterCommand.ts new file mode 100644 index 0000000..8fbf7a5 --- /dev/null +++ b/src/common/ControlCenterCommand.ts @@ -0,0 +1,77 @@ +import { WDAMethod } from './WDAMethod'; + +export class ControlCenterCommand { + public static KILL_SERVER = 'kill_server'; + public static START_SERVER = 'start_server'; + public static UPDATE_INTERFACES = 'update_interfaces'; + public static CONFIGURE_STREAM = 'configure_stream'; + public static RUN_WDA = 'run-wda'; + public static REQUEST_WDA = 'request-wda'; + + private id = -1; + private type = ''; + private pid = 0; + private udid = ''; + private method = ''; + private args?: any; + private data?: any; + + public static fromJSON(json: string): ControlCenterCommand { + const body = JSON.parse(json); + if (!body) { + throw new Error('Invalid input'); + } + const command = new ControlCenterCommand(); + const data = (command.data = body.data); + command.id = body.id; + command.type = body.type; + + if (typeof data.udid === 'string') { + command.udid = data.udid; + } + switch (body.type) { + case this.KILL_SERVER: + if (typeof data.pid !== 'number' && data.pid <= 0) { + throw new Error('Invalid "pid" value'); + } + command.pid = data.pid; + return command; + case this.REQUEST_WDA: + if (typeof data.method !== 'string') { + throw new Error('Invalid "method" value'); + } + command.method = data.method; + command.args = data.args; + return command; + case this.START_SERVER: + case this.UPDATE_INTERFACES: + case this.CONFIGURE_STREAM: + case this.RUN_WDA: + return command; + default: + throw new Error(`Unknown command "${body.command}"`); + } + } + + public getType(): string { + return this.type; + } + public getPid(): number { + return this.pid; + } + public getUdid(): string { + return this.udid; + } + public getId(): number { + return this.id; + } + public getMethod(): WDAMethod | string { + return this.method; + } + public getData(): any { + return this.data; + } + public getArgs(): any { + return this.args; + } +} diff --git a/src/common/DeviceState.ts b/src/common/DeviceState.ts new file mode 100644 index 0000000..f501b44 --- /dev/null +++ b/src/common/DeviceState.ts @@ -0,0 +1,6 @@ +export enum DeviceState { + DEVICE = 'device', + DISCONNECTED = 'disconnected', + + CONNECTED = 'Connected', +} diff --git a/src/common/HostTrackerMessage.ts b/src/common/HostTrackerMessage.ts new file mode 100644 index 0000000..dde6ccf --- /dev/null +++ b/src/common/HostTrackerMessage.ts @@ -0,0 +1,20 @@ +import { Message } from '../types/Message'; +import { HostItem } from '../types/Configuration'; + +export enum MessageType { + HOSTS = 'hosts', + ERROR = 'error', +} + +export interface MessageHosts extends Message { + type: 'hosts'; + data: { + local?: { type: string }[]; + remote?: HostItem[]; + }; +} + +export interface MessageError extends Message { + type: 'error'; + data: string; +} diff --git a/src/common/ProductType.ts b/src/common/ProductType.ts new file mode 100644 index 0000000..ab46888 --- /dev/null +++ b/src/common/ProductType.ts @@ -0,0 +1,147 @@ +export class ProductType { + // from https://gist.github.com/adamawolf/3048717 + private static type: Record = { + i386: 'iPhone Simulator', + x86_64: 'iPhone Simulator', + arm64: 'iPhone Simulator', + 'iPhone1,1': 'iPhone', + 'iPhone1,2': 'iPhone 3G', + 'iPhone2,1': 'iPhone 3GS', + 'iPhone3,1': 'iPhone 4', + 'iPhone3,2': 'iPhone 4 GSM Rev A', + 'iPhone3,3': 'iPhone 4 CDMA', + 'iPhone4,1': 'iPhone 4S', + 'iPhone5,1': 'iPhone 5 (GSM)', + 'iPhone5,2': 'iPhone 5 (GSM+CDMA)', + 'iPhone5,3': 'iPhone 5C (GSM)', + 'iPhone5,4': 'iPhone 5C (Global)', + 'iPhone6,1': 'iPhone 5S (GSM)', + 'iPhone6,2': 'iPhone 5S (Global)', + 'iPhone7,1': 'iPhone 6 Plus', + 'iPhone7,2': 'iPhone 6', + 'iPhone8,1': 'iPhone 6s', + 'iPhone8,2': 'iPhone 6s Plus', + 'iPhone8,4': 'iPhone SE (GSM)', + 'iPhone9,1': 'iPhone 7', + 'iPhone9,2': 'iPhone 7 Plus', + 'iPhone9,3': 'iPhone 7', + 'iPhone9,4': 'iPhone 7 Plus', + 'iPhone10,1': 'iPhone 8', + 'iPhone10,2': 'iPhone 8 Plus', + 'iPhone10,3': 'iPhone X Global', + 'iPhone10,4': 'iPhone 8', + 'iPhone10,5': 'iPhone 8 Plus', + 'iPhone10,6': 'iPhone X GSM', + 'iPhone11,2': 'iPhone XS', + 'iPhone11,4': 'iPhone XS Max', + 'iPhone11,6': 'iPhone XS Max Global', + 'iPhone11,8': 'iPhone XR', + 'iPhone12,1': 'iPhone 11', + 'iPhone12,3': 'iPhone 11 Pro', + 'iPhone12,5': 'iPhone 11 Pro Max', + 'iPhone12,8': 'iPhone SE 2nd Gen', + 'iPhone13,1': 'iPhone 12 Mini', + 'iPhone13,2': 'iPhone 12', + 'iPhone13,3': 'iPhone 12 Pro', + 'iPhone13,4': 'iPhone 12 Pro Max', + 'iPod1,1': '1st Gen iPod', + 'iPod2,1': '2nd Gen iPod', + 'iPod3,1': '3rd Gen iPod', + 'iPod4,1': '4th Gen iPod', + 'iPod5,1': '5th Gen iPod', + 'iPod7,1': '6th Gen iPod', + 'iPod9,1': '7th Gen iPod', + 'iPad1,1': 'iPad', + 'iPad1,2': 'iPad 3G', + 'iPad2,1': '2nd Gen iPad', + 'iPad2,2': '2nd Gen iPad GSM', + 'iPad2,3': '2nd Gen iPad CDMA', + 'iPad2,4': '2nd Gen iPad New Revision', + 'iPad3,1': '3rd Gen iPad', + 'iPad3,2': '3rd Gen iPad CDMA', + 'iPad3,3': '3rd Gen iPad GSM', + 'iPad2,5': 'iPad mini', + 'iPad2,6': 'iPad mini GSM+LTE', + 'iPad2,7': 'iPad mini CDMA+LTE', + 'iPad3,4': '4th Gen iPad', + 'iPad3,5': '4th Gen iPad GSM+LTE', + 'iPad3,6': '4th Gen iPad CDMA+LTE', + 'iPad4,1': 'iPad Air (WiFi)', + 'iPad4,2': 'iPad Air (GSM+CDMA)', + 'iPad4,3': '1st Gen iPad Air (China)', + 'iPad4,4': 'iPad mini Retina (WiFi)', + 'iPad4,5': 'iPad mini Retina (GSM+CDMA)', + 'iPad4,6': 'iPad mini Retina (China)', + 'iPad4,7': 'iPad mini 3 (WiFi)', + 'iPad4,8': 'iPad mini 3 (GSM+CDMA)', + 'iPad4,9': 'iPad Mini 3 (China)', + 'iPad5,1': 'iPad mini 4 (WiFi)', + 'iPad5,2': '4th Gen iPad mini (WiFi+Cellular)', + 'iPad5,3': 'iPad Air 2 (WiFi)', + 'iPad5,4': 'iPad Air 2 (Cellular)', + 'iPad6,3': 'iPad Pro (9.7 inch, WiFi)', + 'iPad6,4': 'iPad Pro (9.7 inch, WiFi+LTE)', + 'iPad6,7': 'iPad Pro (12.9 inch, WiFi)', + 'iPad6,8': 'iPad Pro (12.9 inch, WiFi+LTE)', + 'iPad6,11': 'iPad (2017)', + 'iPad6,12': 'iPad (2017)', + 'iPad7,1': 'iPad Pro 2nd Gen (WiFi)', + 'iPad7,2': 'iPad Pro 2nd Gen (WiFi+Cellular)', + 'iPad7,3': 'iPad Pro 10.5-inch', + 'iPad7,4': 'iPad Pro 10.5-inch', + 'iPad7,5': 'iPad 6th Gen (WiFi)', + 'iPad7,6': 'iPad 6th Gen (WiFi+Cellular)', + 'iPad7,11': 'iPad 7th Gen 10.2-inch (WiFi)', + 'iPad7,12': 'iPad 7th Gen 10.2-inch (WiFi+Cellular)', + 'iPad8,1': 'iPad Pro 11 inch 3rd Gen (WiFi)', + 'iPad8,2': 'iPad Pro 11 inch 3rd Gen (1TB, WiFi)', + 'iPad8,3': 'iPad Pro 11 inch 3rd Gen (WiFi+Cellular)', + 'iPad8,4': 'iPad Pro 11 inch 3rd Gen (1TB, WiFi+Cellular)', + 'iPad8,5': 'iPad Pro 12.9 inch 3rd Gen (WiFi)', + 'iPad8,6': 'iPad Pro 12.9 inch 3rd Gen (1TB, WiFi)', + 'iPad8,7': 'iPad Pro 12.9 inch 3rd Gen (WiFi+Cellular)', + 'iPad8,8': 'iPad Pro 12.9 inch 3rd Gen (1TB, WiFi+Cellular)', + 'iPad8,9': 'iPad Pro 11 inch 4th Gen (WiFi)', + 'iPad8,10': 'iPad Pro 11 inch 4th Gen (WiFi+Cellular)', + 'iPad8,11': 'iPad Pro 12.9 inch 4th Gen (WiFi)', + 'iPad8,12': 'iPad Pro 12.9 inch 4th Gen (WiFi+Cellular)', + 'iPad11,1': 'iPad mini 5th Gen (WiFi)', + 'iPad11,2': 'iPad mini 5th Gen', + 'iPad11,3': 'iPad Air 3rd Gen (WiFi)', + 'iPad11,4': 'iPad Air 3rd Gen', + 'iPad11,6': 'iPad 8th Gen (WiFi)', + 'iPad11,7': 'iPad 8th Gen (WiFi+Cellular)', + 'iPad13,1': 'iPad air 4th Gen (WiFi)', + 'iPad13,2': 'iPad air 4th Gen (WiFi+Cellular)', + 'Watch1,1': 'Apple Watch 38mm case', + 'Watch1,2': 'Apple Watch 42mm case', + 'Watch2,6': 'Apple Watch Series 1 38mm case', + 'Watch2,7': 'Apple Watch Series 1 42mm case', + 'Watch2,3': 'Apple Watch Series 2 38mm case', + 'Watch2,4': 'Apple Watch Series 2 42mm case', + 'Watch3,1': 'Apple Watch Series 3 38mm case (GPS+Cellular)', + 'Watch3,2': 'Apple Watch Series 3 42mm case (GPS+Cellular)', + 'Watch3,3': 'Apple Watch Series 3 38mm case (GPS)', + 'Watch3,4': 'Apple Watch Series 3 42mm case (GPS)', + 'Watch4,1': 'Apple Watch Series 4 40mm case (GPS)', + 'Watch4,2': 'Apple Watch Series 4 44mm case (GPS)', + 'Watch4,3': 'Apple Watch Series 4 40mm case (GPS+Cellular)', + 'Watch4,4': 'Apple Watch Series 4 44mm case (GPS+Cellular)', + 'Watch5,1': 'Apple Watch Series 5 40mm case (GPS)', + 'Watch5,2': 'Apple Watch Series 5 44mm case (GPS)', + 'Watch5,3': 'Apple Watch Series 5 40mm case (GPS+Cellular)', + 'Watch5,4': 'Apple Watch Series 5 44mm case (GPS+Cellular)', + 'Watch5,9': 'Apple Watch SE 40mm case (GPS)', + 'Watch5,10': 'Apple Watch SE 44mm case (GPS)', + 'Watch5,11': 'Apple Watch SE 40mm case (GPS+Cellular)', + 'Watch5,12': 'Apple Watch SE 44mm case (GPS+Cellular)', + 'Watch6,1': 'Apple Watch Series 6 40mm case (GPS)', + 'Watch6,2': 'Apple Watch Series 6 44mm case (GPS)', + 'Watch6,3': 'Apple Watch Series 6 40mm case (GPS+Cellular)', + 'Watch6,4': 'Apple Watch Series 6 44mm case (GPS+Cellular)', + }; + + public static getModel(productType: string): string { + return this.type[productType] || productType; + } +} diff --git a/src/common/TypedEmitter.ts b/src/common/TypedEmitter.ts new file mode 100644 index 0000000..3895fd7 --- /dev/null +++ b/src/common/TypedEmitter.ts @@ -0,0 +1,43 @@ +import { EventEmitter } from 'events'; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type EventMap = Record; +export type EventKey = string & keyof T; +export type EventReceiver = (params: T) => void; + +interface Emitter { + on>(eventName: K, fn: EventReceiver): void; + off>(eventName: K, fn: EventReceiver): void; + emit>(eventName: K, params: T[K]): void; +} + +export class TypedEmitter implements Emitter { + private emitter = new EventEmitter(); + addEventListener>(eventName: K, fn: EventReceiver): void { + this.emitter.on(eventName, fn); + } + + removeEventListener>(eventName: K, fn: EventReceiver): void { + this.emitter.off(eventName, fn); + } + + dispatchEvent(event: Event): boolean { + return this.emitter.emit(event.type, event); + } + + on>(eventName: K, fn: EventReceiver): void { + this.emitter.on(eventName, fn); + } + + once>(eventName: K, fn: EventReceiver): void { + this.emitter.once(eventName, fn); + } + + off>(eventName: K, fn: EventReceiver): void { + this.emitter.off(eventName, fn); + } + + emit>(eventName: K, params: T[K]): boolean { + return this.emitter.emit(eventName, params); + } +} diff --git a/src/common/WDAMethod.ts b/src/common/WDAMethod.ts new file mode 100644 index 0000000..caf2bd5 --- /dev/null +++ b/src/common/WDAMethod.ts @@ -0,0 +1,8 @@ +export enum WDAMethod { + CLICK = 'CLICK', + SCROLL = 'SCROLL', + PRESS_BUTTON = 'PRESS_BUTTON', + GET_SCREEN_WIDTH = 'GET_SCREEN_WIDTH', + APPIUM_SETTINGS = 'APPIUM_SETTINGS', + SEND_KEYS = 'SEND_KEYS', +} diff --git a/src/common/WdaStatus.ts b/src/common/WdaStatus.ts new file mode 100644 index 0000000..3089b9e --- /dev/null +++ b/src/common/WdaStatus.ts @@ -0,0 +1,5 @@ +export enum WdaStatus { + STARTING = 'STARTING', + STARTED = 'STARTED', + STOPPED = 'STOPPED', +} diff --git a/src/packages/multiplexer/CloseEventClass.ts b/src/packages/multiplexer/CloseEventClass.ts new file mode 100644 index 0000000..8932977 --- /dev/null +++ b/src/packages/multiplexer/CloseEventClass.ts @@ -0,0 +1,15 @@ +import { Event2 } from './Event'; + +export class CloseEvent2 extends Event2 implements CloseEvent { + readonly code: number; + readonly reason: string; + readonly wasClean: boolean; + constructor(type: string, { code, reason }: CloseEventInit = {}) { + super(type); + this.code = code || 0; + this.reason = reason || ''; + this.wasClean = this.code === 0; + } +} + +export const CloseEventClass = typeof CloseEvent !== 'undefined' ? CloseEvent : CloseEvent2; diff --git a/src/packages/multiplexer/ErrorEventClass.ts b/src/packages/multiplexer/ErrorEventClass.ts new file mode 100644 index 0000000..fd364b2 --- /dev/null +++ b/src/packages/multiplexer/ErrorEventClass.ts @@ -0,0 +1,20 @@ +import { Event2 } from './Event'; + +export class ErrorEvent2 extends Event2 implements ErrorEvent { + readonly colno: number; + readonly error: any; + readonly filename: string; + readonly lineno: number; + readonly message: string; + + constructor(type: string, { colno, error, filename, lineno, message }: ErrorEventInit = {}) { + super(type); + this.error = error; + this.colno = colno || 0; + this.filename = filename || ''; + this.lineno = lineno || 0; + this.message = message || ''; + } +} + +export const ErrorEventClass = typeof ErrorEvent !== 'undefined' ? ErrorEvent : ErrorEvent2; diff --git a/src/packages/multiplexer/Event.ts b/src/packages/multiplexer/Event.ts new file mode 100644 index 0000000..63eda41 --- /dev/null +++ b/src/packages/multiplexer/Event.ts @@ -0,0 +1,78 @@ +export class Event2 { + static NONE = 0; + static CAPTURING_PHASE = 1; + static AT_TARGET = 2; + static BUBBLING_PHASE = 3; + + public cancelable: boolean; + public bubbles: boolean; + public composed: boolean; + public type: string; + public defaultPrevented: boolean; + public timeStamp: number; + public target: any; + public readonly isTrusted: boolean = true; + readonly AT_TARGET: number = 0; + readonly BUBBLING_PHASE: number = 0; + readonly CAPTURING_PHASE: number = 0; + readonly NONE: number = 0; + + constructor(type: string, options = { cancelable: true, bubbles: true, composed: false }) { + const { cancelable, bubbles, composed } = { ...options }; + this.cancelable = !!cancelable; + this.bubbles = !!bubbles; + this.composed = !!composed; + this.type = `${type}`; + this.defaultPrevented = false; + this.timeStamp = Date.now(); + this.target = null; + } + + stopImmediatePropagation() { + // this[kStop] = true; + } + + preventDefault() { + this.defaultPrevented = true; + } + + get currentTarget() { + return this.target; + } + get srcElement() { + return this.target; + } + + composedPath() { + return this.target ? [this.target] : []; + } + get returnValue() { + return !this.defaultPrevented; + } + get eventPhase() { + return this.target ? Event.AT_TARGET : Event.NONE; + } + get cancelBubble() { + return false; + // return this.propagationStopped; + } + set cancelBubble(value: any) { + if (value) { + this.stopPropagation(); + } + } + stopPropagation() { + // this.propagationStopped = true; + } + initEvent(type: string, bubbles?: boolean, cancelable?: boolean): void { + this.type = type; + if (arguments.length > 1) { + this.bubbles = !!bubbles; + } + if (arguments.length > 2) { + this.cancelable = !!cancelable; + } + } +} + +export const EventClass = typeof Event !== 'undefined' ? Event : Event2; diff --git a/src/packages/multiplexer/Message.ts b/src/packages/multiplexer/Message.ts new file mode 100644 index 0000000..8f5b818 --- /dev/null +++ b/src/packages/multiplexer/Message.ts @@ -0,0 +1,64 @@ +import { MessageType } from './MessageType'; +import Util from '../../app/Util'; +import { CloseEventClass } from './CloseEventClass'; + +export class Message { + public static parse(buffer: ArrayBuffer): Message { + const view = Buffer.from(buffer); + + const type: MessageType = view.readUInt8(0); + const channelId = view.readUInt32LE(1); + const data: ArrayBuffer = buffer.slice(5); + + return new Message(type, channelId, data); + } + + public static fromCloseEvent(id: number, code: number, reason?: string): Message { + const reasonBuffer = reason ? Util.stringToUtf8ByteArray(reason) : Buffer.alloc(0); + const buffer = Buffer.alloc(2 + 4 + reasonBuffer.byteLength); + buffer.writeUInt16LE(code, 0); + if (reasonBuffer.byteLength) { + buffer.writeUInt32LE(reasonBuffer.byteLength, 2); + buffer.set(reasonBuffer, 6); + } + return new Message(MessageType.CloseChannel, id, buffer); + } + + public static createBuffer(type: MessageType, channelId: number, data?: ArrayBuffer): Buffer { + const result = Buffer.alloc(5 + (data ? data.byteLength : 0)); + result.writeUInt8(type, 0); + result.writeUInt32LE(channelId, 1); + if (data?.byteLength) { + result.set(Buffer.from(data), 5); + } + return result; + } + + public constructor( + public readonly type: MessageType, + public readonly channelId: number, + public readonly data: ArrayBuffer, + ) {} + + public toCloseEvent(): CloseEvent { + let code: number | undefined; + let reason: string | undefined; + if (this.data && this.data.byteLength) { + const buffer = Buffer.from(this.data); + code = buffer.readUInt16LE(0); + if (buffer.byteLength > 6) { + const length = buffer.readUInt32LE(2); + reason = Util.utf8ByteArrayToString(buffer.slice(6, 6 + length)); + } + } + return new CloseEventClass('close', { + code, + reason, + wasClean: code === 1000, + }); + } + + public toBuffer(): ArrayBuffer { + return Message.createBuffer(this.type, this.channelId, this.data); + } +} diff --git a/src/packages/multiplexer/MessageEventClass.ts b/src/packages/multiplexer/MessageEventClass.ts new file mode 100644 index 0000000..9838106 --- /dev/null +++ b/src/packages/multiplexer/MessageEventClass.ts @@ -0,0 +1,26 @@ +import { Event2 } from './Event'; + +export class MessageEvent2 extends Event2 implements MessageEvent { + public readonly data: any; + public readonly origin: string; + public readonly lastEventId: string; + public readonly source: any; + public readonly ports: ReadonlyArray; + constructor( + type: string, + { data = null, origin = '', lastEventId = '', source = null, ports = [] }: MessageEventInit = {}, + ) { + super(type); + this.data = data; + this.origin = `${origin}`; + this.lastEventId = `${lastEventId}`; + this.source = source; + this.ports = [...ports]; + } + + initMessageEvent(): void { + throw Error('Deprecated method'); + } +} + +export const MessageEventClass = typeof MessageEvent !== 'undefined' ? MessageEvent : MessageEvent2; diff --git a/src/packages/multiplexer/MessageType.ts b/src/packages/multiplexer/MessageType.ts new file mode 100644 index 0000000..6f7f721 --- /dev/null +++ b/src/packages/multiplexer/MessageType.ts @@ -0,0 +1,7 @@ +export enum MessageType { + CreateChannel = 4, + CloseChannel = 8, + RawBinaryData = 16, + RawStringData = 32, + Data = 64, +} diff --git a/src/packages/multiplexer/Multiplexer.ts b/src/packages/multiplexer/Multiplexer.ts new file mode 100644 index 0000000..ee7b0fd --- /dev/null +++ b/src/packages/multiplexer/Multiplexer.ts @@ -0,0 +1,366 @@ +import { TypedEmitter } from '../../common/TypedEmitter'; +import { Message } from './Message'; +import { MessageType } from './MessageType'; +import { EventClass } from './Event'; +import { CloseEventClass } from './CloseEventClass'; +import { ErrorEventClass } from './ErrorEventClass'; +import { MessageEventClass } from './MessageEventClass'; +import Util from '../../app/Util'; + +interface MultiplexerEvents extends WebSocketEventMap { + empty: Multiplexer; + channel: { channel: Multiplexer; data: ArrayBuffer }; + open: Event; + close: CloseEvent; + message: MessageEvent; +} + +export interface WebsocketEventEmitter { + dispatchEvent(event: Event): boolean; + addEventListener( + type: K, + listener: (this: WebSocket, ev: WebSocketEventMap[K]) => any, + options?: boolean | AddEventListenerOptions, + ): void; + removeEventListener( + type: K, + listener: (this: WebSocket, ev: WebSocketEventMap[K]) => any, + options?: boolean | EventListenerOptions, + ): void; +} + +export class Multiplexer extends TypedEmitter implements WebSocket { + readonly CONNECTING = 0; + readonly OPEN = 1; + readonly CLOSING = 2; + readonly CLOSED = 3; + public binaryType: BinaryType = 'blob'; + public readyState: number; + private channels: Map = new Map(); + private nextId = 0; + private maxId = 4294967296; + private storage: Array = []; + private readonly messageEmitter: WebsocketEventEmitter; + private emptyTimerScheduled = false; + + public onclose: ((this: WebSocket, ev: CloseEvent) => any) | null = null; + public onerror: ((this: WebSocket, ev: Event) => any) | null = null; + public onmessage: ((this: WebSocket, ev: MessageEvent) => any) | null = null; + public onopen: ((this: WebSocket, ev: Event) => any) | null = null; + public url = ''; + + public static wrap(ws: WebSocket): Multiplexer { + return new Multiplexer(ws); + } + + protected constructor(public readonly ws: WebSocket, private _id = 0, emitter?: WebsocketEventEmitter) { + super(); + this.readyState = this.CONNECTING; + if (this._id === 0) { + ws.binaryType = 'arraybuffer'; + this.readyState = this.ws.readyState; + } + this.messageEmitter = emitter || ws; + + const onOpenHandler = (event: Event) => { + this.readyState = this.ws.readyState; + this.dispatchEvent(event); + }; + + const onCloseHandler = (event: CloseEvent) => { + this.readyState = this.ws.readyState; + this.dispatchEvent(event); + this.channels.clear(); + }; + + const onErrorHandler = (event: Event) => { + this.readyState = this.ws.readyState; + this.dispatchEvent(event); + this.channels.clear(); + }; + + const onMessageHandler = (event: MessageEvent) => { + const { data } = event; + // console.log('收到的对象数据', event) + const message = Message.parse(data); + // console.log('解析对象数据', message) + + switch (message.type) { + case MessageType.CreateChannel: { + const { channelId, data } = message; + if (this.nextId < channelId) { + this.nextId = channelId; + } + const channel = this._createChannel(channelId, false); + this.emit('channel', { channel, data }); + break; + } + case MessageType.RawStringData: { + const data = this.channels.get(message.channelId); + if (data) { + const { channel } = data; + const msg = new MessageEventClass('message', { + data: Util.utf8ByteArrayToString(Buffer.from(message.data)), + lastEventId: event.lastEventId, + origin: event.origin, + source: event.source, + }); + channel.dispatchEvent(msg); + } else { + console.error(`Channel with id (${message.channelId}) not found`); + } + break; + } + case MessageType.RawBinaryData: { + const data = this.channels.get(message.channelId); + if (data) { + const { channel } = data; + const msg = new MessageEventClass('message', { + data: message.data, + lastEventId: event.lastEventId, + origin: event.origin, + source: event.source, + }); + channel.dispatchEvent(msg); + } else { + console.error(`Channel with id (${message.channelId}) not found`); + } + break; + } + case MessageType.Data: { + const data = this.channels.get(message.channelId); + if (data) { + const { emitter } = data; + const msg = new MessageEventClass('message', { + data: message.data, + lastEventId: event.lastEventId, + origin: event.origin, + source: event.source, + }); + emitter.dispatchEvent(msg); + } else { + console.error(`Channel with id (${message.channelId}) not found`); + } + break; + } + case MessageType.CloseChannel: { + const data = this.channels.get(message.channelId); + if (data) { + const { channel } = data; + channel.readyState = channel.CLOSING; + try { + channel.dispatchEvent(message.toCloseEvent()); + } finally { + channel.readyState = channel.CLOSED; + } + } else { + console.error(`Channel with id (${message.channelId}) not found`); + } + break; + } + default: + const error = new Error(`Unsupported message type: ${message.type}`); + this.dispatchEvent(new ErrorEventClass('error', { error })); + } + }; + + const onThisOpenHandler = () => { + if (!this.storage.length) { + return; + } + const ws = this.ws; + if (ws instanceof Multiplexer) { + this.storage.forEach((data) => ws.sendData(data)); + } else { + + this.storage.forEach((data) => { + console.log('发送的参数9', data); + + ws.send(data) + }); + } + this.storage.length = 0; + }; + + const onThisCloseHandler = () => { + ws.removeEventListener('open', onOpenHandler); + ws.removeEventListener('error', onErrorHandler); + ws.removeEventListener('close', onCloseHandler); + this.messageEmitter.removeEventListener('message', onMessageHandler); + this.off('close', onThisCloseHandler); + this.off('open', onThisOpenHandler); + }; + + ws.addEventListener('open', onOpenHandler); + ws.addEventListener('error', onErrorHandler); + ws.addEventListener('close', onCloseHandler); + this.messageEmitter.addEventListener('message', onMessageHandler); + + this.on('close', onThisCloseHandler); + this.on('open', onThisOpenHandler); + this.scheduleEmptyEvent(); + } + + public get bufferedAmount(): number { + return 0; + } + + public get extensions(): string { + return ''; + } + + public get protocol(): string { + return ''; + } + + public get id(): number { + return this._id; + } + + private scheduleEmptyEvent(): void { + if (this.emptyTimerScheduled) { + return; + } + this.emptyTimerScheduled = true; + Promise.resolve().then(() => { + if (this.emptyTimerScheduled) { + this.emptyTimerScheduled = false; + this.emit('empty', this); + } + }); + } + + private clearEmptyEvent(): void { + if (this.emptyTimerScheduled) { + this.emptyTimerScheduled = false; + } + } + + public close(code = 1000, reason?: string): void { + if (this.readyState === this.CLOSED || this.readyState === this.CLOSING) { + return; + } + if (this._id) { + this.readyState = this.CLOSING; + + try { + const message = Message.fromCloseEvent(this._id, code, reason).toBuffer(); + if (this.ws instanceof Multiplexer) { + this.ws.sendData(message); + } else { + console.log('发送的参数10'); + + this.ws.send(message); + } + this.emit('close', new CloseEventClass('close', { code, reason })); + } finally { + this.readyState = this.CLOSED; + } + } else { + this.ws.close(code, reason); + } + } + + public send(data: string | ArrayBufferLike | Blob | ArrayBufferView): void { + if (this.ws instanceof Multiplexer) { + if (typeof data === 'string') { + data = Message.createBuffer(MessageType.RawStringData, this._id, Buffer.from(data)); + } else { + data = Message.createBuffer(MessageType.RawBinaryData, this._id, Buffer.from(data)); + } + } + // console.log('发送的参数13send', data); + + this._send(data); + } + + public sendData(data: string | ArrayBufferLike | Blob | ArrayBufferView): void { + if (this.ws instanceof Multiplexer) { + console.log('发送的参数13sendData', data); + data = Message.createBuffer(MessageType.Data, this._id, Buffer.from(data)); + } + + this._send(data); + } + + private _send(data: string | ArrayBufferLike | Blob | ArrayBufferView): void { + const { readyState } = this; + if (readyState === this.OPEN) { + if (this.ws instanceof Multiplexer) { + this.ws.sendData(data); + } else { + // console.log('发送的参数13', data); + this.ws.send(data); + } + } else if (readyState === this.ws.CONNECTING) { + this.storage.push(data); + } else { + throw Error(`Socket is already in CLOSING or CLOSED state.`); + } + } + + private _createChannel(id: number, sendOpenEvent: boolean): Multiplexer { + const emitter = new TypedEmitter(); + const channel = new Multiplexer(this, id, emitter); + this.channels.set(id, { channel, emitter }); + if (sendOpenEvent) { + if (this.readyState === this.OPEN) { + Util.setImmediate(() => { + channel.readyState = this.OPEN; + channel.dispatchEvent(new EventClass('open')); + }); + } + } else { + channel.readyState = this.readyState; + } + channel.addEventListener('close', () => { + this.channels.delete(id); + if (!this.channels.size) { + this.scheduleEmptyEvent(); + } + }); + this.clearEmptyEvent(); + return channel; + } + + public createChannel(data: Buffer): Multiplexer { + if (this.readyState === this.CLOSING || this.readyState === this.CLOSED) { + throw Error('Incorrect socket state'); + } + const id = this.getNextId(); + const channel = this._createChannel(id, true); + console.log('发送的参数原始', data) + this.sendData(Message.createBuffer(MessageType.CreateChannel, id, data)); + return channel; + } + + private getNextId(): number { + let hitTop = false; + while (this.channels.has(++this.nextId)) { + if (this.nextId === this.maxId) { + if (hitTop) { + throw Error('No available id'); + } + this.nextId = 0; + hitTop = true; + } + } + return this.nextId; + } + + public dispatchEvent(event: Event): boolean { + if (event.type === 'close' && typeof this.onclose === 'function') { + Reflect.apply(this.onclose, this, [event]); + } + if (event.type === 'open' && typeof this.onopen === 'function') { + Reflect.apply(this.onopen, this, [event]); + } + if (event.type === 'message' && typeof this.onmessage === 'function') { + Reflect.apply(this.onmessage, this, [event]); + } + if (event.type === 'error' && typeof this.onerror === 'function') { + Reflect.apply(this.onerror, this, [event]); + } + return super.dispatchEvent(event); + } +} diff --git a/src/public/images/buttons/arrow_back.svg b/src/public/images/buttons/arrow_back.svg new file mode 100644 index 0000000..e464d6d --- /dev/null +++ b/src/public/images/buttons/arrow_back.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/public/images/buttons/cancel.svg b/src/public/images/buttons/cancel.svg new file mode 100644 index 0000000..c976ce9 --- /dev/null +++ b/src/public/images/buttons/cancel.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/public/images/buttons/menu.svg b/src/public/images/buttons/menu.svg new file mode 100644 index 0000000..5cfd647 --- /dev/null +++ b/src/public/images/buttons/menu.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/public/images/buttons/offline.svg b/src/public/images/buttons/offline.svg new file mode 100644 index 0000000..468e1d1 --- /dev/null +++ b/src/public/images/buttons/offline.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/public/images/buttons/refresh.svg b/src/public/images/buttons/refresh.svg new file mode 100644 index 0000000..7b94e48 --- /dev/null +++ b/src/public/images/buttons/refresh.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/public/images/buttons/settings.svg b/src/public/images/buttons/settings.svg new file mode 100644 index 0000000..de9f45a --- /dev/null +++ b/src/public/images/buttons/settings.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/public/images/buttons/toggle_off.svg b/src/public/images/buttons/toggle_off.svg new file mode 100644 index 0000000..e60aa40 --- /dev/null +++ b/src/public/images/buttons/toggle_off.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/public/images/buttons/toggle_on.svg b/src/public/images/buttons/toggle_on.svg new file mode 100644 index 0000000..40c0bb8 --- /dev/null +++ b/src/public/images/buttons/toggle_on.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/public/images/multitouch/SOURCE b/src/public/images/multitouch/SOURCE new file mode 100644 index 0000000..8a9bdf5 --- /dev/null +++ b/src/public/images/multitouch/SOURCE @@ -0,0 +1 @@ +https://android.googlesource.com/platform/external/qemu/+/emu-2.0-release/android/skin/qt/images/multitouch/ diff --git a/src/public/images/multitouch/center_point.png b/src/public/images/multitouch/center_point.png new file mode 100644 index 0000000..bbbae58 Binary files /dev/null and b/src/public/images/multitouch/center_point.png differ diff --git a/src/public/images/multitouch/center_point_2x.png b/src/public/images/multitouch/center_point_2x.png new file mode 100644 index 0000000..22bf59e Binary files /dev/null and b/src/public/images/multitouch/center_point_2x.png differ diff --git a/src/public/images/multitouch/touch_point.png b/src/public/images/multitouch/touch_point.png new file mode 100644 index 0000000..3388089 Binary files /dev/null and b/src/public/images/multitouch/touch_point.png differ diff --git a/src/public/images/multitouch/touch_point_2x.png b/src/public/images/multitouch/touch_point_2x.png new file mode 100644 index 0000000..c6d4c54 Binary files /dev/null and b/src/public/images/multitouch/touch_point_2x.png differ diff --git a/src/public/images/skin-light/SOURCE b/src/public/images/skin-light/SOURCE new file mode 100644 index 0000000..9f97be4 --- /dev/null +++ b/src/public/images/skin-light/SOURCE @@ -0,0 +1 @@ +https://android.googlesource.com/platform/external/qemu/+/emu-2.0-release/android/skin/qt/images/light/ diff --git a/src/public/images/skin-light/System_Back_678.svg b/src/public/images/skin-light/System_Back_678.svg new file mode 100644 index 0000000..4637e6b --- /dev/null +++ b/src/public/images/skin-light/System_Back_678.svg @@ -0,0 +1,21 @@ + + + + Artboard 1 + Created with Sketch. + + + + + + + + + + + + + + + + diff --git a/src/public/images/skin-light/System_Home_678.svg b/src/public/images/skin-light/System_Home_678.svg new file mode 100644 index 0000000..5d6d0a2 --- /dev/null +++ b/src/public/images/skin-light/System_Home_678.svg @@ -0,0 +1,15 @@ + + + + System_Home + Created with Sketch. + + + + + + + + + + diff --git a/src/public/images/skin-light/System_Overview_678.svg b/src/public/images/skin-light/System_Overview_678.svg new file mode 100644 index 0000000..2b19b98 --- /dev/null +++ b/src/public/images/skin-light/System_Overview_678.svg @@ -0,0 +1,15 @@ + + + + System_Overview + Created with Sketch. + + + + + + + + + + diff --git a/src/public/images/skin-light/ic_keyboard_678_48dp.svg b/src/public/images/skin-light/ic_keyboard_678_48dp.svg new file mode 100644 index 0000000..ae09f96 --- /dev/null +++ b/src/public/images/skin-light/ic_keyboard_678_48dp.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/public/images/skin-light/ic_more_horiz_678_48dp.svg b/src/public/images/skin-light/ic_more_horiz_678_48dp.svg new file mode 100644 index 0000000..22cf9fe --- /dev/null +++ b/src/public/images/skin-light/ic_more_horiz_678_48dp.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/public/images/skin-light/ic_photo_camera_678_48dp.svg b/src/public/images/skin-light/ic_photo_camera_678_48dp.svg new file mode 100644 index 0000000..e58a0f3 --- /dev/null +++ b/src/public/images/skin-light/ic_photo_camera_678_48dp.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/public/images/skin-light/ic_power_settings_new_678_48px.svg b/src/public/images/skin-light/ic_power_settings_new_678_48px.svg new file mode 100644 index 0000000..c0e4ade --- /dev/null +++ b/src/public/images/skin-light/ic_power_settings_new_678_48px.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/public/images/skin-light/ic_volume_down_678_48px.svg b/src/public/images/skin-light/ic_volume_down_678_48px.svg new file mode 100644 index 0000000..f4e6282 --- /dev/null +++ b/src/public/images/skin-light/ic_volume_down_678_48px.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/public/images/skin-light/ic_volume_up_678_48px.svg b/src/public/images/skin-light/ic_volume_up_678_48px.svg new file mode 100644 index 0000000..dcfaa3a --- /dev/null +++ b/src/public/images/skin-light/ic_volume_up_678_48px.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/public/index.html b/src/public/index.html new file mode 100644 index 0000000..394398c --- /dev/null +++ b/src/public/index.html @@ -0,0 +1,10 @@ + + + + + + WS scrcpy + + + + diff --git a/src/server/Config.ts b/src/server/Config.ts new file mode 100644 index 0000000..37d962f --- /dev/null +++ b/src/server/Config.ts @@ -0,0 +1,156 @@ +import * as process from 'process'; +import * as fs from 'fs'; +import * as path from 'path'; +import { Configuration, HostItem, ServerItem } from '../types/Configuration'; +import { EnvName } from './EnvName'; +import YAML from 'yaml'; + +const DEFAULT_PORT = 8000; + +const YAML_RE = /^.+\.(yaml|yml)$/i; +const JSON_RE = /^.+\.(json|js)$/i; + +export class Config { + private static instance?: Config; + private static initConfig(userConfig: Configuration = {}): Required { + let runGoogTracker = false; + let announceGoogTracker = false; + /// #if INCLUDE_GOOG + runGoogTracker = true; + announceGoogTracker = true; + /// #endif + + let runApplTracker = false; + let announceApplTracker = false; + /// #if INCLUDE_APPL + runApplTracker = true; + announceApplTracker = true; + /// #endif + const server: ServerItem[] = [ + { + secure: false, + port: DEFAULT_PORT, + }, + ]; + const defaultConfig: Required = { + runGoogTracker, + runApplTracker, + announceGoogTracker, + announceApplTracker, + server, + remoteHostList: [], + }; + const merged = Object.assign({}, defaultConfig, userConfig); + merged.server = merged.server.map((item) => this.parseServerItem(item)); + return merged; + } + private static parseServerItem(config: Partial = {}): ServerItem { + const secure = config.secure || false; + const port = config.port || (secure ? 443 : 80); + const options = config.options; + const redirectToSecure = config.redirectToSecure || false; + if (secure && !options) { + throw Error('Must provide "options" for secure server configuration'); + } + if (options?.certPath) { + if (options.cert) { + throw Error(`Can't use "cert" and "certPath" together`); + } + options.cert = this.readFile(options.certPath); + } + if (options?.keyPath) { + if (options.key) { + throw Error(`Can't use "key" and "keyPath" together`); + } + options.key = this.readFile(options.keyPath); + } + const serverItem: ServerItem = { + secure, + port, + redirectToSecure, + }; + if (typeof options !== 'undefined') { + serverItem.options = options; + } + if (typeof redirectToSecure === 'boolean') { + serverItem.redirectToSecure = redirectToSecure; + } + return serverItem; + } + public static getInstance(): Config { + if (!this.instance) { + const configPath = process.env[EnvName.CONFIG_PATH]; + let userConfig: Configuration; + if (!configPath) { + userConfig = {}; + } else { + if (configPath.match(YAML_RE)) { + userConfig = YAML.parse(this.readFile(configPath)); + } else if (configPath.match(JSON_RE)) { + userConfig = JSON.parse(this.readFile(configPath)); + } else { + throw Error(`Unknown file type: ${configPath}`); + } + } + const fullConfig = this.initConfig(userConfig); + this.instance = new Config(fullConfig); + } + return this.instance; + } + + public static readFile(pathString: string): string { + const isAbsolute = pathString.startsWith('/'); + const absolutePath = isAbsolute ? pathString : path.resolve(process.cwd(), pathString); + if (!fs.existsSync(absolutePath)) { + throw Error(`Can't find file "${absolutePath}"`); + } + return fs.readFileSync(absolutePath).toString(); + } + + constructor(private fullConfig: Required) {} + + public getHostList(): HostItem[] { + if (!this.fullConfig.remoteHostList || !this.fullConfig.remoteHostList.length) { + return []; + } + const hostList: HostItem[] = []; + this.fullConfig.remoteHostList.forEach((item) => { + const { hostname, port, pathname, secure, useProxy } = item; + if (Array.isArray(item.type)) { + item.type.forEach((type) => { + hostList.push({ + hostname, + port, + pathname, + secure, + useProxy, + type, + }); + }); + } else { + hostList.push({ hostname, port, pathname, secure, useProxy, type: item.type }); + } + }); + return hostList; + } + + public get runLocalGoogTracker(): boolean { + return this.fullConfig.runGoogTracker; + } + + public get announceLocalGoogTracker(): boolean { + return this.fullConfig.runGoogTracker; + } + + public get runLocalApplTracker(): boolean { + return this.fullConfig.runApplTracker; + } + + public get announceLocalApplTracker(): boolean { + return this.fullConfig.runApplTracker; + } + + public get servers(): ServerItem[] { + return this.fullConfig.server; + } +} diff --git a/src/server/EnvName.ts b/src/server/EnvName.ts new file mode 100644 index 0000000..f37ea6a --- /dev/null +++ b/src/server/EnvName.ts @@ -0,0 +1,4 @@ +export enum EnvName { + CONFIG_PATH = 'WS_SCRCPY_CONFIG', + WS_SCRCPY_PATHNAME = 'WS_SCRCPY_PATHNAME', +} diff --git a/src/server/Utils.ts b/src/server/Utils.ts new file mode 100644 index 0000000..8cdc03b --- /dev/null +++ b/src/server/Utils.ts @@ -0,0 +1,47 @@ +import * as os from 'os'; + +export class Utils { + public static printListeningMsg(proto: string, port: number, pathname: string): void { + const ipv4List: string[] = []; + const ipv6List: string[] = []; + const formatAddress = (ip: string, scopeid: number | undefined): void => { + if (typeof scopeid === 'undefined') { + ipv4List.push(`${proto}://${ip}:${port}${pathname}`); + return; + } + if (scopeid === 0) { + ipv6List.push(`${proto}://[${ip}]:${port}${pathname}`); + } else { + return; + // skip + // ipv6List.push(`${proto}://[${ip}%${scopeid}]:${port}`); + } + }; + Object.keys(os.networkInterfaces()) + .map((key) => os.networkInterfaces()[key]) + .forEach((info) => { + info.forEach((iface) => { + let scopeid: number | undefined; + if (iface.family === 'IPv6') { + scopeid = iface.scopeid; + } else if (iface.family === 'IPv4') { + scopeid = undefined; + } else { + return; + } + formatAddress(iface.address, scopeid); + }); + }); + const nameList = [ + encodeURI(`${proto}://${os.hostname()}:${port}${pathname}`), + encodeURI(`${proto}://localhost:${port}${pathname}`), + ]; + console.log('Listening on:\n\t' + nameList.join(' ')); + if (ipv4List.length) { + console.log('\t' + ipv4List.join(' ')); + } + if (ipv6List.length) { + console.log('\t' + ipv6List.join(' ')); + } + } +} diff --git a/src/server/appl-device/mw/DeviceTracker.ts b/src/server/appl-device/mw/DeviceTracker.ts new file mode 100644 index 0000000..ee972ec --- /dev/null +++ b/src/server/appl-device/mw/DeviceTracker.ts @@ -0,0 +1,91 @@ +import WS from 'ws'; +import { Mw, RequestParameters } from '../../mw/Mw'; +import { ControlCenterCommand } from '../../../common/ControlCenterCommand'; +import { ACTION } from '../../../common/Action'; +import { DeviceTrackerEvent } from '../../../types/DeviceTrackerEvent'; +import { DeviceTrackerEventList } from '../../../types/DeviceTrackerEventList'; +import { ControlCenter } from '../services/ControlCenter'; +import ApplDeviceDescriptor from '../../../types/ApplDeviceDescriptor'; +import { Multiplexer } from '../../../packages/multiplexer/Multiplexer'; +import { ChannelCode } from '../../../common/ChannelCode'; + +export class DeviceTracker extends Mw { + public static readonly TAG = 'IosDeviceTracker'; + public static readonly type = 'ios'; + private icc: ControlCenter = ControlCenter.getInstance(); + private readonly id: string; + + public static processChannel(ws: Multiplexer, code: string): Mw | undefined { + if (code !== ChannelCode.ATRC) { + return; + } + return new DeviceTracker(ws); + } + + public static processRequest(ws: WS, params: RequestParameters): DeviceTracker | undefined { + if (params.action !== ACTION.APPL_DEVICE_LIST) { + return; + } + return new DeviceTracker(ws); + } + + constructor(ws: WS | Multiplexer) { + super(ws); + + this.id = this.icc.getId(); + this.icc + .init() + .then(() => { + this.icc.on('device', this.sendDeviceMessage); + this.buildAndSendMessage(this.icc.getDevices()); + }) + .catch((error: Error) => { + console.error(`[${DeviceTracker.TAG}] Error: ${error.message}`); + }); + } + + private sendDeviceMessage = (device: ApplDeviceDescriptor): void => { + const data: DeviceTrackerEvent = { + device, + id: this.id, + name: this.icc.getName(), + }; + this.sendMessage({ + id: -1, + type: 'device', + data, + }); + }; + + private buildAndSendMessage = (list: ApplDeviceDescriptor[]): void => { + const data: DeviceTrackerEventList = { + list, + id: this.id, + name: this.icc.getName(), + }; + this.sendMessage({ + id: -1, + type: 'devicelist', + data, + }); + }; + + protected onSocketMessage(event: WS.MessageEvent): void { + console.log("接收到的参数2", event.data) + let command: ControlCenterCommand; + try { + command = ControlCenterCommand.fromJSON(event.data.toString()); + } catch (error: any) { + console.error(`[${DeviceTracker.TAG}], Received message: ${event.data}. Error: ${error.message}`); + return; + } + this.icc.runCommand(command).catch((error) => { + console.error(`[${DeviceTracker.TAG}], Received message: ${event.data}. Error: ${error.message}`); + }); + } + + public release(): void { + super.release(); + this.icc.off('device', this.sendDeviceMessage); + } +} diff --git a/src/server/appl-device/mw/QVHStreamProxy.ts b/src/server/appl-device/mw/QVHStreamProxy.ts new file mode 100644 index 0000000..b3d63ff --- /dev/null +++ b/src/server/appl-device/mw/QVHStreamProxy.ts @@ -0,0 +1,81 @@ +import WS from 'ws'; +import { Mw } from '../../mw/Mw'; +import { ControlCenterCommand } from '../../../common/ControlCenterCommand'; +import { QvhackRunner } from '../services/QvhackRunner'; +import { WebsocketProxy } from '../../mw/WebsocketProxy'; +import { Multiplexer } from '../../../packages/multiplexer/Multiplexer'; +import { ChannelCode } from '../../../common/ChannelCode'; +import Util from '../../../app/Util'; + +export class QVHStreamProxy extends Mw { + public static readonly TAG = 'QVHStreamProxy'; + + public static processChannel(ws: Multiplexer, code: string, data: ArrayBuffer): Mw | undefined { + if (code !== ChannelCode.QVHS) { + return; + } + if (!data || data.byteLength < 4) { + return; + } + const buffer = Buffer.from(data); + const length = buffer.readInt32LE(0); + const udid = Util.utf8ByteArrayToString(buffer.slice(4, 4 + length)); + return new QVHStreamProxy(ws, udid); + } + + private qvhProcess: QvhackRunner; + private wsProxy?: WebsocketProxy; + protected name: string; + constructor(protected ws: Multiplexer, private readonly udid: string) { + super(ws); + this.name = `[${QVHStreamProxy.TAG}][udid:${this.udid}]`; + this.qvhProcess = QvhackRunner.getInstance(udid); + this.attachEventListeners(); + } + + private onStarted = (): void => { + const remote = this.qvhProcess.getWebSocketAddress(); + this.wsProxy = WebsocketProxy.createProxy(this.ws, remote); + this.ws.addEventListener('close', this.onSocketClose.bind(this)); + }; + + private attachEventListeners(): void { + if (this.qvhProcess.isStarted()) { + this.onStarted(); + } else { + this.qvhProcess.once('started', this.onStarted); + } + } + + protected onSocketMessage(event: WS.MessageEvent): void { + console.log("接收到的参数13", event.data) + + let command: ControlCenterCommand; + try { + command = ControlCenterCommand.fromJSON(event.data.toString()); + } catch (error: any) { + console.error(`${this.name}, Received message: ${event.data}. Error: ${error.message}`); + return; + } + console.log(`${this.name}, Received message: type:"${command.getType()}", data:${command.getData()}.`); + } + + protected onSocketClose(): void { + if (this.wsProxy) { + this.wsProxy.release(); + delete this.wsProxy; + } + this.release(); + } + + public release(): void { + super.release(); + if (this.qvhProcess) { + this.qvhProcess.release(); + } + if (this.wsProxy) { + this.wsProxy.release(); + delete this.wsProxy; + } + } +} diff --git a/src/server/appl-device/mw/WebDriverAgentProxy.ts b/src/server/appl-device/mw/WebDriverAgentProxy.ts new file mode 100644 index 0000000..2ac5408 --- /dev/null +++ b/src/server/appl-device/mw/WebDriverAgentProxy.ts @@ -0,0 +1,136 @@ +import WS from 'ws'; +import { Mw } from '../../mw/Mw'; +import { ControlCenterCommand } from '../../../common/ControlCenterCommand'; +import { WdaRunner } from '../services/WDARunner'; +import { MessageRunWdaResponse } from '../../../types/MessageRunWdaResponse'; +import { Multiplexer } from '../../../packages/multiplexer/Multiplexer'; +import { ChannelCode } from '../../../common/ChannelCode'; +import Util from '../../../app/Util'; +import { WdaStatus } from '../../../common/WdaStatus'; + +export class WebDriverAgentProxy extends Mw { + public static readonly TAG = 'WebDriverAgentProxy'; + protected name: string; + private wda?: WdaRunner; + + public static processChannel(ws: Multiplexer, code: string, data: ArrayBuffer): Mw | undefined { + if (code !== ChannelCode.WDAP) { + return; + } + if (!data || data.byteLength < 4) { + return; + } + const buffer = Buffer.from(data); + const length = buffer.readInt32LE(0); + const udid = Util.utf8ByteArrayToString(buffer.slice(4, 4 + length)); + return new WebDriverAgentProxy(ws, udid); + } + + constructor(protected ws: Multiplexer, private readonly udid: string) { + super(ws); + this.name = `[${WebDriverAgentProxy.TAG}][udid: ${this.udid}]`; + } + + private runWda(command: ControlCenterCommand): void { + const udid = command.getUdid(); + const id = command.getId(); + if (this.wda) { + const message: MessageRunWdaResponse = { + id, + type: 'run-wda', + data: { + udid: udid, + status: WdaStatus.STARTED, + code: -1, + text: 'WDA already started', + }, + }; + this.sendMessage(message); + return; + } + this.wda = WdaRunner.getInstance(udid); + this.wda.on('status-change', ({ status, code, text }) => { + this.onStatusChange(command, status, code, text); + }); + if (this.wda.isStarted()) { + this.onStatusChange(command, WdaStatus.STARTED); + } else { + this.wda.start(); + } + } + + private onStatusChange = (command: ControlCenterCommand, status: WdaStatus, code?: number, text?: string): void => { + const id = command.getId(); + const udid = command.getUdid(); + const type = 'run-wda'; + const message: MessageRunWdaResponse = { + id, + type, + data: { + udid, + status, + code, + text, + }, + }; + this.sendMessage(message); + }; + + private requestWda(command: ControlCenterCommand): void { + if (!this.wda) { + return; + } + this.wda + .request(command) + .then((response) => { + this.sendMessage({ + id: command.getId(), + type: command.getType(), + data: { + success: true, + response, + }, + }); + }) + .catch((e) => { + this.sendMessage({ + id: command.getId(), + type: command.getType(), + data: { + success: false, + error: e.message, + }, + }); + }); + } + + protected onSocketMessage(event: WS.MessageEvent): void { + console.log("接收到的参数44", event.data) + + let command: ControlCenterCommand; + try { + command = ControlCenterCommand.fromJSON(event.data.toString()); + } catch (error: any) { + console.error(`[${WebDriverAgentProxy.TAG}], Received message: ${event.data}. Error: ${error.message}`); + return; + } + const type = command.getType(); + switch (type) { + case ControlCenterCommand.RUN_WDA: + this.runWda(command); + break; + case ControlCenterCommand.REQUEST_WDA: + this.requestWda(command); + break; + default: + throw new Error(`Unsupported command: "${type}"`); + } + } + + public release(): void { + super.release(); + if (this.wda) { + this.wda.release(); + } + } +} diff --git a/src/server/appl-device/services/ControlCenter.ts b/src/server/appl-device/services/ControlCenter.ts new file mode 100644 index 0000000..970e95c --- /dev/null +++ b/src/server/appl-device/services/ControlCenter.ts @@ -0,0 +1,126 @@ +import { Service } from '../../services/Service'; +import { BaseControlCenter } from '../../services/BaseControlCenter'; +import { ControlCenterCommand } from '../../../common/ControlCenterCommand'; +import * as os from 'os'; +import * as crypto from 'crypto'; +import ApplDeviceDescriptor from '../../../types/ApplDeviceDescriptor'; +import { IOSDeviceLib } from 'ios-device-lib'; +import { DeviceState } from '../../../common/DeviceState'; +import { ProductType } from '../../../common/ProductType'; + +export class ControlCenter extends BaseControlCenter implements Service { + private static instance?: ControlCenter; + + private initialized = false; + private tracker?: IOSDeviceLib.IOSDeviceLib; + private descriptors: Map = new Map(); + private readonly id: string; + + protected constructor() { + super(); + const idString = `appl|${os.hostname()}|${os.uptime()}`; + this.id = crypto.createHash('md5').update(idString).digest('hex'); + } + + public static getInstance(): ControlCenter { + if (!this.instance) { + this.instance = new ControlCenter(); + } + return this.instance; + } + + public static hasInstance(): boolean { + return !!ControlCenter.instance; + } + + private onDeviceUpdate = (device: IOSDeviceLib.IDeviceActionInfo): void => { + const udid = device.deviceId; + const state = device.status || ''; + const name = device.deviceName || ''; + const productType = device.productType || ''; + const version = device.productVersion || ''; + const model = ProductType.getModel(productType); + const descriptor = { + udid, + name, + model, + version, + state, + 'last.update.timestamp': Date.now(), + }; + this.descriptors.set(udid, descriptor); + this.emit('device', descriptor); + }; + + private onDeviceLost = (device: IOSDeviceLib.IDeviceActionInfo): void => { + const udid = device.deviceId; + const descriptor = this.descriptors.get(udid); + if (!descriptor) { + console.warn(`Received "lost" event for unknown device "${udid}"`); + return; + } + descriptor.state = DeviceState.DISCONNECTED; + this.emit('device', descriptor); + }; + + public async init(): Promise { + if (this.initialized) { + return; + } + this.tracker = await this.startTracker(); + this.initialized = true; + } + + private async startTracker(): Promise { + if (this.tracker) { + return this.tracker; + } + this.tracker = new IOSDeviceLib(this.onDeviceUpdate, this.onDeviceUpdate, this.onDeviceLost); + return this.tracker; + } + + private stopTracker(): void { + if (this.tracker) { + this.tracker.dispose(); + this.tracker = undefined; + } + this.tracker = undefined; + this.initialized = false; + } + + public getDevices(): ApplDeviceDescriptor[] { + return Array.from(this.descriptors.values()); + } + + public getId(): string { + return this.id; + } + + public getName(): string { + return `iDevice Tracker [${os.hostname()}]`; + } + + public start(): Promise { + return this.init().catch((e) => { + console.error(`Error: Failed to init "${this.getName()}". ${e.message}`); + }); + } + + public release(): void { + this.stopTracker(); + } + + public async runCommand(command: ControlCenterCommand): Promise { + const udid = command.getUdid(); + const device = this.descriptors.get(udid); + if (!device) { + console.error(`Device with udid:"${udid}" not found`); + return; + } + const type = command.getType(); + switch (type) { + default: + throw new Error(`Unsupported command: "${type}"`); + } + } +} diff --git a/src/server/appl-device/services/QvhackRunner.ts b/src/server/appl-device/services/QvhackRunner.ts new file mode 100644 index 0000000..e6b93f1 --- /dev/null +++ b/src/server/appl-device/services/QvhackRunner.ts @@ -0,0 +1,80 @@ +import * as portfinder from 'portfinder'; +import { ProcessRunner, ProcessRunnerEvents } from '../../services/ProcessRunner'; + +export class QvhackRunner extends ProcessRunner { + private static instances: Map = new Map(); + public static SHUTDOWN_TIMEOUT = 15000; + public static getInstance(udid: string): QvhackRunner { + let instance = this.instances.get(udid); + if (!instance) { + instance = new QvhackRunner(udid); + this.instances.set(udid, instance); + instance.start(); + } + instance.lock(); + return instance; + } + protected TAG = '[QvhackRunner]'; + protected name: string; + protected cmd = 'ws-qvh'; + protected releaseTimeoutId?: NodeJS.Timeout; + protected address = ''; + protected started = false; + private holders = 0; + + constructor(private readonly udid: string) { + super(); + this.name = `${this.TAG}[udid: ${this.udid}]`; + } + + public getWebSocketAddress(): string { + return this.address; + } + + protected lock(): void { + if (this.releaseTimeoutId) { + clearTimeout(this.releaseTimeoutId); + } + this.holders++; + } + + protected unlock(): void { + this.holders--; + if (this.holders > 0) { + return; + } + this.releaseTimeoutId = setTimeout(() => { + super.release(); + QvhackRunner.instances.delete(this.udid); + }, QvhackRunner.SHUTDOWN_TIMEOUT); + } + + protected async getArgs(): Promise { + const port = await portfinder.getPortPromise(); + const host = `127.0.0.1:${port}`; + this.address = `ws://${host}/ws?stream=${encodeURIComponent(this.udid)}`; + return [host]; + } + + public async start(): Promise { + return this.runProcess() + .then(() => { + // Wait for server to start listen on a port + this.once('stderr', () => { + this.started = true; + this.emit('started', true); + }); + }) + .catch((e) => { + console.error(this.name, e.message); + }); + } + + public isStarted(): boolean { + return this.started; + } + + public release(): void { + this.unlock(); + } +} diff --git a/src/server/appl-device/services/WDARunner.ts b/src/server/appl-device/services/WDARunner.ts new file mode 100644 index 0000000..5f0a865 --- /dev/null +++ b/src/server/appl-device/services/WDARunner.ts @@ -0,0 +1,209 @@ +import { ControlCenterCommand } from '../../../common/ControlCenterCommand'; +import { TypedEmitter } from '../../../common/TypedEmitter'; +import * as portfinder from 'portfinder'; +import { Server, XCUITestDriver } from '../../../types/WdaServer'; +import * as XCUITest from 'appium-xcuitest-driver'; +import { WDAMethod } from '../../../common/WDAMethod'; +import { timing } from 'appium-support'; +import { WdaStatus } from '../../../common/WdaStatus'; + +const MJPEG_SERVER_PORT = 9100; + +export interface WdaRunnerEvents { + 'status-change': { status: WdaStatus; text?: string; code?: number }; + error: Error; +} + +export class WdaRunner extends TypedEmitter { + protected static TAG = 'WDARunner'; + private static instances: Map = new Map(); + public static SHUTDOWN_TIMEOUT = 15000; + private static servers: Map = new Map(); + private static cachedScreenWidth: Map = new Map(); + public static getInstance(udid: string): WdaRunner { + let instance = this.instances.get(udid); + if (!instance) { + instance = new WdaRunner(udid); + this.instances.set(udid, instance); + } + instance.lock(); + return instance; + } + public static async getServer(udid: string): Promise { + let server = this.servers.get(udid); + if (!server) { + const port = await portfinder.getPortPromise(); + server = await XCUITest.startServer(port, '127.0.0.1'); + server.on('error', (...args: any[]) => { + console.error('Server Error:', args); + }); + server.on('close', (...args: any[]) => { + console.error('Server Close:', args); + }); + this.servers.set(udid, server); + } + return server; + } + + public static async getScreenWidth(udid: string, driver: XCUITestDriver): Promise { + const cached = this.cachedScreenWidth.get(udid); + if (cached) { + return cached; + } + const info = await driver.getScreenInfo(); + if (info && info.statusBarSize.width > 0) { + const screenWidth = info.statusBarSize.width; + this.cachedScreenWidth.set(udid, screenWidth); + return screenWidth; + } + const el = await driver.findElement('xpath', '//XCUIElementTypeApplication'); + const size = await driver.getSize(el); + if (size) { + const screenWidth = size.width; + this.cachedScreenWidth.set(udid, screenWidth); + return screenWidth; + } + return 0; + } + + protected name: string; + protected started = false; + protected starting = false; + private server?: Server; + private mjpegServerPort = 0; + private wdaLocalPort = 0; + private holders = 0; + protected releaseTimeoutId?: NodeJS.Timeout; + + constructor(private readonly udid: string) { + super(); + this.name = `[${WdaRunner.TAG}][udid: ${this.udid}]`; + } + + protected lock(): void { + if (this.releaseTimeoutId) { + clearTimeout(this.releaseTimeoutId); + } + this.holders++; + } + + protected unlock(): void { + this.holders--; + if (this.holders > 0) { + return; + } + this.releaseTimeoutId = setTimeout(async () => { + WdaRunner.servers.delete(this.udid); + WdaRunner.instances.delete(this.udid); + if (this.server) { + if (this.server.driver) { + await this.server.driver.deleteSession(); + } + this.server.close(); + delete this.server; + } + }, WdaRunner.SHUTDOWN_TIMEOUT); + } + + public get mjpegPort(): number { + return this.mjpegServerPort; + } + + public async request(command: ControlCenterCommand): Promise { + const driver = this.server?.driver; + if (!driver) { + return; + } + + const method = command.getMethod(); + const args = command.getArgs(); + switch (method) { + case WDAMethod.GET_SCREEN_WIDTH: + return WdaRunner.getScreenWidth(this.udid, driver); + case WDAMethod.CLICK: + return driver.performTouch([{ action: 'tap', options: { x: args.x, y: args.y } }]); + case WDAMethod.PRESS_BUTTON: + return driver.mobilePressButton({ name: args.name }); + case WDAMethod.SCROLL: + const { from, to } = args; + return driver.performTouch([ + { action: 'press', options: { x: from.x, y: from.y } }, + { action: 'wait', options: { ms: 500 } }, + { action: 'moveTo', options: { x: to.x, y: to.y } }, + { action: 'release', options: {} }, + ]); + case WDAMethod.APPIUM_SETTINGS: + return driver.updateSettings(args.options); + case WDAMethod.SEND_KEYS: + return driver.keys(args.keys); + default: + return `Unknown command: ${method}`; + } + } + + public async start(): Promise { + if (this.started || this.starting) { + return; + } + this.emit('status-change', { status: WdaStatus.STARTING }); + this.starting = true; + const server = await WdaRunner.getServer(this.udid); + try { + const remoteMjpegServerPort = MJPEG_SERVER_PORT; + const ports = await Promise.all([portfinder.getPortPromise(), portfinder.getPortPromise()]); + this.wdaLocalPort = ports[0]; + this.mjpegServerPort = ports[1]; + await server.driver.createSession({ + platformName: 'iOS', + deviceName: 'my iphone', + udid: this.udid, + wdaLocalPort: this.wdaLocalPort, + usePrebuiltWDA: true, + mjpegServerPort: remoteMjpegServerPort, + }); + await server.driver.wda.xcodebuild.waitForStart(new timing.Timer().start()); + if (server.driver?.wda?.xcodebuild?.xcodebuild) { + server.driver.wda.xcodebuild.xcodebuild.on('exit', (code: number) => { + this.started = false; + this.starting = false; + server.driver.deleteSession(); + delete this.server; + this.emit('status-change', { status: WdaStatus.STOPPED, code }); + if (this.holders > 0) { + this.start(); + } + }); + } else { + this.started = false; + this.starting = false; + delete this.server; + throw new Error('xcodebuild process not found'); + } + /// #if USE_WDA_MJPEG_SERVER + const { DEVICE_CONNECTIONS_FACTORY } = await import( + 'appium-xcuitest-driver/build/lib/device-connections-factory' + ); + + await DEVICE_CONNECTIONS_FACTORY.requestConnection(this.udid, this.mjpegServerPort, { + usePortForwarding: true, + devicePort: remoteMjpegServerPort, + }); + /// #endif + this.started = true; + this.emit('status-change', { status: WdaStatus.STARTED }); + } catch (error: any) { + this.started = false; + this.starting = false; + this.emit('error', error); + } + this.server = server; + } + + public isStarted(): boolean { + return this.started; + } + + public release(): void { + this.unlock(); + } +} diff --git a/src/server/goog-device/AdbUtils.ts b/src/server/goog-device/AdbUtils.ts new file mode 100644 index 0000000..e0a0093 --- /dev/null +++ b/src/server/goog-device/AdbUtils.ts @@ -0,0 +1,372 @@ +import * as portfinder from 'portfinder'; +import * as http from 'http'; +import * as path from 'path'; +import { ACTION } from '../../common/Action'; +import { AdbExtended } from './adb'; +import { DevtoolsInfo, RemoteBrowserInfo, RemoteTarget, VersionMetadata } from '../../types/RemoteDevtools'; +import { URL } from 'url'; +import { Forward } from '@dead50f7/adbkit/lib/Forward'; +import Entry from '@dead50f7/adbkit/lib/adb/sync/entry'; +import Stats from '@dead50f7/adbkit/lib/adb/sync/stats'; +import PullTransfer from '@dead50f7/adbkit/lib/adb/sync/pulltransfer'; +import { FileStats } from '../../types/FileStats'; +import Protocol from '@dead50f7/adbkit/lib/adb/protocol'; +import { Multiplexer } from '../../packages/multiplexer/Multiplexer'; +import { ReadStream } from 'fs'; +import PushTransfer from '@dead50f7/adbkit/lib/adb/sync/pushtransfer'; + +type IncomingMessage = { + statusCode?: number; + contentType?: string; + body: string; +}; + +const proto = 'http://'; +const fakeHost = '127.0.0.1:6666'; +const fakeHostRe = /127\.0\.0\.1:6666/; + +export class AdbUtils { + private static async formatStatsMin(entry: Entry): Promise { + return { + name: entry.name, + isDir: entry.isDirectory() ? 1 : 0, + size: entry.size, + dateModified: entry.mtimeMs ? entry.mtimeMs : entry.mtime.getTime(), + }; + } + + public static async push(serial: string, stream: ReadStream, pathString: string): Promise { + const client = AdbExtended.createClient(); + const transfer = await client.push(serial, stream, pathString); + client.on('error', (error: Error) => { + transfer.emit('error', error); + }); + return transfer; + } + + public static async stats(serial: string, pathString: string, stats?: Stats, deep = 0): Promise { + if (!stats || (stats.isSymbolicLink() && pathString.endsWith('/'))) { + const client = AdbExtended.createClient(); + stats = await client.stat(serial, pathString); + } + if (stats.isSymbolicLink()) { + if (deep === 5) { + throw Error('Too deep'); + } + if (!pathString.endsWith('/')) { + pathString += '/'; + } + try { + stats = await this.stats(serial, pathString, stats, deep++); + } catch (error: any) { + if (error.message === 'Too deep') { + if (deep === 0) { + console.error(`Symlink is too deep: ${pathString}`); + return stats; + } + throw error; + } + if (error.code !== 'ENOENT') { + console.error(error.message); + } + } + return stats; + } + return stats; + } + + public static async readdir(serial: string, pathString: string): Promise { + const client = AdbExtended.createClient(); + const list = await client.readdir(serial, pathString); + const all = list.map(async (entry) => { + if (entry.isSymbolicLink()) { + const stat = await this.stats(serial, path.join(pathString, entry.name)); + const mtime = stat.mtimeMs ? stat.mtimeMs : stat.mtime.getTime(); + entry = new Entry(entry.name, stat.mode, stat.size, (mtime / 1000) | 0); + } + return AdbUtils.formatStatsMin(entry); + }); + return Promise.all(all); + } + + public static async pipePullFile(serial: string, pathString: string): Promise { + const client = AdbExtended.createClient(); + const transfer = await client.pull(serial, pathString); + + transfer.on('progress', function (stats) { + console.log('[%s] [%s] Pulled %d bytes so far', serial, pathString, stats.bytesTransferred); + }); + transfer.on('end', function () { + console.log('[%s] [%s] Pull complete', serial, pathString); + }); + return new Promise((resolve, reject) => { + transfer.on('readable', () => { + resolve(transfer); + }); + transfer.on('error', (e) => { + reject(e); + }); + }); + } + + public static async pipeStatToStream(serial: string, pathString: string, stream: Multiplexer): Promise { + const client = AdbExtended.createClient(); + return client.pipeStat(serial, pathString, stream); + } + + public static async pipeReadDirToStream(serial: string, pathString: string, stream: Multiplexer): Promise { + const client = AdbExtended.createClient(); + return client.pipeReadDir(serial, pathString, stream); + } + + public static async pipePullFileToStream(serial: string, pathString: string, stream: Multiplexer): Promise { + const client = AdbExtended.createClient(); + const transfer = await client.pull(serial, pathString); + transfer.on('data', (data) => { + console.log('发送的参数14'); + + stream.send(Buffer.concat([Buffer.from(Protocol.DATA, 'ascii'), data])); + }); + return new Promise((resolve, reject) => { + transfer.on('end', function () { + console.log('发送的参数15'); + + stream.send(Buffer.from(Protocol.DONE, 'ascii')); + stream.close(); + resolve(); + }); + transfer.on('error', (e) => { + reject(e); + }); + }); + } + + public static async forward(serial: string, remote: string): Promise { + const client = AdbExtended.createClient(); + const forwards = await client.listForwards(serial); + const forward = forwards.find((item: Forward) => { + return item.remote === remote && item.local.startsWith('tcp:') && item.serial === serial; + }); + if (forward) { + const { local } = forward; + return parseInt(local.split('tcp:')[1], 10); + } + const port = await portfinder.getPortPromise(); + const local = `tcp:${port}`; + await client.forward(serial, local, remote); + return port; + } + + public static async getDevtoolsRemoteList(serial: string): Promise { + const client = AdbExtended.createClient(); + const stream = await client.shell(serial, 'cat /proc/net/unix'); + const buffer = await AdbExtended.util.readAll(stream); + const lines = buffer + .toString() + .split('\n') + .filter((s: string) => { + if (!s) { + return false; + } + return s.includes('devtools_remote'); + }); + const names: string[] = []; + lines.forEach((line: string) => { + const temp = line.split(' '); + if (temp.length !== 8) { + return; + } + if (temp[5] === '01') { + const name = temp[7].substr(1); + names.push(name); + } + }); + return names; + } + + private static async createHttpRequest( + serial: string, + unixSocketName: string, + url: string, + ): Promise { + const client = AdbExtended.createClient(); + const socket = await client.openLocal(serial, `localabstract:${unixSocketName}`); + const request = new (http.ClientRequest as any)(url, { + createConnection: () => { + return socket; + }, + }); + const message: http.IncomingMessage = await new Promise((resolve, reject) => { + request.on('response', (r: http.IncomingMessage) => { + resolve(r); + }); + request.on('socket', () => { + request.end(); + }); + request.on('error', (error: Error) => { + reject(error); + }); + }); + let data = ''; + return new Promise((resolve, reject) => { + message.on('data', (d) => { + data += d; + }); + message.on('end', () => { + const { statusCode } = message; + resolve({ + statusCode, + contentType: message.headers['content-type'], + body: data, + }); + }); + message.on('error', (e) => { + reject(e); + }); + }); + } + + private static parseResponse(message: IncomingMessage): T { + if (!message) { + throw Error('empty response'); + } + const { contentType, statusCode } = message; + if (typeof statusCode !== 'number' || statusCode !== 200) { + throw Error(`wrong status code: ${statusCode}`); + } + if (!contentType?.startsWith('application/json')) { + throw Error(`wrong content type: ${contentType}`); + } + const json = JSON.parse(message.body); + return json as T; + } + + private static patchWebSocketDebuggerUrl(host: string, serial: string, socket: string, url: string): string { + if (url) { + const remote = `localabstract:${socket}`; + const path = url.replace(/ws:\/\//, '').replace(fakeHostRe, ''); + return `${host}/${ACTION.PROXY_ADB}/${serial}/${remote}/${path}`; + } + return url; + } + + public static async getRemoteDevtoolsVersion( + host: string, + serial: string, + socket: string, + ): Promise { + const data = await this.createHttpRequest(serial, socket, `${proto}${fakeHost}/json/version`); + if (!data) { + throw Error('Empty response'); + } + const metadata = this.parseResponse(data); + if (metadata.webSocketDebuggerUrl) { + metadata.webSocketDebuggerUrl = this.patchWebSocketDebuggerUrl( + host, + serial, + socket, + metadata.webSocketDebuggerUrl, + ); + } + return metadata; + } + + public static async getRemoteDevtoolsTargets( + host: string, + serial: string, + socket: string, + ): Promise { + const data = await this.createHttpRequest(serial, socket, `${proto}${fakeHost}/json`); + const list = this.parseResponse(data); + if (!list || !list.length) { + return []; + } + return list.map((target) => { + const { devtoolsFrontendUrl, webSocketDebuggerUrl } = target; + if (devtoolsFrontendUrl) { + let temp = devtoolsFrontendUrl; + let bundledOnDevice = false; + const ws = this.patchWebSocketDebuggerUrl(host, serial, socket, webSocketDebuggerUrl); + + if (!temp.startsWith('http')) { + bundledOnDevice = true; + temp = `${proto}${fakeHost}${temp}`; + } + const url = new URL(temp); + // don't use `url.searchParams.set` here, argument will be url-encoded + // chrome-devtools.fronted will now work with url-encoded value + url.searchParams.delete('ws'); + let urlString = url.toString(); + if (urlString.includes('?')) { + urlString += '&'; + } else { + urlString += '?'; + } + urlString += `ws=${ws}`; + + if (bundledOnDevice) { + urlString = urlString.substr(`${proto}${fakeHost}`.length); + } + target.devtoolsFrontendUrl = urlString; + target.webSocketDebuggerUrl = ws; + } + return target; + }); + } + + public static async getRemoteDevtoolsInfo(host: string, serial: string): Promise { + const list = await this.getDevtoolsRemoteList(serial); + if (!list || !list.length) { + const deviceName = await this.getDeviceName(serial); + return { + deviceName, + deviceSerial: serial, + browsers: [], + }; + } + + const all: Promise[] = []; + list.forEach((socket) => { + const v = this.getRemoteDevtoolsVersion(host, serial, socket).catch((error: Error) => { + console.error('getRemoteDevtoolsVersion failed:', error.message); + return { + 'Android-Package': 'string', + Browser: 'string', + 'Protocol-Version': 'string', + 'User-Agent': 'string', + 'V8-Version': 'string', + 'WebKit-Version': 'string', + webSocketDebuggerUrl: 'string', + }; + }); + const t = this.getRemoteDevtoolsTargets(host, serial, socket).catch((error: Error) => { + console.error('getRemoteDevtoolsTargets failed:', error.message); + return []; + }); + const p = Promise.all([v, t]).then((result) => { + const [version, targets] = result; + return { + socket, + version, + targets, + }; + }); + all.push(p); + }); + all.unshift(this.getDeviceName(serial)); + const result = await Promise.all(all); + const deviceName: string = result.shift() as string; + const browsers: RemoteBrowserInfo[] = result as RemoteBrowserInfo[]; + return { + deviceName, + deviceSerial: serial, + browsers, + }; + } + + public static async getDeviceName(serial: string): Promise { + const client = AdbExtended.createClient(); + const props = await client.getProperties(serial); + return props['ro.product.model'] || 'Unknown device'; + } +} diff --git a/src/server/goog-device/Device.ts b/src/server/goog-device/Device.ts new file mode 100644 index 0000000..d3d0c2b --- /dev/null +++ b/src/server/goog-device/Device.ts @@ -0,0 +1,465 @@ +import { AdbExtended } from './adb'; +import AdbKitClient from '@dead50f7/adbkit/lib/adb/client'; +import PushTransfer from '@dead50f7/adbkit/lib/adb/sync/pushtransfer'; +import { spawn } from 'child_process'; +import { NetInterface } from '../../types/NetInterface'; +import { TypedEmitter } from '../../common/TypedEmitter'; +import GoogDeviceDescriptor from '../../types/GoogDeviceDescriptor'; +import { ScrcpyServer } from './ScrcpyServer'; +import { Properties } from './Properties'; +import Timeout = NodeJS.Timeout; + +enum PID_DETECTION { + UNKNOWN, + PIDOF, + GREP_PS, + GREP_PS_A, + LS_PROC, +} + +export interface DeviceEvents { + update: Device; +} + +export class Device extends TypedEmitter { + private static readonly INITIAL_UPDATE_TIMEOUT = 1500; + private static readonly MAX_UPDATES_COUNT = 7; + private connected = true; + private pidDetectionVariant: PID_DETECTION = PID_DETECTION.UNKNOWN; + private client: AdbKitClient; + private properties?: Record; + private spawnServer = true; + private updateTimeoutId?: Timeout; + private updateTimeout = Device.INITIAL_UPDATE_TIMEOUT; + private updateCount = 0; + private throttleTimeoutId?: Timeout; + private lastEmit = 0; + public readonly TAG: string; + public readonly descriptor: GoogDeviceDescriptor; + + constructor(public readonly udid: string, state: string) { + super(); + this.TAG = `[${udid}]`; + this.descriptor = { + udid, + state, + interfaces: [], + pid: -1, + 'wifi.interface': '', + 'ro.build.version.release': '', + 'ro.build.version.sdk': '', + 'ro.product.manufacturer': '', + 'ro.product.model': '', + 'ro.product.cpu.abi': '', + 'last.update.timestamp': 0, + }; + this.client = AdbExtended.createClient(); + this.setState(state); + } + + public setState(state: string): void { + if (state === 'device') { + this.connected = true; + this.properties = undefined; + } else { + this.connected = false; + } + this.descriptor.state = state; + this.emitUpdate(); + this.fetchDeviceInfo(); + } + + public isConnected(): boolean { + return this.connected; + } + + public async getPidOf(processName: string): Promise { + if (!this.connected) { + return; + } + if (this.pidDetectionVariant === PID_DETECTION.UNKNOWN) { + this.pidDetectionVariant = await this.findDetectionVariant(); + } + switch (this.pidDetectionVariant) { + case PID_DETECTION.PIDOF: + return this.pidOf(processName); + case PID_DETECTION.GREP_PS: + return this.grepPs(processName); + case PID_DETECTION.GREP_PS_A: + return this.grepPs_A(processName); + default: + return this.listProc(processName); + } + } + + public killProcess(pid: number): Promise { + const command = `kill ${pid}`; + return this.runShellCommandAdbKit(command); + } + + public async runShellCommandAdb(command: string): Promise { + return new Promise((resolve, reject) => { + const cmd = 'adb'; + const args = ['-s', `${this.udid}`, 'shell', command]; + const adb = spawn(cmd, args, { stdio: ['ignore', 'pipe', 'pipe'] }); + let output = ''; + console.log('执行ADB命令') + adb.stdout.on('data', (data) => { + output += data.toString(); + console.log(this.TAG, `stdout: ${data.toString().replace(/\n$/, '')}`); + }); + + adb.stderr.on('data', (data) => { + console.error(this.TAG, `stderr: ${data}`); + }); + + adb.on('error', (error: Error) => { + console.error(this.TAG, `failed to spawn adb process.\n${error.stack}`); + reject(error); + }); + + adb.on('close', (code) => { + console.log(this.TAG, `adb process (${args.join(' ')}) exited with code ${code}`); + resolve(output); + }); + }); + } + + public async runShellCommandAdbKit(command: string): Promise { + return this.client + .shell(this.udid, command) + .then(AdbExtended.util.readAll) + .then((output: Buffer) => output.toString().trim()); + } + + public async push(contents: string, path: string): Promise { + return this.client.push(this.udid, contents, path); + } + + public async getProperties(): Promise | undefined> { + if (this.properties) { + return this.properties; + } + if (!this.connected) { + return; + } + this.properties = await this.client.getProperties(this.udid); + return this.properties; + } + + private interfacesSort = (a: NetInterface, b: NetInterface): number => { + if (a.name > b.name) { + return 1; + } + if (a.name < b.name) { + return -1; + } + return 0; + }; + + public async getNetInterfaces(): Promise { + if (!this.connected) { + return []; + } + const list: NetInterface[] = []; + const output = await this.runShellCommandAdbKit(`ip -4 -f inet -o a | grep 'scope global'`); + const lines = output.split('\n').filter((i: string) => !!i); + lines.forEach((value: string) => { + const temp = value.split(' ').filter((i: string) => !!i); + const name = temp[1]; + const ipAndMask = temp[3]; + const ipv4 = ipAndMask.split('/')[0]; + list.push({ name, ipv4 }); + }); + return list.sort(this.interfacesSort); + } + + private async pidOf(processName: string): Promise { + return this.runShellCommandAdbKit(`pidof ${processName}`) + .then((output) => { + return output + .split(' ') + .map((pid) => parseInt(pid, 10)) + .filter((num) => !isNaN(num)); + }) + .catch(() => { + return []; + }); + } + + private filterPsOutput(processName: string, output: string): number[] { + const list: number[] = []; + const processes = output.split('\n'); + processes.map((line) => { + const cols = line + .trim() + .split(' ') + .filter((item) => item.length); + if (cols[cols.length - 1] === processName) { + const pid = parseInt(cols[1], 10); + if (!isNaN(pid)) { + list.push(pid); + } + } + }); + return list; + } + + private async grepPs_A(processName: string): Promise { + return this.runShellCommandAdbKit(`ps -A | grep ${processName}`) + .then((output) => { + return this.filterPsOutput(processName, output); + }) + .catch(() => { + return []; + }); + } + + private async grepPs(processName: string): Promise { + return this.runShellCommandAdbKit(`ps | grep ${processName}`) + .then((output) => { + return this.filterPsOutput(processName, output); + }) + .catch(() => { + return []; + }); + } + + private async listProc(processName: string): Promise { + const find = `find /proc -maxdepth 2 -name cmdline 2>/dev/null`; + const lines = await this.runShellCommandAdbKit( + `for L in \`${find}\`; do grep -sae '^${processName}' $L 2>&1 >/dev/null && echo $L; done`, + ); + const re = /\/proc\/([0-9]+)\/cmdline/; + const list: number[] = []; + lines.split('\n').map((line) => { + const trim = line.trim(); + const m = trim.match(re); + if (m) { + list.push(parseInt(m[1], 10)); + } + }); + return list; + } + + private async executedWithoutError(command: string): Promise { + return this.runShellCommandAdbKit(command) + .then((output) => { + const err = parseInt(output, 10); + return err === 0; + }) + .catch(() => { + return false; + }); + } + + private async hasPs(): Promise { + return this.executedWithoutError('ps | grep init 2>&1 >/dev/null; echo $?'); + } + + private async hasPs_A(): Promise { + return this.executedWithoutError('ps -A | grep init 2>&1 >/dev/null; echo $?'); + } + + private async hasPidOf(): Promise { + const ok = await this.executedWithoutError('which pidof 2>&1 >/dev/null && echo $?'); + if (!ok) { + return false; + } + return this.runShellCommandAdbKit('echo $PPID; pidof init') + .then((output) => { + const pids = output.split('\n').filter((a) => a.length); + if (pids.length < 2) { + return false; + } + const parentPid = pids[0].replace('\r', ''); + const list = pids[1].split(' '); + if (list.includes(parentPid)) { + return false; + } + return list.includes('1'); + }) + .catch(() => { + return false; + }); + } + + private async findDetectionVariant(): Promise { + if (await this.hasPidOf()) { + return PID_DETECTION.PIDOF; + } + if (await this.hasPs_A()) { + return PID_DETECTION.GREP_PS_A; + } + if (await this.hasPs()) { + return PID_DETECTION.GREP_PS; + } + return PID_DETECTION.LS_PROC; + } + + private scheduleInfoUpdate(): void { + if (this.updateTimeoutId) { + return; + } + if (++this.updateCount > Device.MAX_UPDATES_COUNT) { + console.error(this.TAG, 'The maximum number of attempts to fetch device info has been reached.'); + return; + } + this.updateTimeoutId = setTimeout(this.fetchDeviceInfo, this.updateTimeout); + this.updateTimeout *= 2; + } + + private fetchDeviceInfo = (): void => { + if (this.connected) { + const propsPromise = this.getProperties().then((props) => { + if (!props) { + return false; + } + let changed = false; + Properties.forEach((propName: keyof GoogDeviceDescriptor) => { + if (props[propName] !== this.descriptor[propName]) { + changed = true; + (this.descriptor[propName] as any) = props[propName]; + } + }); + if (changed) { + this.emitUpdate(); + } + return true; + }); + const netIntPromise = this.updateInterfaces().then((interfaces) => { + return !!interfaces.length; + }); + let pidPromise: Promise; + if (this.spawnServer) { + pidPromise = this.startServer(); + } else { + pidPromise = this.getServerPid(); + } + const serverPromise = pidPromise.then(() => { + return !(this.descriptor.pid === -1 && this.spawnServer); + }); + Promise.all([propsPromise, netIntPromise, serverPromise]) + .then((results) => { + this.updateTimeoutId = undefined; + const failedCount = results.filter((result) => !result).length; + if (!failedCount) { + this.updateCount = 0; + this.updateTimeout = Device.INITIAL_UPDATE_TIMEOUT; + } else { + this.scheduleInfoUpdate(); + } + }) + .catch(() => { + this.updateTimeoutId = undefined; + this.scheduleInfoUpdate(); + }); + } else { + this.updateCount = 0; + this.updateTimeout = Device.INITIAL_UPDATE_TIMEOUT; + this.updateTimeoutId = undefined; + this.emitUpdate(); + } + return; + }; + + private emitUpdate(setUpdateTime = true): void { + const THROTTLE = 300; + const now = Date.now(); + const time = now - this.lastEmit; + if (setUpdateTime) { + this.descriptor['last.update.timestamp'] = now; + } + if (time > THROTTLE) { + this.lastEmit = now; + this.emit('update', this); + return; + } + if (!this.throttleTimeoutId) { + this.throttleTimeoutId = setTimeout(() => { + delete this.throttleTimeoutId; + this.emitUpdate(false); + }, THROTTLE - time); + } + } + + private async getServerPid(): Promise { + const pids = await ScrcpyServer.getServerPid(this); + let pid; + if (!Array.isArray(pids) || !pids.length) { + pid = -1; + } else { + pid = pids[0]; + } + if (this.descriptor.pid !== pid) { + this.descriptor.pid = pid; + this.emitUpdate(); + } + if (pid !== -1) { + return pid; + } else { + return; + } + } + + public async updateInterfaces(): Promise { + return this.getNetInterfaces().then((interfaces) => { + let changed = false; + const old = this.descriptor.interfaces; + if (old.length !== interfaces.length) { + changed = true; + } else { + old.forEach((value, idx) => { + if (value.name !== interfaces[idx].name || value.ipv4 !== interfaces[idx].ipv4) { + changed = true; + } + }); + } + if (changed) { + this.descriptor.interfaces = interfaces; + this.emitUpdate(); + } + return this.descriptor.interfaces; + }); + } + + public async killServer(pid: number): Promise { + this.spawnServer = false; + const realPid = await this.getServerPid(); + if (typeof realPid !== 'number') { + return; + } + if (realPid !== pid) { + console.error(this.TAG, `Requested to kill server with PID ${pid}. Real server PID is ${realPid}.`); + } + try { + const output = await this.killProcess(realPid); + if (output) { + console.log(this.TAG, `kill server: "${output}"`); + } + this.descriptor.pid = -1; + this.emitUpdate(); + } catch (error: any) { + console.error(this.TAG, `Error: ${error.message}`); + throw error; + } + } + + public async startServer(): Promise { + this.spawnServer = true; + const pid = await this.getServerPid(); + if (typeof pid === 'number') { + return pid; + } + try { + const output = await ScrcpyServer.run(this); + if (output) { + console.log(this.TAG, `start server: "${output}"`); + } + return this.getServerPid(); + } catch (error: any) { + console.error(this.TAG, `Error: ${error.message}`); + throw error; + } + } +} diff --git a/src/server/goog-device/Properties.ts b/src/server/goog-device/Properties.ts new file mode 100644 index 0000000..1929d2b --- /dev/null +++ b/src/server/goog-device/Properties.ts @@ -0,0 +1,10 @@ +import GoogDeviceDescriptor from '../../types/GoogDeviceDescriptor'; + +export const Properties: ReadonlyArray = [ + 'ro.product.cpu.abi', + 'ro.product.manufacturer', + 'ro.product.model', + 'ro.build.version.release', + 'ro.build.version.sdk', + 'wifi.interface', +]; diff --git a/src/server/goog-device/ScrcpyServer.ts b/src/server/goog-device/ScrcpyServer.ts new file mode 100644 index 0000000..e7a500d --- /dev/null +++ b/src/server/goog-device/ScrcpyServer.ts @@ -0,0 +1,141 @@ +import '../../../vendor/Genymobile/scrcpy/scrcpy-server.jar'; +import '../../../vendor/Genymobile/scrcpy/LICENSE'; + +import { Device } from './Device'; +import { ARGS_STRING, SERVER_PACKAGE, SERVER_PROCESS_NAME, SERVER_VERSION } from '../../common/Constants'; +import path from 'path'; +import PushTransfer from '@dead50f7/adbkit/lib/adb/sync/pushtransfer'; +import { ServerVersion } from './ServerVersion'; + +const TEMP_PATH = '/data/local/tmp/'; +const FILE_DIR = path.join(__dirname, 'vendor/Genymobile/scrcpy'); +const FILE_NAME = 'scrcpy-server.jar'; +const RUN_COMMAND = `CLASSPATH=${TEMP_PATH}${FILE_NAME} nohup app_process ${ARGS_STRING}`; + +type WaitForPidParams = { tryCounter: number; processExited: boolean; lookPidFile: boolean }; + +export class ScrcpyServer { + private static PID_FILE_PATH = '/data/local/tmp/ws_scrcpy.pid'; + private static async copyServer(device: Device): Promise { + const src = path.join(FILE_DIR, FILE_NAME); + const dst = TEMP_PATH + FILE_NAME; // don't use path.join(): will not work on win host + return device.push(src, dst); + } + + // Important to notice that we first try to read PID from file. + // Checking with `.getServerPid()` will return process id, but process may stop. + // PID file only created after WebSocket server has been successfully started. + private static async waitForServerPid(device: Device, params: WaitForPidParams): Promise { + const { tryCounter, processExited, lookPidFile } = params; + if (processExited) { + return; + } + const timeout = 500 + 100 * tryCounter; + if (lookPidFile) { + const fileName = ScrcpyServer.PID_FILE_PATH; + const content = await device.runShellCommandAdbKit(`test -f ${fileName} && cat ${fileName}`); + if (content.trim()) { + const pid = parseInt(content, 10); + if (pid && !isNaN(pid)) { + const realPid = await this.getServerPid(device); + if (realPid?.includes(pid)) { + return realPid; + } else { + params.lookPidFile = false; + } + } + } + } else { + const list = await this.getServerPid(device); + if (Array.isArray(list) && list.length) { + return list; + } + } + if (++params.tryCounter > 5) { + throw new Error('Failed to start server'); + } + return new Promise((resolve) => { + setTimeout(() => { + resolve(this.waitForServerPid(device, params)); + }, timeout); + }); + } + + public static async getServerPid(device: Device): Promise { + if (!device.isConnected()) { + return; + } + const list = await device.getPidOf(SERVER_PROCESS_NAME); + if (!Array.isArray(list) || !list.length) { + return; + } + const serverPid: number[] = []; + const promises = list.map((pid) => { + return device.runShellCommandAdbKit(`cat /proc/${pid}/cmdline`).then((output) => { + const args = output.split('\0'); + if (!args.length || args[0] !== SERVER_PROCESS_NAME) { + return; + } + let first = args[0]; + while (args.length && first !== SERVER_PACKAGE) { + args.shift(); + first = args[0]; + } + if (args.length < 3) { + return; + } + const versionString = args[1]; + if (versionString === SERVER_VERSION) { + serverPid.push(pid); + } else { + const currentVersion = new ServerVersion(versionString); + if (currentVersion.isCompatible()) { + const desired = new ServerVersion(SERVER_VERSION); + if (desired.gt(currentVersion)) { + console.log( + device.TAG, + `Found old server version running (PID: ${pid}, Version: ${versionString})`, + ); + console.log(device.TAG, 'Perform kill now'); + device.killProcess(pid); + } + } + } + return; + }); + }); + await Promise.all(promises); + return serverPid; + } + + public static async run(device: Device): Promise { + if (!device.isConnected()) { + return; + } + let list: number[] | string | undefined = await this.getServerPid(device); + if (Array.isArray(list) && list.length) { + return list; + } + await this.copyServer(device); + + const params: WaitForPidParams = { tryCounter: 0, processExited: false, lookPidFile: true }; + const runPromise = device.runShellCommandAdb(RUN_COMMAND); + runPromise + .then((out) => { + if (device.isConnected()) { + console.log(device.TAG, 'Server exited:', out); + } + }) + .catch((e) => { + console.log(device.TAG, 'Error:', e.message); + }) + .finally(() => { + params.processExited = true; + }); + list = await Promise.race([runPromise, this.waitForServerPid(device, params)]); + if (Array.isArray(list) && list.length) { + return list; + } + return; + } +} diff --git a/src/server/goog-device/ServerVersion.ts b/src/server/goog-device/ServerVersion.ts new file mode 100644 index 0000000..15bc2aa --- /dev/null +++ b/src/server/goog-device/ServerVersion.ts @@ -0,0 +1,43 @@ +export class ServerVersion { + protected parts: string[] = []; + protected suffix: string; + protected readonly compatible: boolean; + + constructor(public readonly versionString: string) { + const temp = versionString.split('-'); + const main = temp.shift(); + this.suffix = temp.join('-'); + if (main) { + this.parts = main.split('.'); + } + this.compatible = this.suffix.startsWith('ws') && this.parts.length >= 2; + } + public equals(a: ServerVersion | string): boolean { + const versionString = typeof a === 'string' ? a : a.versionString; + return this.versionString === versionString; + } + public gt(a: ServerVersion | string): boolean { + if (this.equals(a)) { + return false; + } + if (typeof a === 'string') { + a = new ServerVersion(a); + } + const minLength = Math.min(this.parts.length, a.parts.length); + for (let i = 0; i < minLength; i++) { + if (this.parts[i] > a.parts[i]) { + return true; + } + } + if (this.parts.length > a.parts.length) { + return true; + } + if (this.parts.length < a.parts.length) { + return false; + } + return this.suffix > a.suffix; + } + public isCompatible(): boolean { + return this.compatible; + } +} diff --git a/src/server/goog-device/adb/ExtendedClient.ts b/src/server/goog-device/adb/ExtendedClient.ts new file mode 100644 index 0000000..28be748 --- /dev/null +++ b/src/server/goog-device/adb/ExtendedClient.ts @@ -0,0 +1,32 @@ +import Client from '@dead50f7/adbkit/lib/adb/client'; +import { ExtendedSync } from './ExtendedSync'; +import { SyncCommand } from './command/host-transport/sync'; +import { Multiplexer } from '../../../packages/multiplexer/Multiplexer'; + +export class ExtendedClient extends Client { + public async pipeSyncService(serial: string): Promise { + const transport = await this.transport(serial); + return new SyncCommand(transport).execute(); + } + + public async pipeReadDir(serial: string, pathString: string, stream: Multiplexer): Promise { + const sync = await this.pipeSyncService(serial); + return sync.pipeReadDir(pathString, stream).then(() => { + sync.end(); + }); + } + + public async pipePull(serial: string, path: string, stream: Multiplexer): Promise { + const sync = await this.pipeSyncService(serial); + return sync.pipePull(path, stream).then(() => { + sync.end(); + }); + } + + public async pipeStat(serial: string, path: string, stream: Multiplexer): Promise { + const sync = await this.pipeSyncService(serial); + return sync.pipeStat(path, stream).then(() => { + sync.end(); + }); + } +} diff --git a/src/server/goog-device/adb/ExtendedSync.ts b/src/server/goog-device/adb/ExtendedSync.ts new file mode 100644 index 0000000..975c32c --- /dev/null +++ b/src/server/goog-device/adb/ExtendedSync.ts @@ -0,0 +1,112 @@ +import Connection from '@dead50f7/adbkit/lib/adb/connection'; +import Parser from '@dead50f7/adbkit/lib/adb/parser'; +import Protocol from '@dead50f7/adbkit/lib/adb/protocol'; +import { Multiplexer } from '../../../packages/multiplexer/Multiplexer'; + +export class ExtendedSync { + private parser: Parser; + + constructor(private connection: Connection) { + this.connection = connection; + this.parser = this.connection.parser; + } + + public async pipeReadDir(path: string, stream: Multiplexer): Promise { + const readNext = async (): Promise => { + const reply = await this.parser.readAscii(4); + switch (reply) { + case Protocol.DENT: + const stat = await this.parser.readBytes(16); + const namelen = stat.readUInt32LE(12); + const name = await this.parser.readBytes(namelen); + console.log('发送的参数16'); + + stream.send(Buffer.concat([Buffer.from(reply), stat, name])); + return readNext(); + case Protocol.DONE: + await this.parser.readBytes(16); + stream.close(0); + return; + case Protocol.FAIL: + return this._readError(stream); + default: + return this.parser.unexpected(reply, 'DENT, DONE or FAIL'); + } + }; + this._sendCommandWithArg(Protocol.LIST, path); + return readNext(); + } + + public pipePull(path: string, stream: Multiplexer): Promise { + this._sendCommandWithArg(Protocol.RECV, `${path}`); + return this._readData(stream); + } + + public async pipeStat(path: string, stream: Multiplexer): Promise { + this._sendCommandWithArg(Protocol.STAT, `${path}`); + const reply = await this.parser.readAscii(4); + switch (reply) { + case Protocol.STAT: + const stat = await this.parser.readBytes(12); + console.log('发送的参数17'); + + stream.send(Buffer.concat([Buffer.from(reply), stat])); + stream.close(1000); + break; + case Protocol.FAIL: + return this._readError(stream); + default: + return this.parser.unexpected(reply, 'STAT or FAIL'); + } + } + + private _readData(stream: Multiplexer): Promise { + const readNext = async (): Promise => { + const reply = await this.parser.readAscii(4); + switch (reply) { + case Protocol.DATA: + const lengthData = await this.parser.readBytes(4); + const length = lengthData.readUInt32LE(0); + const data = await this.parser.readBytes(length); + console.log('发送的参数18'); + + stream.send(Buffer.concat([Buffer.from(reply), data])); + return readNext(); + case Protocol.DONE: + await this.parser.readBytes(4); + stream.close(1000); + return; + case Protocol.FAIL: + return this._readError(stream); + default: + return this.parser.unexpected(reply, 'DATA, DONE or FAIL'); + } + }; + return readNext(); + } + + private _sendCommandWithArg(cmd: string, arg: string): Connection { + const arglen = Buffer.byteLength(arg, 'utf-8'); + const payload = Buffer.alloc(cmd.length + 4 + arglen); + let pos = 0; + payload.write(cmd, pos, cmd.length); + pos += cmd.length; + payload.writeUInt32LE(arglen, pos); + pos += 4; + payload.write(arg, pos); + return this.connection.write(payload); + } + + private async _readError(stream: Multiplexer): Promise { + const length = await this.parser.readBytes(4); + const message = await this.parser.readAscii(length.readUInt32LE(0)); + stream.close(4000, message); + await this.parser.end(); + return; + } + + public end(): ExtendedSync { + this.connection.end(); + return this; + } +} diff --git a/src/server/goog-device/adb/command/host-transport/sync.ts b/src/server/goog-device/adb/command/host-transport/sync.ts new file mode 100644 index 0000000..9932b71 --- /dev/null +++ b/src/server/goog-device/adb/command/host-transport/sync.ts @@ -0,0 +1,20 @@ +import Protocol from '@dead50f7/adbkit/lib/adb/protocol'; +import Command from '@dead50f7/adbkit/lib/adb/command'; +import { ExtendedSync } from '../../ExtendedSync'; +import Bluebird from 'bluebird'; + +export class SyncCommand extends Command { + execute(): Bluebird { + this._send('sync:'); + return this.parser.readAscii(4).then((reply) => { + switch (reply) { + case Protocol.OKAY: + return new ExtendedSync(this.connection); + case Protocol.FAIL: + return this.parser.readError(); + default: + return this.parser.unexpected(reply, 'OKAY or FAIL'); + } + }); + } +} diff --git a/src/server/goog-device/adb/index.ts b/src/server/goog-device/adb/index.ts new file mode 100644 index 0000000..17a639f --- /dev/null +++ b/src/server/goog-device/adb/index.ts @@ -0,0 +1,28 @@ +import Adb from '@dead50f7/adbkit/lib/adb'; +import { ExtendedClient } from './ExtendedClient'; +import { ClientOptions } from '@dead50f7/adbkit/lib/ClientOptions'; + +interface Options { + host?: string; + port?: number; + bin?: string; +} + +export class AdbExtended extends Adb { + static createClient(options: Options = {}): ExtendedClient { + const opts: ClientOptions = { + bin: options.bin, + host: options.host || process.env.ADB_HOST || '127.0.0.1', + port: options.port || 0, + }; + if (!opts.port) { + const port = parseInt(process.env.ADB_PORT || '', 10); + if (!isNaN(port)) { + opts.port = port; + } else { + opts.port = 5037; + } + } + return new ExtendedClient(opts); + } +} diff --git a/src/server/goog-device/filePush/FilePushReader.ts b/src/server/goog-device/filePush/FilePushReader.ts new file mode 100644 index 0000000..51e0d31 --- /dev/null +++ b/src/server/goog-device/filePush/FilePushReader.ts @@ -0,0 +1,255 @@ +import { ReadableOptions } from 'stream'; +import { CommandControlMessage, FilePushState } from '../../../app/controlMessage/CommandControlMessage'; +import { FilePushResponseStatus } from '../../../app/googDevice/filePush/FilePushResponseStatus'; +import PushTransfer from '@dead50f7/adbkit/lib/adb/sync/pushtransfer'; +import { ReadStream } from './ReadStream'; +import { AdbExtended } from '../adb'; + +enum State { + INITIAL, + NEW, + START, + APPEND, + FINISH, + CANCEL, +} + +export class FilePushReader { + private static fileId = 1; + private static maxId = 4294967295; // 2^32 - 1 + + public static handle(serial: string, channel: WebSocket): FilePushReader { + return new FilePushReader(serial, channel); + } + + public static getNextId(): number { + this.fileId++; + if (this.fileId > this.maxId) { + this.fileId = 1; + } + return this.fileId; + } + + private static createResponse(id: number, code: number): Buffer { + const buffer = Buffer.alloc(3); + let offset = 0; + offset = buffer.writeInt16BE(id, offset); + buffer.writeInt8(code, offset); + return buffer; + } + + public readStream?: ReadStream; + private pushTransfer?: PushTransfer; + private fileName = ''; + private fileSize = 0; + private pushId = -1; + private state: State = State.INITIAL; + private createStreamPromiseMap: Map> = new Map(); + private disposed = false; + + constructor(private readonly serial: string, private readonly channel: WebSocket) { + channel.addEventListener('message', this.onMessage); + channel.addEventListener('close', this.onClose); + } + + private verifyId(id: number): boolean { + if (id !== this.pushId) { + this.closeWithError(FilePushResponseStatus.ERROR_UNKNOWN_ID); + return false; + } + return true; + } + + private sendResponse(status: FilePushResponseStatus): void { + if (this.channel.readyState === this.channel.CLOSING || this.channel.readyState === this.channel.CLOSED) { + return; + } + console.log('发送的参数19'); + + this.channel.send(FilePushReader.createResponse(this.pushId, status)); + } + + private closeWithError(code: number, message?: string): void { + this.channel.removeEventListener('message', this.onMessage); + this.channel.removeEventListener('close', this.onClose); + this.channel.close(4000 - code, message); + this.release(); + } + + private onMessage = async (event: MessageEvent): Promise => { + const command = CommandControlMessage.pushFileCommandFromBuffer(Buffer.from(event.data)); + + const { id, state } = command; + switch (state) { + case FilePushState.NEW: + if (this.state !== State.INITIAL) { + this.closeWithError(FilePushResponseStatus.ERROR_INVALID_STATE); + return; + } + this.state = State.NEW; + this.pushId = FilePushReader.getNextId(); + this.sendResponse(FilePushResponseStatus.NEW_PUSH_ID); + break; + case FilePushState.START: + if (!this.verifyId(id)) { + return; + } + if (this.state !== State.NEW) { + this.closeWithError(FilePushResponseStatus.ERROR_INVALID_STATE); + return; + } + const { fileName, fileSize } = command; + if (!fileName) { + this.closeWithError(FilePushResponseStatus.ERROR_INVALID_NAME); + return; + } + if (!fileSize) { + this.closeWithError(FilePushResponseStatus.ERROR_INCORRECT_SIZE); + return; + } + this.fileName = fileName; + this.fileSize = fileSize; + this.state = State.START; + this.sendResponse(FilePushResponseStatus.NO_ERROR); + break; + case FilePushState.APPEND: + if (!this.verifyId(id)) { + return; + } + const { chunk } = command; + if (!chunk || !chunk.length) { + this.closeWithError(FilePushResponseStatus.ERROR_INCORRECT_SIZE); + return; + } + if (this.state === State.START) { + const promise = this.createStream(chunk); + this.createStreamPromiseMap.set(id, promise); + await promise; + this.createStreamPromiseMap.delete(id); + this.state = State.APPEND; + return; + } else if (this.state !== State.APPEND) { + this.closeWithError(FilePushResponseStatus.ERROR_INVALID_STATE); + return; + } + this.readStream?.push(chunk); + break; + case FilePushState.FINISH: + if (!this.verifyId(id)) { + return; + } + const promise = this.createStreamPromiseMap.get(id); + if (promise) { + await promise; + } + if (this.state !== State.APPEND) { + this.closeWithError(FilePushResponseStatus.ERROR_INVALID_STATE); + return; + } + this.state = State.FINISH; + if (this.readStream) { + this.readStream.push(null); + this.readStream.close(); + this.readStream = undefined; + } + break; + case FilePushState.CANCEL: + if (!this.verifyId(id)) { + return; + } + this.state = State.CANCEL; + if (this.readStream) { + this.readStream.push(null); + this.readStream.close(); + this.readStream = undefined; + } + if (this.pushTransfer) { + this.pushTransfer.cancel(); + } + break; + default: + if (!this.verifyId(id)) { + return; + } + this.closeWithError(FilePushResponseStatus.ERROR_INVALID_STATE); + } + }; + + private async createStream(chunk: Buffer): Promise { + const opts = { + construct: (callback: (error?: Error | null) => void) => { + callback(null); + }, + read: () => { + if (!this.readStream) { + return; + } + if (this.readStream.bytesRead > this.fileSize) { + console.error(`bytesRead (${this.readStream.bytesRead}) > fileSize (${this.fileSize})`); + } + this.sendResponse(FilePushResponseStatus.NO_ERROR); + }, + } as ReadableOptions; // FIXME: incorrect type in @type/node@12. fixed in @type/node@16 + this.readStream = new ReadStream(this.fileName, opts); + this.readStream.push(chunk); + const client = AdbExtended.createClient(); + this.pushTransfer = await client.push(this.serial, this.readStream, this.fileName); + client.on('error', (error: Error) => { + console.error(`Client error (${this.serial} | ${this.fileName}):`, error.message); + this.closeWithError(FilePushResponseStatus.ERROR_OTHER, error.message); + }); + this.pushTransfer.on('error', this.onPushError); + this.pushTransfer.on('end', this.onPushEnd); + this.pushTransfer.on('cancel', this.onPushCancel); + } + + private onClose = (): void => { + this.release(); + }; + + private onPushError = (error: Error) => { + this.closeWithError(FilePushResponseStatus.ERROR_OTHER, error.message); + }; + + private onPushEnd = () => { + if (this.state === State.FINISH) { + this.sendResponse(FilePushResponseStatus.NO_ERROR); + this.release(); + } else { + this.closeWithError(FilePushResponseStatus.ERROR_INVALID_STATE); + } + }; + + private onPushCancel = () => { + if (this.state === State.CANCEL) { + this.sendResponse(FilePushResponseStatus.NO_ERROR); + this.release(); + } else { + this.closeWithError(FilePushResponseStatus.ERROR_INVALID_STATE); + } + }; + + public release(): void { + if (this.disposed) { + return; + } + this.disposed = true; + this.createStreamPromiseMap.clear(); + if (this.readStream) { + this.readStream.close(); + this.readStream = undefined; + } + if (this.pushTransfer) { + this.pushTransfer.off('error', this.onPushError); + this.pushTransfer.off('end', this.onPushEnd); + this.pushTransfer.off('cancel', this.onPushCancel); + this.pushTransfer = undefined; + } + this.channel.removeEventListener('message', this.onMessage); + this.channel.removeEventListener('close', this.onClose); + const { readyState, CLOSED, CLOSING } = this.channel; + if (readyState !== CLOSED && readyState !== CLOSING) { + this.channel.close(); + } + } +} diff --git a/src/server/goog-device/filePush/ReadStream.ts b/src/server/goog-device/filePush/ReadStream.ts new file mode 100644 index 0000000..b31c20d --- /dev/null +++ b/src/server/goog-device/filePush/ReadStream.ts @@ -0,0 +1,24 @@ +import { Readable, ReadableOptions } from 'stream'; + +export class ReadStream extends Readable { + private _bytesRead = 0; + constructor(private readonly _path: string, opts?: ReadableOptions) { + super(opts); + } + public get bytesRead(): number { + return this._bytesRead; + } + public get path(): string | Buffer { + return this._path; + } + public push(chunk: any, encoding?: string): boolean { + if (chunk) { + this._bytesRead += chunk.length; + } + return super.push(chunk, encoding); + } + + public close(): void { + this.destroy(); + } +} diff --git a/src/server/goog-device/mw/DeviceTracker.ts b/src/server/goog-device/mw/DeviceTracker.ts new file mode 100644 index 0000000..47d8961 --- /dev/null +++ b/src/server/goog-device/mw/DeviceTracker.ts @@ -0,0 +1,92 @@ +import WS from 'ws'; +import { Mw, RequestParameters } from '../../mw/Mw'; +import { ControlCenterCommand } from '../../../common/ControlCenterCommand'; +import { ControlCenter } from '../services/ControlCenter'; +import { ACTION } from '../../../common/Action'; +import GoogDeviceDescriptor from '../../../types/GoogDeviceDescriptor'; +import { DeviceTrackerEvent } from '../../../types/DeviceTrackerEvent'; +import { DeviceTrackerEventList } from '../../../types/DeviceTrackerEventList'; +import { Multiplexer } from '../../../packages/multiplexer/Multiplexer'; +import { ChannelCode } from '../../../common/ChannelCode'; + +export class DeviceTracker extends Mw { + public static readonly TAG = 'DeviceTracker'; + public static readonly type = 'android'; + private adt: ControlCenter = ControlCenter.getInstance(); + private readonly id: string; + + public static processChannel(ws: Multiplexer, code: string): Mw | undefined { + if (code !== ChannelCode.GTRC) { + return; + } + return new DeviceTracker(ws); + } + + public static processRequest(ws: WS, params: RequestParameters): DeviceTracker | undefined { + if (params.action !== ACTION.GOOG_DEVICE_LIST) { + return; + } + return new DeviceTracker(ws); + } + + constructor(ws: WS | Multiplexer) { + super(ws); + + this.id = this.adt.getId(); + this.adt + .init() + .then(() => { + this.adt.on('device', this.sendDeviceMessage); + this.buildAndSendMessage(this.adt.getDevices()); + }) + .catch((error: Error) => { + console.error(`[${DeviceTracker.TAG}] Error: ${error.message}`); + }); + } + + private sendDeviceMessage = (device: GoogDeviceDescriptor): void => { + const data: DeviceTrackerEvent = { + device, + id: this.id, + name: this.adt.getName(), + }; + this.sendMessage({ + id: -1, + type: 'device', + data, + }); + }; + + private buildAndSendMessage = (list: GoogDeviceDescriptor[]): void => { + const data: DeviceTrackerEventList = { + list, + id: this.id, + name: this.adt.getName(), + }; + this.sendMessage({ + id: -1, + type: 'devicelist', + data, + }); + }; + + protected onSocketMessage(event: WS.MessageEvent): void { + console.log("接收到的参数5", event.data) + + let command: ControlCenterCommand; + try { + command = ControlCenterCommand.fromJSON(event.data.toString()); + } catch (error: any) { + console.error(`[${DeviceTracker.TAG}], Received message: ${event.data}. Error: ${error?.message}`); + return; + } + this.adt.runCommand(command).catch((e) => { + console.error(`[${DeviceTracker.TAG}], Received message: ${event.data}. Error: ${e.message}`); + }); + } + + public release(): void { + super.release(); + this.adt.off('device', this.sendDeviceMessage); + } +} diff --git a/src/server/goog-device/mw/FileListing.ts b/src/server/goog-device/mw/FileListing.ts new file mode 100644 index 0000000..e853cbf --- /dev/null +++ b/src/server/goog-device/mw/FileListing.ts @@ -0,0 +1,101 @@ +import { Mw } from '../../mw/Mw'; +import { AdbUtils } from '../AdbUtils'; +import Util from '../../../app/Util'; +import Protocol from '@dead50f7/adbkit/lib/adb/protocol'; +import { Multiplexer } from '../../../packages/multiplexer/Multiplexer'; +import { ChannelCode } from '../../../common/ChannelCode'; +import { FilePushReader } from '../filePush/FilePushReader'; + +export class FileListing extends Mw { + public static readonly TAG = 'FileListing'; + protected name = 'FileListing'; + + public static processChannel(ws: Multiplexer, code: string, data: ArrayBuffer): Mw | undefined { + if (code !== ChannelCode.FSLS) { + return; + } + if (!data || data.byteLength < 4) { + return; + } + const buffer = Buffer.from(data); + const length = buffer.readInt32LE(0); + const serial = Util.utf8ByteArrayToString(buffer.slice(4, 4 + length)); + return new FileListing(ws, serial); + } + + constructor(ws: Multiplexer, private readonly serial: string) { + super(ws); + ws.on('channel', (params) => { + FileListing.handleNewChannel(this.serial, params.channel, params.data); + }); + } + + protected sendMessage = (): void => { + throw Error('Do not use this method. You must send data over channels'); + }; + + protected onSocketMessage(): void { + // Nothing here. All communication are performed over the channels. See `handleNewChannel` below. + } + + private static handleNewChannel(serial: string, channel: Multiplexer, arrayBuffer: ArrayBuffer): void { + const data = Buffer.from(arrayBuffer); + if (data.length < 4) { + console.error(`[${FileListing.TAG}]`, `Invalid message. Too short (${data.length})`); + return; + } + let offset = 0; + const cmd = Util.utf8ByteArrayToString(data.slice(offset, 4)); + offset += 4; + switch (cmd) { + case Protocol.LIST: + case Protocol.STAT: + case Protocol.RECV: + const length = data.readUInt32LE(offset); + offset += 4; + const pathBuffer = data.slice(offset, offset + length); + const pathString = Util.utf8ByteArrayToString(pathBuffer); + FileListing.handle(cmd, serial, pathString, channel).catch((error: Error) => { + console.error(`[${FileListing.TAG}]`, error.message); + }); + break; + case Protocol.SEND: + FilePushReader.handle(serial, channel); + break; + default: + console.error(`[${FileListing.TAG}]`, `Invalid message. Wrong command (${cmd})`); + channel.close(4001, `Invalid message. Wrong command (${cmd})`); + break; + } + } + + private static async handle(cmd: string, serial: string, pathString: string, channel: Multiplexer): Promise { + try { + if (cmd === Protocol.STAT) { + return AdbUtils.pipeStatToStream(serial, pathString, channel); + } + if (cmd === Protocol.LIST) { + return AdbUtils.pipeReadDirToStream(serial, pathString, channel); + } + if (cmd === Protocol.RECV) { + return AdbUtils.pipePullFileToStream(serial, pathString, channel); + } + } catch (error: any) { + FileListing.sendError(error?.message, channel); + } + } + + private static sendError(message: string, channel: Multiplexer): void { + if (channel.readyState === channel.OPEN) { + const length = Buffer.byteLength(message, 'utf-8'); + const buf = Buffer.alloc(4 + 4 + length); + let offset = buf.write(Protocol.FAIL, 'ascii'); + offset = buf.writeUInt32LE(length, offset); + buf.write(message, offset, 'utf-8'); + console.log('发送的参数20'); + + channel.send(buf); + channel.close(); + } + } +} diff --git a/src/server/goog-device/mw/RemoteDevtools.ts b/src/server/goog-device/mw/RemoteDevtools.ts new file mode 100644 index 0000000..eb5f9ea --- /dev/null +++ b/src/server/goog-device/mw/RemoteDevtools.ts @@ -0,0 +1,70 @@ +import WS from 'ws'; +import { Mw, RequestParameters } from '../../mw/Mw'; +import { RemoteDevtoolsCommand } from '../../../types/RemoteDevtoolsCommand'; +import { AdbUtils } from '../AdbUtils'; +import { ACTION } from '../../../common/Action'; + +export class RemoteDevtools extends Mw { + public static readonly TAG = 'RemoteDevtools'; + public static processRequest(ws: WS, params: RequestParameters): RemoteDevtools | undefined { + const { action, request, url } = params; + if (action !== ACTION.DEVTOOLS) { + return; + } + const host = request.headers['host']; + const udid = url.searchParams.get('udid'); + if (!udid) { + ws.close(4003, `[${this.TAG}] Invalid value "${udid}" for "udid" parameter`); + return; + } + if (typeof host !== 'string' || !host) { + ws.close(4003, `[${this.TAG}] Invalid value "${host}" in "Host" header`); + return; + } + return new RemoteDevtools(ws, host, udid); + } + constructor(protected ws: WS, private readonly host: string, private readonly udid: string) { + super(ws); + } + protected onSocketMessage(event: WS.MessageEvent): void { + console.log("接收到的参数6", event.data) + + let data; + try { + data = JSON.parse(event.data.toString()); + } catch (error: any) { + console.log(`Received message: ${event.data}`); + return; + } + if (!data || !data.command) { + console.log(`Received message: ${event.data}`); + return; + } + const command = data.command; + switch (command) { + case RemoteDevtoolsCommand.LIST_DEVTOOLS: { + AdbUtils.getRemoteDevtoolsInfo(this.host, this.udid) + .then((list) => { + console.log('发送的参数21'); + + this.ws.send( + JSON.stringify({ + type: ACTION.DEVTOOLS, + data: list, + }), + ); + }) + .catch((e) => { + const { message } = e; + console.error(`Command: "${command}", error: ${message}`); + console.log('发送的参数22'); + + this.ws.send(JSON.stringify({ command, error: message })); + }); + break; + } + default: + console.warn(`Unsupported command: "${data.command}"`); + } + } +} diff --git a/src/server/goog-device/mw/RemoteShell.ts b/src/server/goog-device/mw/RemoteShell.ts new file mode 100644 index 0000000..1f9663e --- /dev/null +++ b/src/server/goog-device/mw/RemoteShell.ts @@ -0,0 +1,169 @@ +import WS from 'ws'; +import { Mw, RequestParameters } from '../../mw/Mw'; +import * as pty from 'node-pty'; +import * as os from 'os'; +import { IPty } from 'node-pty'; +import { Message } from '../../../types/Message'; +import { XtermClientMessage, XtermServiceParameters } from '../../../types/XtermMessage'; +import { ACTION } from '../../../common/Action'; +import { Multiplexer } from '../../../packages/multiplexer/Multiplexer'; +import { ChannelCode } from '../../../common/ChannelCode'; + +const OS_WINDOWS = os.platform() === 'win32'; +const USE_BINARY = !OS_WINDOWS; +const EVENT_TYPE_SHELL = 'shell'; + +export class RemoteShell extends Mw { + public static readonly TAG = 'RemoteShell'; + private term?: IPty; + private initialized = false; + private timeoutString: NodeJS.Timeout | null = null; + private timeoutBuffer: NodeJS.Timeout | null = null; + private terminated = false; + private closeCode = 1000; + private closeReason = ''; + + public static processChannel(ws: Multiplexer, code: string): Mw | undefined { + if (code !== ChannelCode.SHEL) { + return; + } + return new RemoteShell(ws); + } + + public static processRequest(ws: WS, params: RequestParameters): RemoteShell | undefined { + if (params.action !== ACTION.SHELL) { + return; + } + return new RemoteShell(ws); + } + + constructor(protected ws: WS | Multiplexer) { + super(ws); + } + + public createTerminal(params: XtermServiceParameters): IPty { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const env = Object.assign({}, process.env) as any; + env['COLORTERM'] = 'truecolor'; + const { cols = 80, rows = 24 } = params; + const cwd = env.PWD || '/'; + const file = OS_WINDOWS ? 'adb.exe' : 'adb'; + const term = pty.spawn(file, ['-s', params.udid, 'shell'], { + name: 'xterm-256color', + cols, + rows, + cwd, + env, + encoding: null, + }); + const send = USE_BINARY ? this.bufferUtf8(5) : this.buffer(5); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore Documentation is incorrect for `encoding: null` + term.on('data', send); + term.on('exit', (code: number) => { + if (code === 0) { + this.closeCode = 1000; + } else { + this.closeCode = 4500; + } + this.closeReason = `[${[RemoteShell.TAG]}] terminal process exited with code: ${code}`; + if (this.timeoutString || this.timeoutBuffer) { + this.terminated = true; + } else { + this.ws.close(this.closeCode, this.closeReason); + } + }); + return term; + } + + protected onSocketMessage(event: WS.MessageEvent): void { + console.log("接收到的参数7", event.data) + + if (this.initialized) { + if (!this.term) { + return; + } + return this.term.write(event.data as string); + } + let data; + try { + data = JSON.parse(event.data.toString()); + } catch (error: any) { + console.error(`[${RemoteShell.TAG}]`, error?.message); + return; + } + this.handleMessage(data as Message).catch((error: Error) => { + console.error(`[${RemoteShell.TAG}]`, error.message); + }); + } + + private handleMessage = async (message: Message): Promise => { + if (message.type !== EVENT_TYPE_SHELL) { + return; + } + const data: XtermClientMessage = message.data as XtermClientMessage; + const { type } = data; + if (type === 'start') { + this.term = this.createTerminal(data); + this.initialized = true; + } + if (type === 'stop') { + this.release(); + } + }; + + // string message buffering + private buffer(timeout: number): (data: string) => void { + let s = ''; + return (data: string) => { + s += data; + if (!this.timeoutString) { + this.timeoutString = setTimeout(() => { + console.log('发送的参数23'); + + this.ws.send(s); + s = ''; + this.timeoutString = null; + if (this.terminated) { + this.ws.close(this.closeCode, this.closeReason); + } + }, timeout); + } + }; + } + + private bufferUtf8(timeout: number): (data: Buffer) => void { + let buffer: Buffer[] = []; + let length = 0; + return (data: Buffer) => { + buffer.push(data); + length += data.length; + if (!this.timeoutBuffer) { + this.timeoutBuffer = setTimeout(() => { + console.log('发送的参数24'); + + this.ws.send(Buffer.concat(buffer, length)); + buffer = []; + this.timeoutBuffer = null; + length = 0; + if (this.terminated) { + this.ws.close(this.closeCode, this.closeReason); + } + }, timeout); + } + }; + } + + public release(): void { + super.release(); + if (this.timeoutBuffer) { + clearTimeout(this.timeoutBuffer); + } + if (this.timeoutString) { + clearTimeout(this.timeoutString); + } + if (this.term) { + this.term.kill(); + } + } +} diff --git a/src/server/goog-device/mw/WebsocketProxyOverAdb.ts b/src/server/goog-device/mw/WebsocketProxyOverAdb.ts new file mode 100644 index 0000000..cc605cb --- /dev/null +++ b/src/server/goog-device/mw/WebsocketProxyOverAdb.ts @@ -0,0 +1,64 @@ +import { WebsocketProxy } from '../../mw/WebsocketProxy'; +import { AdbUtils } from '../AdbUtils'; +import WS from 'ws'; +import { RequestParameters } from '../../mw/Mw'; +import { ACTION } from '../../../common/Action'; + +export class WebsocketProxyOverAdb extends WebsocketProxy { + public static processRequest(ws: WS, params: RequestParameters): WebsocketProxy | undefined { + const { action, url } = params; + let udid: string | null = ''; + let remote: string | null = ''; + let path: string | null = ''; + let isSuitable = false; + if (action === ACTION.PROXY_ADB) { + isSuitable = true; + remote = url.searchParams.get('remote'); + udid = url.searchParams.get('udid'); + path = url.searchParams.get('path'); + } + if (url && url.pathname) { + const temp = url.pathname.split('/'); + // Shortcut for action=proxy, without query string + if (temp.length >= 4 && temp[0] === '' && temp[1] === ACTION.PROXY_ADB) { + isSuitable = true; + temp.splice(0, 2); + udid = decodeURIComponent(temp.shift() || ''); + remote = decodeURIComponent(temp.shift() || ''); + path = temp.join('/') || '/'; + } + } + if (!isSuitable) { + return; + } + if (typeof remote !== 'string' || !remote) { + ws.close(4003, `[${this.TAG}] Invalid value "${remote}" for "remote" parameter`); + return; + } + if (typeof udid !== 'string' || !udid) { + ws.close(4003, `[${this.TAG}] Invalid value "${udid}" for "udid" parameter`); + return; + } + if (path && typeof path !== 'string') { + ws.close(4003, `[${this.TAG}] Invalid value "${path}" for "path" parameter`); + return; + } + return this.createProxyOverAdb(ws, udid, remote, path); + } + + public static createProxyOverAdb(ws: WS, udid: string, remote: string, path?: string | null): WebsocketProxy { + const service = new WebsocketProxy(ws); + AdbUtils.forward(udid, remote) + .then((port) => { + console.log('ws链接1', `ws://127.0.0.1:${port}${path ? path : ''}`) + + return service.init(`ws://127.0.0.1:${port}${path ? path : ''}`); + }) + .catch((e) => { + const msg = `[${this.TAG}] Failed to start service: ${e.message}`; + console.error(msg); + ws.close(4005, msg); + }); + return service; + } +} diff --git a/src/server/goog-device/services/ControlCenter.ts b/src/server/goog-device/services/ControlCenter.ts new file mode 100644 index 0000000..168852f --- /dev/null +++ b/src/server/goog-device/services/ControlCenter.ts @@ -0,0 +1,180 @@ +import { TrackerChangeSet } from '@dead50f7/adbkit/lib/TrackerChangeSet'; +import { Device } from '../Device'; +import { Service } from '../../services/Service'; +import AdbKitClient from '@dead50f7/adbkit/lib/adb/client'; +import { AdbExtended } from '../adb'; +import GoogDeviceDescriptor from '../../../types/GoogDeviceDescriptor'; +import Tracker from '@dead50f7/adbkit/lib/adb/tracker'; +import Timeout = NodeJS.Timeout; +import { BaseControlCenter } from '../../services/BaseControlCenter'; +import { ControlCenterCommand } from '../../../common/ControlCenterCommand'; +import * as os from 'os'; +import * as crypto from 'crypto'; +import { DeviceState } from '../../../common/DeviceState'; + +export class ControlCenter extends BaseControlCenter implements Service { + private static readonly defaultWaitAfterError = 1000; + private static instance?: ControlCenter; + + private initialized = false; + private client: AdbKitClient = AdbExtended.createClient(); + private tracker?: Tracker; + private waitAfterError = 1000; + private restartTimeoutId?: Timeout; + private deviceMap: Map = new Map(); + private descriptors: Map = new Map(); + private readonly id: string; + + protected constructor() { + super(); + const idString = `goog|${os.hostname()}|${os.uptime()}`; + this.id = crypto.createHash('md5').update(idString).digest('hex'); + } + + public static getInstance(): ControlCenter { + if (!this.instance) { + this.instance = new ControlCenter(); + } + return this.instance; + } + + public static hasInstance(): boolean { + return !!ControlCenter.instance; + } + + private restartTracker = (): void => { + if (this.restartTimeoutId) { + return; + } + console.log(`Device tracker is down. Will try to restart in ${this.waitAfterError}ms`); + this.restartTimeoutId = setTimeout(() => { + this.stopTracker(); + this.waitAfterError *= 1.2; + this.init(); + }, this.waitAfterError); + }; + + private onChangeSet = (changes: TrackerChangeSet): void => { + this.waitAfterError = ControlCenter.defaultWaitAfterError; + if (changes.added.length) { + for (const item of changes.added) { + const { id, type } = item; + this.handleConnected(id, type); + } + } + if (changes.removed.length) { + for (const item of changes.removed) { + const { id } = item; + this.handleConnected(id, DeviceState.DISCONNECTED); + } + } + if (changes.changed.length) { + for (const item of changes.changed) { + const { id, type } = item; + this.handleConnected(id, type); + } + } + }; + + private onDeviceUpdate = (device: Device): void => { + const { udid, descriptor } = device; + this.descriptors.set(udid, descriptor); + this.emit('device', descriptor); + }; + + private handleConnected(udid: string, state: string): void { + let device = this.deviceMap.get(udid); + if (device) { + device.setState(state); + } else { + device = new Device(udid, state); + device.on('update', this.onDeviceUpdate); + this.deviceMap.set(udid, device); + } + } + + public async init(): Promise { + if (this.initialized) { + return; + } + this.tracker = await this.startTracker(); + const list = await this.client.listDevices(); + list.forEach((device) => { + const { id, type } = device; + this.handleConnected(id, type); + }); + this.initialized = true; + } + + private async startTracker(): Promise { + if (this.tracker) { + return this.tracker; + } + const tracker = await this.client.trackDevices(); + tracker.on('changeSet', this.onChangeSet); + tracker.on('end', this.restartTracker); + tracker.on('error', this.restartTracker); + return tracker; + } + + private stopTracker(): void { + if (this.tracker) { + this.tracker.off('changeSet', this.onChangeSet); + this.tracker.off('end', this.restartTracker); + this.tracker.off('error', this.restartTracker); + this.tracker.end(); + this.tracker = undefined; + } + this.tracker = undefined; + this.initialized = false; + } + + public getDevices(): GoogDeviceDescriptor[] { + return Array.from(this.descriptors.values()); + } + + public getDevice(udid: string): Device | undefined { + return this.deviceMap.get(udid); + } + + public getId(): string { + return this.id; + } + + public getName(): string { + return `aDevice Tracker [${os.hostname()}]`; + } + + public start(): Promise { + return this.init().catch((e) => { + console.error(`Error: Failed to init "${this.getName()}". ${e.message}`); + }); + } + + public release(): void { + this.stopTracker(); + } + + public async runCommand(command: ControlCenterCommand): Promise { + const udid = command.getUdid(); + const device = this.getDevice(udid); + if (!device) { + console.error(`Device with udid:"${udid}" not found`); + return; + } + const type = command.getType(); + switch (type) { + case ControlCenterCommand.KILL_SERVER: + await device.killServer(command.getPid()); + return; + case ControlCenterCommand.START_SERVER: + await device.startServer(); + return; + case ControlCenterCommand.UPDATE_INTERFACES: + await device.updateInterfaces(); + return; + default: + throw new Error(`Unsupported command: "${type}"`); + } + } +} diff --git a/src/server/index.ts b/src/server/index.ts new file mode 100644 index 0000000..cd571fa --- /dev/null +++ b/src/server/index.ts @@ -0,0 +1,171 @@ +import '../../LICENSE'; +import * as readline from 'readline'; +import { Config } from './Config'; +import { HttpServer } from './services/HttpServer'; +import { WebSocketServer } from './services/WebSocketServer'; +import { Service, ServiceClass } from './services/Service'; +import { MwFactory } from './mw/Mw'; +import { WebsocketProxy } from './mw/WebsocketProxy'; +import { HostTracker } from './mw/HostTracker'; +import { WebsocketMultiplexer } from './mw/WebsocketMultiplexer'; + +const servicesToStart: ServiceClass[] = [HttpServer, WebSocketServer]; + +// MWs that accept WebSocket +const mwList: MwFactory[] = [WebsocketProxy, WebsocketMultiplexer]; + +// MWs that accept Multiplexer +const mw2List: MwFactory[] = [HostTracker]; + +const runningServices: Service[] = []; +const loadPlatformModulesPromises: Promise[] = []; + +const config = Config.getInstance(); + +/// #if INCLUDE_GOOG +async function loadGoogModules() { + const { ControlCenter } = await import('./goog-device/services/ControlCenter'); + const { DeviceTracker } = await import('./goog-device/mw/DeviceTracker'); + const { WebsocketProxyOverAdb } = await import('./goog-device/mw/WebsocketProxyOverAdb'); + + if (config.runLocalGoogTracker) { + mw2List.push(DeviceTracker); + } + + if (config.announceLocalGoogTracker) { + HostTracker.registerLocalTracker(DeviceTracker); + } + + servicesToStart.push(ControlCenter); + + /// #if INCLUDE_ADB_SHELL + const { RemoteShell } = await import('./goog-device/mw/RemoteShell'); + mw2List.push(RemoteShell); + /// #endif + + /// #if INCLUDE_DEV_TOOLS + const { RemoteDevtools } = await import('./goog-device/mw/RemoteDevtools'); + mwList.push(RemoteDevtools); + /// #endif + + /// #if INCLUDE_FILE_LISTING + const { FileListing } = await import('./goog-device/mw/FileListing'); + mw2List.push(FileListing); + /// #endif + + mwList.push(WebsocketProxyOverAdb); +} +loadPlatformModulesPromises.push(loadGoogModules()); +/// #endif + +/// #if INCLUDE_APPL +async function loadApplModules() { + const { ControlCenter } = await import('./appl-device/services/ControlCenter'); + const { DeviceTracker } = await import('./appl-device/mw/DeviceTracker'); + const { WebDriverAgentProxy } = await import('./appl-device/mw/WebDriverAgentProxy'); + + // Hack to reduce log-level of appium libs + const { default: npmlog } = await import('npmlog'); + npmlog.level = 'warn'; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (global as any)._global_npmlog = npmlog; + + if (config.runLocalApplTracker) { + mw2List.push(DeviceTracker); + } + + if (config.announceLocalApplTracker) { + HostTracker.registerLocalTracker(DeviceTracker); + } + + servicesToStart.push(ControlCenter); + + /// #if USE_QVH_SERVER + const { QVHStreamProxy } = await import('./appl-device/mw/QVHStreamProxy'); + mw2List.push(QVHStreamProxy); + /// #endif + mw2List.push(WebDriverAgentProxy); +} +loadPlatformModulesPromises.push(loadApplModules()); +/// #endif + +Promise.all(loadPlatformModulesPromises) + .then(() => { + return servicesToStart.map((serviceClass: ServiceClass) => { + const service = serviceClass.getInstance(); + runningServices.push(service); + return service.start(); + }); + }) + .then(() => { + const wsService = WebSocketServer.getInstance(); + mwList.forEach((mwFactory: MwFactory) => { + wsService.registerMw(mwFactory); + }); + + mw2List.forEach((mwFactory: MwFactory) => { + WebsocketMultiplexer.registerMw(mwFactory); + }); + + if (process.platform === 'win32') { + readline + .createInterface({ + input: process.stdin, + output: process.stdout, + }) + .on('SIGINT', exit); + } + + process.on('SIGINT', exit); + process.on('SIGTERM', exit); + }) + .catch((error) => { + console.error(error.message); + exit('1'); + }); + +let interrupted = false; +function exit(signal: string) { + console.log(`\nReceived signal ${signal}`); + if (interrupted) { + console.log('Force exit'); + process.exit(0); + return; + } + interrupted = true; + runningServices.forEach((service: Service) => { + const serviceName = service.getName(); + console.log(`Stopping ${serviceName} ...`); + service.release(); + }); +} + + +export function start() { + return Promise.all(loadPlatformModulesPromises) + .then(() => { + return servicesToStart.map((serviceClass: ServiceClass) => { + const service = serviceClass.getInstance(); + runningServices.push(service); + return service.start(); + }); + }) + .then(() => { + const wsService = WebSocketServer.getInstance(); + mwList.forEach((mwFactory: MwFactory) => { + wsService.registerMw(mwFactory); + }); + + mw2List.forEach((mwFactory: MwFactory) => { + WebsocketMultiplexer.registerMw(mwFactory); + }); + + process.on('SIGINT', exit); + process.on('SIGTERM', exit); + }); +} + +// // 如果是直接运行,则自动启动 +// if (require.main === module) { +// start(); +// } \ No newline at end of file diff --git a/src/server/mw/HostTracker.ts b/src/server/mw/HostTracker.ts new file mode 100644 index 0000000..08e921f --- /dev/null +++ b/src/server/mw/HostTracker.ts @@ -0,0 +1,64 @@ +import WS from 'ws'; +import { Mw } from './Mw'; +import { Config } from '../Config'; +import { MessageError, MessageHosts, MessageType } from '../../common/HostTrackerMessage'; +import { HostItem } from '../../types/Configuration'; +import { Multiplexer } from '../../packages/multiplexer/Multiplexer'; +import { ChannelCode } from '../../common/ChannelCode'; + +export interface TrackerClass { + type: string; +} + +export class HostTracker extends Mw { + public static readonly TAG = 'HostTracker'; + private static localTrackers: Set = new Set(); + private static remoteHostItems?: HostItem[]; + + public static processChannel(ws: Multiplexer, code: string): Mw | undefined { + if (code !== ChannelCode.HSTS) { + return; + } + return new HostTracker(ws); + } + + public static registerLocalTracker(tracker: TrackerClass): void { + this.localTrackers.add(tracker); + } + + constructor(ws: Multiplexer) { + super(ws); + + const local: { type: string }[] = Array.from(HostTracker.localTrackers.keys()).map((tracker) => { + return { type: tracker.type }; + }); + if (!HostTracker.remoteHostItems) { + const config = Config.getInstance(); + HostTracker.remoteHostItems = Array.from(config.getHostList()); + } + const message: MessageHosts = { + id: -1, + type: MessageType.HOSTS, + data: { + local, + remote: HostTracker.remoteHostItems, + }, + }; + this.sendMessage(message); + } + + protected onSocketMessage(event: WS.MessageEvent): void { + console.log("接收到的参数8", event.data) + + const message: MessageError = { + id: -1, + type: MessageType.ERROR, + data: `Unsupported message: "${event.data.toString()}"`, + }; + this.sendMessage(message); + } + + public release(): void { + super.release(); + } +} diff --git a/src/server/mw/MjpegProxyFactory.ts b/src/server/mw/MjpegProxyFactory.ts new file mode 100644 index 0000000..d3b4e41 --- /dev/null +++ b/src/server/mw/MjpegProxyFactory.ts @@ -0,0 +1,46 @@ +import { Request, Response } from 'express'; +import MjpegProxy from 'node-mjpeg-proxy'; +import { WdaRunner } from '../appl-device/services/WDARunner'; +import { WdaStatus } from '../../common/WdaStatus'; + +export class MjpegProxyFactory { + private static instances: Map = new Map(); + proxyRequest = async (req: Request, res: Response): Promise => { + const { udid } = req.params; + if (!udid) { + res.destroy(); + return; + } + let proxy = MjpegProxyFactory.instances.get(udid); + if (!proxy) { + const wda = await WdaRunner.getInstance(udid); + if (!wda.isStarted()) { + // FIXME: `wda.start()` should resolve on 'started' + const startPromise = new Promise((resolve) => { + const onStatusChange = ({ status }: { status: WdaStatus }) => { + if (status === WdaStatus.STARTED) { + wda.off('status-change', onStatusChange); + resolve(undefined); + } + }; + wda.on('status-change', onStatusChange); + }); + await wda.start(); + await startPromise; + } + const port = wda.mjpegPort; + const url = `http://127.0.0.1:${port}`; + proxy = new MjpegProxy(url); + proxy.on('streamstop', (): void => { + wda.release(); + MjpegProxyFactory.instances.delete(udid); + }); + proxy.on('error', (data: { msg: Error; url: string }): void => { + console.error('msg: ' + data.msg); + console.error('url: ' + data.url); + }); + MjpegProxyFactory.instances.set(udid, proxy); + } + proxy.proxyRequest(req, res); + }; +} diff --git a/src/server/mw/Mw.ts b/src/server/mw/Mw.ts new file mode 100644 index 0000000..e8df56c --- /dev/null +++ b/src/server/mw/Mw.ts @@ -0,0 +1,56 @@ +import { Message } from '../../types/Message'; +import * as http from 'http'; +import { Multiplexer } from '../../packages/multiplexer/Multiplexer'; +import WS from 'ws'; + +export type RequestParameters = { + request: http.IncomingMessage; + url: URL; + action: string; +}; + +export interface MwFactory { + processRequest(ws: WS, params: RequestParameters): Mw | undefined; + processChannel(ws: Multiplexer, code: string, data?: ArrayBuffer): Mw | undefined; +} + +export abstract class Mw { + protected name = 'Mw'; + + public static processChannel(_ws: Multiplexer, _code: string, _data?: ArrayBuffer): Mw | undefined { + return; + } + + public static processRequest(_ws: WS, _params: RequestParameters): Mw | undefined { + return; + } + + protected constructor(protected readonly ws: WS | Multiplexer) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + this.ws.addEventListener('message', this.onSocketMessage.bind(this)); + this.ws.addEventListener('close', this.onSocketClose.bind(this)); + } + + protected abstract onSocketMessage(event: WS.MessageEvent): void; + + protected sendMessage = (data: Message): void => { + if (this.ws.readyState !== this.ws.OPEN) { + return; + } + // console.log('发送的参数25'); + + this.ws.send(JSON.stringify(data)); + }; + + protected onSocketClose(): void { + this.release(); + } + + public release(): void { + const { readyState, CLOSED, CLOSING } = this.ws; + if (readyState !== CLOSED && readyState !== CLOSING) { + this.ws.close(); + } + } +} diff --git a/src/server/mw/WebsocketMultiplexer.ts b/src/server/mw/WebsocketMultiplexer.ts new file mode 100644 index 0000000..db62bc9 --- /dev/null +++ b/src/server/mw/WebsocketMultiplexer.ts @@ -0,0 +1,78 @@ +import { Mw, MwFactory, RequestParameters } from './Mw'; +import { ACTION } from '../../common/Action'; +import { Multiplexer } from '../../packages/multiplexer/Multiplexer'; +import WS from 'ws'; +import Util from '../../app/Util'; + +export class WebsocketMultiplexer extends Mw { + public static readonly TAG = 'WebsocketMultiplexer'; + private static mwFactories: Set = new Set(); + private multiplexer: Multiplexer; + // private mw: Set = new Set(); + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + public static processRequest(ws: WS, params: RequestParameters): WebsocketMultiplexer | undefined { + const { action } = params; + if (action !== ACTION.MULTIPLEX) { + return; + } + return this.createMultiplexer(ws); + } + + public static createMultiplexer(ws: WS): WebsocketMultiplexer { + const service = new WebsocketMultiplexer(ws); + service.init().catch((e) => { + const msg = `[${this.TAG}] Failed to start service: ${e.message}`; + console.error(msg); + ws.close(4005, msg); + }); + return service; + } + + constructor(ws: WS) { + super(ws); + this.multiplexer = Multiplexer.wrap(ws as unknown as WebSocket); + } + + public async init(): Promise { + this.multiplexer.addEventListener('channel', this.onChannel); + } + + public static registerMw(mwFactory: MwFactory): void { + this.mwFactories.add(mwFactory); + } + + protected onSocketMessage(_event: WS.MessageEvent): void { + // console.log("接收到的参数10", _event.data) + + // none; + } + + protected onChannel({ channel, data }: { channel: Multiplexer; data: ArrayBuffer }): void { + let processed = false; + for (const mwFactory of WebsocketMultiplexer.mwFactories.values()) { + try { + const code = Util.utf8ByteArrayToString(Buffer.from(data).slice(0, 4)); + const buffer = data.byteLength > 4 ? data.slice(4) : undefined; + const mw = mwFactory.processChannel(channel, code, buffer); + if (mw) { + processed = true; + // this.mw.add(mw); + // const remove = () => { + // this.mw.delete(mw); + // }; + // channel.addEventListener('close', remove); + // channel.addEventListener('error', remove); + } + } finally { + } + } + if (!processed) { + channel.close(4002, `[${WebsocketMultiplexer.TAG}] Unsupported request`); + } + } + + public release(): void { + super.release(); + } +} diff --git a/src/server/mw/WebsocketProxy copy.ts b/src/server/mw/WebsocketProxy copy.ts new file mode 100644 index 0000000..b01bb9b --- /dev/null +++ b/src/server/mw/WebsocketProxy copy.ts @@ -0,0 +1,824 @@ +import { Mw, RequestParameters } from './Mw'; +import WS from 'ws'; +import { ACTION } from '../../common/Action'; +import { Multiplexer } from '../../packages/multiplexer/Multiplexer'; +import { watchNode, dump, dumpNode } from '../../utils/dumpHierarchy'; + +export class WebsocketProxy extends Mw { + public static readonly TAG = 'WebsocketProxy'; + private remoteSocket?: WS; + private released = false; + private storage: WS.MessageEvent[] = []; + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + public static processRequest(ws: WS, params: RequestParameters): WebsocketProxy | undefined { + const { action, url } = params; + if (action !== ACTION.PROXY_WS) { + return; + } + const wsString = url.searchParams.get('ws'); + if (!wsString) { + ws.close(4003, `[${this.TAG}] Invalid value "${ws}" for "ws" parameter`); + return; + } + return this.createProxy(ws, wsString); + } + + public static createProxy(ws: WS | Multiplexer, remoteUrl: string): WebsocketProxy { + const service = new WebsocketProxy(ws); + service.init(remoteUrl).catch((e) => { + const msg = `[${this.TAG}] Failed to start service: ${e.message}`; + console.error(msg); + ws.close(4005, msg); + }); + return service; + } + + constructor(ws: WS | Multiplexer) { + super(ws); + } + + public async init(remoteUrl: string): Promise { + this.name = `[${WebsocketProxy.TAG}{$${remoteUrl}}]`; + const remoteSocket = new WS(remoteUrl); + remoteSocket.onopen = () => { + this.remoteSocket = remoteSocket; + this.flush(); + }; + remoteSocket.onmessage = (event) => { + if (this.ws && this.ws.readyState === this.ws.OPEN) { + if (Array.isArray(event.data)) { + // console.log('接收的参数27'); + console.log('接收数组数据'); + + event.data.forEach((data) => this.ws.send(data)); + } else { + this.ws.send(event.data); + // console.log('接收转发'); + + } + } + }; + remoteSocket.onclose = (e) => { + if (this.ws.readyState === this.ws.OPEN) { + this.ws.close(e.wasClean ? 1000 : 4010); + } + }; + remoteSocket.onerror = (e) => { + if (this.ws.readyState === this.ws.OPEN) { + this.ws.close(4011, e.message); + } + }; + } + + private flush(): void { + if (this.remoteSocket) { + while (this.storage.length) { + const event = this.storage.shift(); + if (event && event.data) { + console.log('发送的参数26'); + + this.remoteSocket.send(event.data); + } + } + if (this.released) { + this.remoteSocket.close(); + } + } + this.storage.length = 0; + } + + protected onSocketMessage(event: WS.MessageEvent): void { + //速度优化解决方案:将首次点击成功后,将消息发送给前端储存起来,后续点击直接进行像素点 触发 + + // 新增消息解析逻辑 + let message = event.data; + // 判断是否为打开应用指令 + // console.log('接收到的信息', message); + if (typeof message === 'string') { // 抖音包名 + + try { + // 尝试将字符串解析为对象 + const parsedMessage = JSON.parse(message) as { udid: string, action: string, index: number, resourceId: string, type: string, num: number }; // 添加index字段 + console.log('执行指令', parsedMessage); + // 执行ADB命令 + const { exec } = require('child_process'); + const fs = require('fs'); + const xml2js = require('xml2js'); + const path = require('path'); + + if (parsedMessage.action === 'openDY') { + exec(`adb -s ${parsedMessage.udid} shell am start -n com.zhiliaoapp.musically/com.ss.android.ugc.aweme.splash.SplashActivity`, + // (error: Error, stdout: string, stderr: string) => { + (error: Error) => { + if (error) { + console.error('执行失败:', error); + this.ws.send(JSON.stringify({ + status: 'error', + message: error.message + })); + return; + } + this.ws.send(JSON.stringify({ type: parsedMessage.action, status: 'success', udid: parsedMessage.udid, index: parsedMessage.index })); + console.log('抖音已启动'); + // this.ws.send(JSON.stringify({ + // status: 'success', + // data: stdout + // })); + }); + } else if (parsedMessage.action === 'killNow') { + // exec(`adb -s ${parsedMessage.udid} shell input swipe 500 800 500 300 100`, + exec(`for /f "tokens=3 delims=/ " %a in ('adb -s ${parsedMessage.udid} shell dumpsys window ^| findstr "mCurrentFocus"') do adb shell am force-stop %a `, + // (error: Error, stdout: string, stderr: string) => { + (error: Error) => { + if (error) { + console.error('执行失败:', error); + this.ws.send(JSON.stringify({ + status: 'error', + message: error.message + })); + return; + } + this.ws.send(JSON.stringify({ type: parsedMessage.action, status: 'success', udid: parsedMessage.udid, index: parsedMessage.index })); + console.log('kill 当前页面进程,关闭软件'); + // this.ws.send(JSON.stringify({ + // status: 'success', + // data: stdout + // })); + }); + } else if (parsedMessage.action === 'install') { + // exec(`adb -s ${parsedMessage.udid} shell input swipe 500 800 500 300 100`, + exec(`adb -s ${parsedMessage.udid} install ${parsedMessage.resourceId}`, + // (error: Error, stdout: string, stderr: string) => { + (error: Error) => { + if (error) { + console.error('执行失败:', error); + this.ws.send(JSON.stringify({ + status: 'error', + message: error.message + })); + return; + } + console.log('安装app'); + // this.ws.send(JSON.stringify({ + // status: 'success', + // data: stdout + // })); + }); + } else if (parsedMessage.action === 'pushFile') { + // exec(`adb -s ${parsedMessage.udid} shell input swipe 500 800 500 300 100`, + exec(`adb -s ${parsedMessage.udid} push "${parsedMessage.resourceId}" /sdcard/`, + // (error: Error, stdout: string, stderr: string) => { + (error: Error) => { + if (error) { + console.error('执行失败:', error); + this.ws.send(JSON.stringify({ + status: 'error', + message: error.message + })); + return; + } + console.log('安装app'); + // this.ws.send(JSON.stringify({ + // status: 'success', + // data: stdout + // })); + }); + } else if (parsedMessage.action === 'slideDown') { + exec(`adb -s ${parsedMessage.udid} shell input swipe 500 800 500 300 100`, + // (error: Error, stdout: string, stderr: string) => { + (error: Error) => { + if (error) { + console.error('执行失败:', error); + this.ws.send(JSON.stringify({ + status: 'error', + message: error.message + })); + return; + } + console.log('滑动向下'); + // this.ws.send(JSON.stringify({ + // status: 'success', + // data: stdout + // })); + }); + } else if (parsedMessage.action === 'slideUp') { + exec(`adb -s ${parsedMessage.udid} shell input swipe 500 300 500 800 100`, + // (error: Error, stdout: string, stderr: string) => { + (error: Error) => { + if (error) { + console.error('执行失败:', error); + this.ws.send(JSON.stringify({ + status: 'error', + message: error.message + })); + return; + } + console.log('滑动向上'); + // this.ws.send(JSON.stringify({ + // status: 'success', + // data: stdout + // })); + }); + } else if (parsedMessage.action === 'slideRight') { + exec(`adb -s ${parsedMessage.udid} shell input swipe 500 1000 10 1000 50`, + // (error: Error, stdout: string, stderr: string) => { + (error: Error) => { + if (error) { + console.error('执行失败:', error); + this.ws.send(JSON.stringify({ + status: 'error', + message: error.message + })); + return; + } + console.log('滑动向上'); + // this.ws.send(JSON.stringify({ + // status: 'success', + // data: stdout + // })); + }); + } else if (parsedMessage.action === 'getSize') { + exec(`adb -s ${parsedMessage.udid} shell wm size`, + // (error: Error, stdout: string, stderr: string) => { + (error: Error, stdout: string) => { + console.log(stdout); + if (error) { + console.error('执行失败:', error); + this.ws.send( + JSON.stringify({ + status: 'error', + message: error.message, + }) + ); + return; + } + + // 解析屏幕尺寸(例如 "Physical size: 1080x1920") + const sizeMatch = stdout.match(/Physical size:\s*(\d+)x(\d+)/); + if (sizeMatch) { + const width = parseInt(sizeMatch[1]); + const height = parseInt(sizeMatch[2]); + console.log(`设备 ${parsedMessage.udid} 的屏幕尺寸: ${width}x${height}`); + + this.ws.send( + JSON.stringify({ + status: 'success', + device: parsedMessage.udid, + action: parsedMessage.action, + index: parsedMessage.index, + width, + height, + }) + ); + } else { + const errorMsg = '无法解析屏幕尺寸信息'; + console.error(errorMsg); + this.ws.send( + JSON.stringify({ + status: 'error', + message: errorMsg, + }) + ); + } + }) + } else if (parsedMessage.action === 'click') { + const time = parsedMessage.type == 'clickSysMesage' || parsedMessage.type == 'CommentText' ? 5 : 10 + + watchNode(parsedMessage.udid, parsedMessage.resourceId, time).then((data) => { + console.log('节点出现', data) + if (data.found) { + + try { + // const targetResourceId = parsedMessage.resourceId + + + //节点列表 + const targetNodeList = data.nodes; + + console.log('节点列表', targetNodeList) + let bounds = targetNodeList[0].bounds; // 选择节点列表中的第一个节点 + if (parsedMessage.type == 'clickCopy' || parsedMessage.type == 'search') { + console.log('最后一个节点') + + bounds = targetNodeList[targetNodeList.length - 1].bounds; // 选择节点列表中的最后一个节点 + } else if (parsedMessage.type == 'clickCopyList') { + console.log('获取所有节点的对话信息') + + // targetNodeList.forEach((item: any) => { + // console.log(item.text) + // }) + const result = parseMessages(targetNodeList) + console.log(result, 'result') + this.ws.send(JSON.stringify({ type: parsedMessage.type, status: 'success', message: result, udid: parsedMessage.udid, index: parsedMessage.index })); + + return; + } else if (parsedMessage.type == 'clickCopyText') { + console.log('最后2个节点') + + bounds = targetNodeList[2].bounds; // 选择节点列表中的第3个节点 + } else if (parsedMessage.type == 'getmesNum') { + + bounds = targetNodeList[0].bounds; // 选择节点列表中的第一个节点 + const childCount = targetNodeList[0].childCount + if (childCount == 2) { + this.ws.send(JSON.stringify({ type: parsedMessage.type, status: 'success', message: 0, udid: parsedMessage.udid, index: parsedMessage.index })); + return; + } else if (childCount == 3) { + this.ws.send(JSON.stringify({ type: parsedMessage.type, status: 'success', message: 1, udid: parsedMessage.udid, index: parsedMessage.index })); + // return; + } + } else if (parsedMessage.type == 'clickMesage') { + const targetNode = targetNodeList.find((item: any) => + (item as any).childCount > 0); + console.log('targetNode', targetNode) + if (targetNode) { + // console.log('noticeMes执行1', noticeMes) + bounds = targetNode.bounds; // 使用第一个满足条件的元素 + // } else if (noticeMes[0]) { + // console.log('noticeMes执行2', noticeMes) + // bounds = noticeMes[0].bounds; // 使用第一个满足条件的元素 + } else { + this.ws.send(JSON.stringify({ status: 'error', type: parsedMessage.type, message: '没有消息' })); + return; // 没有符合条件的元素则返回 + } + } else if (parsedMessage.type == 'isVideoAndLive') { + + const VideoType = targetNodeList[0]['content-desc']; // 选择节点列表中的第一个节点 + console.log('类型', VideoType) + this.ws.send(JSON.stringify({ type: parsedMessage.type, status: 'success', message: VideoType, udid: parsedMessage.udid, index: parsedMessage.index })); + return; + } else if (parsedMessage.type == 'isHost') { + console.log(parsedMessage.index, '关注的内容', targetNodeList[0]) + if (targetNodeList[0]) { + console.log('有关注') + this.ws.send(JSON.stringify({ type: parsedMessage.type, status: 'success', message: 1, udid: parsedMessage.udid, index: parsedMessage.index })); + + } + // this.ws.send(JSON.stringify({ type: parsedMessage.type, status: 'success', message: VideoType, udid: parsedMessage.udid, index: parsedMessage.index })); + return; + } else if (parsedMessage.type == 'hostVideo') { + console.log('第' + parsedMessage.num + '个节点') + bounds = targetNodeList[parsedMessage.num].bounds; // 选择节点列表中的第一个节点 + } else { + console.log('第一个节点') + bounds = targetNodeList[0].bounds; // 选择节点列表中的第一个节点 + } + // const match = bounds.match(/\[(\d+),(\d+)]\[(\d+),(\d+)]/); + if (bounds) { + const x1 = parseInt(bounds.left); + const y1 = parseInt(bounds.top); + const x2 = parseInt(bounds.right); + const y2 = parseInt(bounds.bottom); + const clickX = Math.floor((x1 + x2) / 2); + const clickY = Math.floor((y1 + y2) / 2); + console.log(clickX, clickY, '坐标'); + // Step 4: Click + console.log(parsedMessage.udid, '设备id'); + if (parsedMessage.type == 'clickCopy') { + console.log('复制坐标'); + this.ws.send(JSON.stringify({ type: parsedMessage.type, status: 'success', message: '坐标返回', udid: parsedMessage.udid, index: parsedMessage.index, x: clickX, y: clickY })); + return; + } + exec(`adb -s ${parsedMessage.udid} shell input tap ${clickX} ${clickY}`, (clickErr: Error | null) => { + if (clickErr) { + // this.ws.send(JSON.stringify({ status: 'error', message: '点击失败:' + clickErr.message })); + console.log('点击失败:' + clickErr.message); + return; + } + console.log('点击了按钮'); + this.ws.send(JSON.stringify({ type: parsedMessage.type, status: 'success', message: '点击成功', udid: parsedMessage.udid, index: parsedMessage.index, x: clickX, y: clickY })); + }); + } else { + // this.ws.send(JSON.stringify({ status: 'error', message: '解析坐标失败' })); + console.log('解析坐标失败'); + } + } catch (e) { + if (e instanceof Error) { + // this.ws.send(JSON.stringify({ status: 'error', message: '查找节点失败:' + e.message })); + console.log('查找节点失败:' + e.message) + } else { + // this.ws.send(JSON.stringify({ status: 'error', message: '查找节点失败:未知错误' })); + console.log('查找节点失败:未知错误') + } + } + + } else { + if (parsedMessage.type == 'isHost') { + this.ws.send(JSON.stringify({ type: parsedMessage.type, status: 'success', message: 0, udid: parsedMessage.udid, index: parsedMessage.index })); + return; + } else { + this.ws.send(JSON.stringify({ status: 'error', type: parsedMessage.type, message: '未找到按钮' })); + console.log('未找到按钮') + } + } + + }) + .catch(err => { + console.error('Error:', err); + + }); + + + return; + + + + + const tempXmlPath = path.join(__dirname, `dump_${parsedMessage.udid}.xml`); + + // Step 1: Dump UI XML + exec(`adb -s ${parsedMessage.udid} shell uiautomator dump --compressed /sdcard/window_dump.xml`, (err: Error | null, stdout: string, stderr: string) => { + if (err) { + this.ws.send(JSON.stringify({ status: 'error', type: parsedMessage.type, message: 'Dump失败:' + err.message })); + return; + } + console.log(stdout, 'stdout') + console.log(stderr, 'stderr') + if (stderr) { + this.ws.send(JSON.stringify({ status: 'error', type: parsedMessage.type, message: '手机无空闲:' + stderr })); + return; + } + console.log('Dump成功'); + // Step 2: Pull XML + exec(`adb -s ${parsedMessage.udid} pull /sdcard/window_dump.xml ${tempXmlPath}`, (pullErr: Error | null) => { + if (pullErr) { + this.ws.send(JSON.stringify({ status: 'error', type: parsedMessage.type, message: '拉取XML失败:' + pullErr.message })); + return; + } + + if (parsedMessage.type == 'test') { + console.log('test') + return; + } + + console.log('拉取XML成功'); + // Step 3: Parse XML + const xml = fs.readFileSync(tempXmlPath, 'utf-8'); + + xml2js.parseString(xml, (parseErr: Error, result: any) => { + // console.log(result.hierarchy.node, 'result.hierarchy.node'); + if (parseErr) { + // this.ws.send(JSON.stringify({ status: 'error', message: '解析XML失败:' + parseErr.message })); + console.log('解析XML失败:') + return; + } + + try { + const nodes = result.hierarchy.node; + const targetResourceId = parsedMessage.resourceId + const findNodes = (nodeList: any[]): any => { + let foundNodes: any[] = []; + for (const node of nodeList) { + if (node.$?.['resource-id'] === targetResourceId) { + foundNodes.push(node); + } + if (node.node) { + foundNodes = foundNodes.concat(findNodes(node.node)); + } + } + return foundNodes; + }; + + const targetNodeList = findNodes(nodes); + if (!targetNodeList[0]) { + if (parsedMessage.type == 'isHost') { + this.ws.send(JSON.stringify({ type: parsedMessage.type, status: 'success', message: 0, udid: parsedMessage.udid, index: parsedMessage.index })); + + return; + } else { + this.ws.send(JSON.stringify({ status: 'error', type: parsedMessage.type, message: '未找到按钮' })); + console.log('未找到按钮') + + } + + } + console.log('节点列表', targetNodeList) + let bounds = targetNodeList[0].$.bounds; // 选择节点列表中的第一个节点 + if (parsedMessage.type == 'clickCopy' || parsedMessage.type == 'search') { + console.log('最后一个节点') + + bounds = targetNodeList[targetNodeList.length - 1].$.bounds; // 选择节点列表中的最后一个节点 + } else if (parsedMessage.type == 'clickCopyList') { + console.log('获取所有节点的对话信息') + + // targetNodeList.forEach((item: any) => { + // console.log(item.$.text) + // }) + const result = parseMessages(targetNodeList) + console.log(result, 'result') + this.ws.send(JSON.stringify({ type: parsedMessage.type, status: 'success', message: result, udid: parsedMessage.udid, index: parsedMessage.index })); + + return; + } else if (parsedMessage.type == 'clickCopyText') { + console.log('最后2个节点') + + bounds = targetNodeList[2].$.bounds; // 选择节点列表中的第3个节点 + } else if (parsedMessage.type == 'getmesNum') { + + bounds = targetNodeList[0].$.bounds; // 选择节点列表中的第一个节点 + const childCount = targetNodeList[0].node ? targetNodeList[0].node.length : 0; + if (childCount == 2) { + this.ws.send(JSON.stringify({ type: parsedMessage.type, status: 'success', message: 0, udid: parsedMessage.udid, index: parsedMessage.index })); + return; + } else if (childCount == 3) { + this.ws.send(JSON.stringify({ type: parsedMessage.type, status: 'success', message: 1, udid: parsedMessage.udid, index: parsedMessage.index })); + return; + } + } else if (parsedMessage.type == 'isVideoAndLive') { + + const VideoType = targetNodeList[0].$['content-desc']; // 选择节点列表中的第一个节点 + console.log('类型', VideoType) + this.ws.send(JSON.stringify({ type: parsedMessage.type, status: 'success', message: VideoType, udid: parsedMessage.udid, index: parsedMessage.index })); + return; + } else if (parsedMessage.type == 'isHost') { + console.log(parsedMessage.index, '关注的内容', targetNodeList[0]) + if (targetNodeList[0]) { + console.log('有关注') + this.ws.send(JSON.stringify({ type: parsedMessage.type, status: 'success', message: 1, udid: parsedMessage.udid, index: parsedMessage.index })); + + } + // this.ws.send(JSON.stringify({ type: parsedMessage.type, status: 'success', message: VideoType, udid: parsedMessage.udid, index: parsedMessage.index })); + return; + } else if (parsedMessage.type == 'hostVideo') { + console.log('第' + parsedMessage.num + '个节点') + bounds = targetNodeList[parsedMessage.num].$.bounds; // 选择节点列表中的第一个节点 + } else { + console.log('第一个节点') + bounds = targetNodeList[0].$.bounds; // 选择节点列表中的第一个节点 + } + const match = bounds.match(/\[(\d+),(\d+)]\[(\d+),(\d+)]/); + if (match) { + const x1 = parseInt(match[1]); + const y1 = parseInt(match[2]); + const x2 = parseInt(match[3]); + const y2 = parseInt(match[4]); + const clickX = Math.floor((x1 + x2) / 2); + const clickY = Math.floor((y1 + y2) / 2); + console.log(clickX, clickY, '坐标'); + // Step 4: Click + console.log(parsedMessage.udid, '设备id'); + if (parsedMessage.type == 'clickCopy') { + console.log('复制坐标'); + this.ws.send(JSON.stringify({ type: parsedMessage.type, status: 'success', message: '坐标返回', udid: parsedMessage.udid, index: parsedMessage.index, x: clickX, y: clickY })); + return; + } + exec(`adb -s ${parsedMessage.udid} shell input tap ${clickX} ${clickY}`, (clickErr: Error | null) => { + if (clickErr) { + // this.ws.send(JSON.stringify({ status: 'error', message: '点击失败:' + clickErr.message })); + console.log('点击失败:' + clickErr.message); + return; + } + console.log('点击了按钮'); + this.ws.send(JSON.stringify({ type: parsedMessage.type, status: 'success', message: '点击成功', udid: parsedMessage.udid, index: parsedMessage.index, x: clickX, y: clickY })); + }); + } else { + // this.ws.send(JSON.stringify({ status: 'error', message: '解析坐标失败' })); + console.log('解析坐标失败'); + } + } catch (e) { + if (e instanceof Error) { + // this.ws.send(JSON.stringify({ status: 'error', message: '查找节点失败:' + e.message })); + console.log('查找节点失败:' + e.message) + } else { + // this.ws.send(JSON.stringify({ status: 'error', message: '查找节点失败:未知错误' })); + console.log('查找节点失败:未知错误') + } + } + }); + }); + }); + } else if (parsedMessage.action === 'dump') { + dumpNode(parsedMessage.udid, parsedMessage.resourceId).then((data) => { + console.log('节点出现', data) + if (data.found) { + + try { + // const targetResourceId = parsedMessage.resourceId + + + //节点列表 + const targetNodeList = data.nodes; + + console.log('节点列表', targetNodeList) + let bounds = targetNodeList[0].bounds; // 选择节点列表中的第一个节点 + if (parsedMessage.type == 'clickCopy' || parsedMessage.type == 'search') { + console.log('最后一个节点') + + bounds = targetNodeList[targetNodeList.length - 1].bounds; // 选择节点列表中的最后一个节点 + } else if (parsedMessage.type == 'clickCopyList') { + console.log('获取所有节点的对话信息') + + // targetNodeList.forEach((item: any) => { + // console.log(item.text) + // }) + const result = parseMessages(targetNodeList) + console.log(result, 'result') + this.ws.send(JSON.stringify({ type: parsedMessage.type, status: 'success', message: result, udid: parsedMessage.udid, index: parsedMessage.index })); + + return; + } else if (parsedMessage.type == 'clickCopyText') { + console.log('最后2个节点') + + bounds = targetNodeList[2].bounds; // 选择节点列表中的第3个节点 + } else if (parsedMessage.type == 'getmesNum') { + + bounds = targetNodeList[0].bounds; // 选择节点列表中的第一个节点 + const childCount = targetNodeList[0].childCount + if (childCount == 2) { + this.ws.send(JSON.stringify({ type: parsedMessage.type, status: 'success', message: 0, udid: parsedMessage.udid, index: parsedMessage.index })); + return; + } else if (childCount == 3) { + this.ws.send(JSON.stringify({ type: parsedMessage.type, status: 'success', message: 1, udid: parsedMessage.udid, index: parsedMessage.index })); + // return; + } + } else if (parsedMessage.type == 'clickMesage') { + const targetNode = targetNodeList.find((item: any) => + (item as any).childCount > 0); + console.log('targetNode', targetNode) + if (targetNode) { + // console.log('noticeMes执行1', noticeMes) + bounds = targetNode.bounds; // 使用第一个满足条件的元素 + // } else if (noticeMes[0]) { + // console.log('noticeMes执行2', noticeMes) + // bounds = noticeMes[0].bounds; // 使用第一个满足条件的元素 + } else { + this.ws.send(JSON.stringify({ status: 'error', type: parsedMessage.type, message: '没有消息' })); + return; // 没有符合条件的元素则返回 + } + } else if (parsedMessage.type == 'isVideoAndLive') { + + const VideoType = targetNodeList[0]['content-desc']; // 选择节点列表中的第一个节点 + console.log('类型', VideoType) + this.ws.send(JSON.stringify({ type: parsedMessage.type, status: 'success', message: VideoType, udid: parsedMessage.udid, index: parsedMessage.index })); + return; + } else if (parsedMessage.type == 'isHost') { + console.log(parsedMessage.index, '关注的内容', targetNodeList[0]) + if (targetNodeList[0]) { + console.log('有关注') + this.ws.send(JSON.stringify({ type: parsedMessage.type, status: 'success', message: 1, udid: parsedMessage.udid, index: parsedMessage.index })); + + } + // this.ws.send(JSON.stringify({ type: parsedMessage.type, status: 'success', message: VideoType, udid: parsedMessage.udid, index: parsedMessage.index })); + return; + } else if (parsedMessage.type == 'hostVideo') { + console.log('第' + parsedMessage.num + '个节点') + bounds = targetNodeList[parsedMessage.num].bounds; // 选择节点列表中的第一个节点 + } else if (parsedMessage.type == 'CommentText') { + + this.ws.send(JSON.stringify({ type: parsedMessage.type, status: 'success', message: targetNodeList[0].text, udid: parsedMessage.udid, index: parsedMessage.index })); + return; + } + else { + console.log('第一个节点') + bounds = targetNodeList[0].bounds; // 选择节点列表中的第一个节点 + } + // const match = bounds.match(/\[(\d+),(\d+)]\[(\d+),(\d+)]/); + if (bounds) { + const x1 = parseInt(bounds.left); + const y1 = parseInt(bounds.top); + const x2 = parseInt(bounds.right); + const y2 = parseInt(bounds.bottom); + const clickX = Math.floor((x1 + x2) / 2); + const clickY = Math.floor((y1 + y2) / 2); + console.log(clickX, clickY, '坐标'); + // Step 4: Click + console.log(parsedMessage.udid, '设备id'); + if (parsedMessage.type == 'clickCopy') { + console.log('复制坐标'); + this.ws.send(JSON.stringify({ type: parsedMessage.type, status: 'success', message: '坐标返回', udid: parsedMessage.udid, index: parsedMessage.index, x: clickX, y: clickY })); + return; + } + exec(`adb -s ${parsedMessage.udid} shell input tap ${clickX} ${clickY}`, (clickErr: Error | null) => { + if (clickErr) { + // this.ws.send(JSON.stringify({ status: 'error', message: '点击失败:' + clickErr.message })); + console.log('点击失败:' + clickErr.message); + return; + } + console.log('点击了按钮'); + this.ws.send(JSON.stringify({ type: parsedMessage.type, status: 'success', message: '点击成功', udid: parsedMessage.udid, index: parsedMessage.index, x: clickX, y: clickY })); + }); + } else { + // this.ws.send(JSON.stringify({ status: 'error', message: '解析坐标失败' })); + console.log('解析坐标失败'); + } + } catch (e) { + if (e instanceof Error) { + // this.ws.send(JSON.stringify({ status: 'error', message: '查找节点失败:' + e.message })); + console.log('查找节点失败:' + e.message) + } else { + // this.ws.send(JSON.stringify({ status: 'error', message: '查找节点失败:未知错误' })); + console.log('查找节点失败:未知错误') + } + } + + } else { + if (parsedMessage.type == 'isHost') { + this.ws.send(JSON.stringify({ type: parsedMessage.type, status: 'success', message: 0, udid: parsedMessage.udid, index: parsedMessage.index })); + return; + } else { + this.ws.send(JSON.stringify({ status: 'error', type: parsedMessage.type, message: '未找到按钮' })); + console.log('未找到按钮') + } + } + + }) + } else if (parsedMessage.action === 'test') { + dump(parsedMessage.udid).then((data) => { + console.log('dump ui树', data.xml) + }) + } + + } catch (error) { + console.error('解析消息失败:', error); + return; + } + + + } + + if (this.remoteSocket) { + // console.log('服务端接收的参数'); + this.remoteSocket.send(event.data); + } else { + this.storage.push(event); + } + + + // function bufferToString(arrayBuffer: ArrayBuffer, encoding = 'utf8') { + // return Buffer.from(arrayBuffer).toString(encoding); + // } + + + interface MessageItem { + text: string; + position: 'left' | 'right'; + } + + function parseMessages(nodeList: any[]): MessageItem[] { + // 辅助函数:解析 bounds 为对象格式 + function parseBounds(bounds: any): { left: number, right: number } | null { + if (!bounds) return null; + + // 如果是对象形式 + if (typeof bounds === 'object' && 'left' in bounds && 'right' in bounds) { + return { + left: parseInt(bounds.left), + right: parseInt(bounds.right), + }; + } + + // 如果是字符串形式:[138,1359][478,1491] + if (typeof bounds === 'string') { + const matches = bounds.match(/\[(\d+),\d+\]\[(\d+),\d+\]/); + if (matches && matches.length === 3) { + return { + left: parseInt(matches[1]), + right: parseInt(matches[2]), + }; + } + } + + return null; + } + + // 获取最大 right 值,计算中点 + const maxX = Math.max( + ...nodeList.map(item => { + const b = parseBounds(item.bounds); + return b ? b.right : 0; + }) + ); + const SCREEN_MIDDLE = maxX / 2; + + // 构造消息 + return nodeList + .filter(item => item.text) + .map(item => { + const b = parseBounds(item.bounds); + if (!b) return null; + + const centerX = (b.left + b.right) / 2; + + return { + text: item.text, + position: centerX < SCREEN_MIDDLE ? 'left' : 'right', + }; + }) + .filter(Boolean) as MessageItem[]; + } + + } + + public release(): void { + if (this.released) { + return; + } + super.release(); + this.released = true; + this.flush(); + } + + + +} diff --git a/src/server/mw/WebsocketProxy.ts b/src/server/mw/WebsocketProxy.ts new file mode 100644 index 0000000..a1e6db4 --- /dev/null +++ b/src/server/mw/WebsocketProxy.ts @@ -0,0 +1,617 @@ +import { Mw, RequestParameters } from './Mw'; +import WS from 'ws'; +import { ACTION } from '../../common/Action'; +import { Multiplexer } from '../../packages/multiplexer/Multiplexer'; +import { watchNode, dump, dumpNode } from '../../utils/dumpHierarchy'; + +export class WebsocketProxy extends Mw { + public static readonly TAG = 'WebsocketProxy'; + private remoteSocket?: WS; + private released = false; + private storage: WS.MessageEvent[] = []; + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + public static processRequest(ws: WS, params: RequestParameters): WebsocketProxy | undefined { + const { action, url } = params; + if (action !== ACTION.PROXY_WS) { + return; + } + const wsString = url.searchParams.get('ws'); + if (!wsString) { + ws.close(4003, `[${this.TAG}] Invalid value "${ws}" for "ws" parameter`); + return; + } + return this.createProxy(ws, wsString); + } + + public static createProxy(ws: WS | Multiplexer, remoteUrl: string): WebsocketProxy { + const service = new WebsocketProxy(ws); + service.init(remoteUrl).catch((e) => { + const msg = `[${this.TAG}] Failed to start service: ${e.message}`; + console.error(msg); + ws.close(4005, msg); + }); + return service; + } + + constructor(ws: WS | Multiplexer) { + super(ws); + } + + public async init(remoteUrl: string): Promise { + this.name = `[${WebsocketProxy.TAG}{$${remoteUrl}}]`; + const remoteSocket = new WS(remoteUrl); + remoteSocket.onopen = () => { + this.remoteSocket = remoteSocket; + this.flush(); + }; + remoteSocket.onmessage = (event) => { + if (this.ws && this.ws.readyState === this.ws.OPEN) { + if (Array.isArray(event.data)) { + // console.log('接收的参数27'); + console.log('接收数组数据'); + + event.data.forEach((data) => this.ws.send(data)); + } else { + this.ws.send(event.data); + // console.log('接收转发'); + + } + } + }; + remoteSocket.onclose = (e) => { + if (this.ws.readyState === this.ws.OPEN) { + this.ws.close(e.wasClean ? 1000 : 4010); + } + }; + remoteSocket.onerror = (e) => { + if (this.ws.readyState === this.ws.OPEN) { + this.ws.close(4011, e.message); + } + }; + } + + private flush(): void { + if (this.remoteSocket) { + while (this.storage.length) { + const event = this.storage.shift(); + if (event && event.data) { + console.log('发送的参数26'); + + this.remoteSocket.send(event.data); + } + } + if (this.released) { + this.remoteSocket.close(); + } + } + this.storage.length = 0; + } + + protected onSocketMessage(event: WS.MessageEvent): void { + //速度优化解决方案:将首次点击成功后,将消息发送给前端储存起来,后续点击直接进行像素点 触发 + + // 新增消息解析逻辑 + let message = event.data; + // 判断是否为打开应用指令 + // console.log('接收到的信息', message); + if (typeof message === 'string') { // 抖音包名 + + try { + // 尝试将字符串解析为对象 + const parsedMessage = JSON.parse(message) as { udid: string, action: string, index: number, resourceId: string, type: string, num: number }; // 添加index字段 + console.log('执行指令', parsedMessage); + // 执行ADB命令 + const { exec } = require('child_process'); + // const fs = require('fs'); + // const xml2js = require('xml2js'); + // const path = require('path'); + + if (parsedMessage.action === 'openDY') { + exec(`adb -s ${parsedMessage.udid} shell am start -n com.zhiliaoapp.musically/com.ss.android.ugc.aweme.splash.SplashActivity`, + // (error: Error, stdout: string, stderr: string) => { + (error: Error) => { + if (error) { + console.error('执行失败:', error); + this.ws.send(JSON.stringify({ + status: 'error', + message: error.message + })); + return; + } + this.ws.send(JSON.stringify({ type: parsedMessage.action, status: 'success', udid: parsedMessage.udid, index: parsedMessage.index })); + console.log('抖音已启动'); + // this.ws.send(JSON.stringify({ + // status: 'success', + // data: stdout + // })); + }); + } else if (parsedMessage.action === 'killNow') { + // exec(`adb -s ${parsedMessage.udid} shell input swipe 500 800 500 300 100`, + exec(`for /f "tokens=3 delims=/ " %a in ('adb -s ${parsedMessage.udid} shell dumpsys window ^| findstr "mCurrentFocus"') do adb -s ${parsedMessage.udid} shell am force-stop %a `, + // (error: Error, stdout: string, stderr: string) => { + (error: Error) => { + if (error) { + console.error('执行失败:', error); + this.ws.send(JSON.stringify({ + status: 'error', + message: error.message + })); + return; + } + this.ws.send(JSON.stringify({ type: parsedMessage.action, status: 'success', udid: parsedMessage.udid, index: parsedMessage.index })); + console.log('kill 当前页面进程,关闭软件'); + // this.ws.send(JSON.stringify({ + // status: 'success', + // data: stdout + // })); + }); + } else if (parsedMessage.action === 'install') { + // exec(`adb -s ${parsedMessage.udid} shell input swipe 500 800 500 300 100`, + exec(`adb -s ${parsedMessage.udid} install ${parsedMessage.resourceId}`, + // (error: Error, stdout: string, stderr: string) => { + (error: Error) => { + if (error) { + console.error('执行失败:', error); + this.ws.send(JSON.stringify({ + status: 'error', + message: error.message + })); + return; + } + console.log('安装app'); + // this.ws.send(JSON.stringify({ + // status: 'success', + // data: stdout + // })); + }); + } else if (parsedMessage.action === 'pushFile') { + // exec(`adb -s ${parsedMessage.udid} shell input swipe 500 800 500 300 100`, + exec(`adb -s ${parsedMessage.udid} push "${parsedMessage.resourceId}" /sdcard/`, + // (error: Error, stdout: string, stderr: string) => { + (error: Error) => { + if (error) { + console.error('执行失败:', error); + this.ws.send(JSON.stringify({ + status: 'error', + message: error.message + })); + return; + } + console.log('安装app'); + // this.ws.send(JSON.stringify({ + // status: 'success', + // data: stdout + // })); + }); + } else if (parsedMessage.action === 'slideDown') { + exec(`adb -s ${parsedMessage.udid} shell input swipe 500 800 500 300 100`, + // (error: Error, stdout: string, stderr: string) => { + (error: Error) => { + if (error) { + console.error('执行失败:', error); + this.ws.send(JSON.stringify({ + status: 'error', + message: error.message + })); + return; + } + console.log('滑动向下'); + // this.ws.send(JSON.stringify({ + // status: 'success', + // data: stdout + // })); + }); + } else if (parsedMessage.action === 'slideUp') { + exec(`adb -s ${parsedMessage.udid} shell input swipe 500 300 500 800 100`, + // (error: Error, stdout: string, stderr: string) => { + (error: Error) => { + if (error) { + console.error('执行失败:', error); + this.ws.send(JSON.stringify({ + status: 'error', + message: error.message + })); + return; + } + console.log('滑动向上'); + // this.ws.send(JSON.stringify({ + // status: 'success', + // data: stdout + // })); + }); + } else if (parsedMessage.action === 'slideRight') { + exec(`adb -s ${parsedMessage.udid} shell input swipe 500 1000 10 1000 50`, + // (error: Error, stdout: string, stderr: string) => { + (error: Error) => { + if (error) { + console.error('执行失败:', error); + this.ws.send(JSON.stringify({ + status: 'error', + message: error.message + })); + return; + } + console.log('滑动向上'); + // this.ws.send(JSON.stringify({ + // status: 'success', + // data: stdout + // })); + }); + } else if (parsedMessage.action === 'getSize') { + exec(`adb -s ${parsedMessage.udid} shell wm size`, + // (error: Error, stdout: string, stderr: string) => { + (error: Error, stdout: string) => { + console.log(stdout); + if (error) { + console.error('执行失败:', error); + this.ws.send( + JSON.stringify({ + status: 'error', + message: error.message, + }) + ); + return; + } + + // 解析屏幕尺寸(例如 "Physical size: 1080x1920") + const sizeMatch = stdout.match(/Physical size:\s*(\d+)x(\d+)/); + if (sizeMatch) { + const width = parseInt(sizeMatch[1]); + const height = parseInt(sizeMatch[2]); + console.log(`设备 ${parsedMessage.udid} 的屏幕尺寸: ${width}x${height}`); + + this.ws.send( + JSON.stringify({ + status: 'success', + device: parsedMessage.udid, + action: parsedMessage.action, + index: parsedMessage.index, + width, + height, + }) + ); + } else { + const errorMsg = '无法解析屏幕尺寸信息'; + console.error(errorMsg); + this.ws.send( + JSON.stringify({ + status: 'error', + message: errorMsg, + }) + ); + } + }) + } else if (parsedMessage.action === 'click') { + //判断有无评论 + let time = 10; + if (parsedMessage.type == 'clickSysMesage') { + time = 2 + } else if (parsedMessage.type == 'CommentText') { + time = 5 + } + + console.log("time", time) + watchNode(parsedMessage.udid, parsedMessage.resourceId, time).then((data) => { + console.log('节点出现', data) + if (data.found) { + + try { + // const targetResourceId = parsedMessage.resourceId + //节点列表 + const targetNodeList = data.nodes; + + console.log('节点列表', targetNodeList) + let bounds = targetNodeList[0].bounds; // 选择节点列表中的第一个节点 + if (parsedMessage.type == 'clickCopy' || parsedMessage.type == 'search') { + console.log('最后一个节点') + + bounds = targetNodeList[targetNodeList.length - 1].bounds; // 选择节点列表中的最后一个节点 + } else if (parsedMessage.type == 'clickCopyText') { + console.log('最后2个节点') + + bounds = targetNodeList[2].bounds; // 选择节点列表中的第3个节点 + } else if (parsedMessage.type == 'clickMesage') { + const targetNode = targetNodeList.find((item: any) => + (item as any).childCount > 0); + console.log('targetNode', targetNode) + if (targetNode) { + // console.log('noticeMes执行1', noticeMes) + bounds = targetNode.bounds; // 使用第一个满足条件的元素 + // } else if (noticeMes[0]) { + // console.log('noticeMes执行2', noticeMes) + // bounds = noticeMes[0].bounds; // 使用第一个满足条件的元素 + } else { + this.ws.send(JSON.stringify({ status: 'error', type: parsedMessage.type, message: '没有消息' })); + return; // 没有符合条件的元素则返回 + } + } else if (parsedMessage.type == 'isVideoAndLive') { + + const VideoType = targetNodeList[0]['content-desc']; // 选择节点列表中的第一个节点 + console.log('类型', VideoType) + this.ws.send(JSON.stringify({ type: parsedMessage.type, status: 'success', message: VideoType, udid: parsedMessage.udid, index: parsedMessage.index })); + return; + } else if (parsedMessage.type == 'isHost') { + console.log(parsedMessage.index, '关注的内容', targetNodeList[0]) + if (targetNodeList[0]) { + console.log('有关注') + this.ws.send(JSON.stringify({ type: parsedMessage.type, status: 'success', message: 1, udid: parsedMessage.udid, index: parsedMessage.index })); + + } + // this.ws.send(JSON.stringify({ type: parsedMessage.type, status: 'success', message: VideoType, udid: parsedMessage.udid, index: parsedMessage.index })); + return; + } else if (parsedMessage.type == 'hostVideo') { + console.log('第' + parsedMessage.num + '个节点') + bounds = targetNodeList[parsedMessage.num].bounds; // 选择节点列表中的第一个节点 + } else { + console.log('第一个节点') + bounds = targetNodeList[0].bounds; // 选择节点列表中的第一个节点 + } + // const match = bounds.match(/\[(\d+),(\d+)]\[(\d+),(\d+)]/); + if (bounds) { + const x1 = parseInt(bounds.left); + const y1 = parseInt(bounds.top); + const x2 = parseInt(bounds.right); + const y2 = parseInt(bounds.bottom); + const clickX = Math.floor((x1 + x2) / 2); + const clickY = Math.floor((y1 + y2) / 2); + console.log(clickX, clickY, '坐标'); + // Step 4: Click + console.log(parsedMessage.udid, '设备id'); + if (parsedMessage.type == 'clickCopy') { + console.log('复制坐标'); + this.ws.send(JSON.stringify({ type: parsedMessage.type, status: 'success', message: '坐标返回', udid: parsedMessage.udid, index: parsedMessage.index, x: clickX, y: clickY })); + return; + } + exec(`adb -s ${parsedMessage.udid} shell input tap ${clickX} ${clickY}`, (clickErr: Error | null) => { + if (clickErr) { + // this.ws.send(JSON.stringify({ status: 'error', message: '点击失败:' + clickErr.message })); + console.log('点击失败:' + clickErr.message); + return; + } + console.log('点击了按钮'); + this.ws.send(JSON.stringify({ type: parsedMessage.type, status: 'success', message: '点击成功', udid: parsedMessage.udid, index: parsedMessage.index, x: clickX, y: clickY })); + }); + } else { + // this.ws.send(JSON.stringify({ status: 'error', message: '解析坐标失败' })); + console.log('解析坐标失败'); + } + } catch (e) { + if (e instanceof Error) { + // this.ws.send(JSON.stringify({ status: 'error', message: '查找节点失败:' + e.message })); + console.log('查找节点失败:' + e.message) + } else { + // this.ws.send(JSON.stringify({ status: 'error', message: '查找节点失败:未知错误' })); + console.log('查找节点失败:未知错误') + } + } + + } else { + if (parsedMessage.type == 'isHost') { + this.ws.send(JSON.stringify({ type: parsedMessage.type, status: 'success', message: 0, udid: parsedMessage.udid, index: parsedMessage.index })); + return; + } else { + this.ws.send(JSON.stringify({ status: 'error', type: parsedMessage.type, message: '未找到按钮' })); + console.log('未找到按钮') + } + } + + }) + .catch(err => { + console.error('Error:', err); + + }); + return; + } else if (parsedMessage.action === 'dump') { + dumpNode(parsedMessage.udid, parsedMessage.resourceId).then((data) => { + console.log('节点dump', data) + if (parsedMessage.type == 'getmesNum') { + console.error('获取消息数量') + // console.log('获取消息数量', 2, hasMessage ? '有消息' : '无消息') + } + if (data.found) { + + try { + // const targetResourceId = parsedMessage.resourceId + + + //节点列表 + const targetNodeList = data.nodes; + + console.log('节点列表', targetNodeList) + let bounds = targetNodeList[0].bounds; // 选择节点列表中的第一个节点 + if (parsedMessage.type == 'clickCopy' || parsedMessage.type == 'search') { + console.log('最后一个节点') + + bounds = targetNodeList[targetNodeList.length - 1].bounds; // 选择节点列表中的最后一个节点 + } else if (parsedMessage.type == 'clickCopyList') { + console.log('获取所有节点的对话信息') + const result = parseMessages(targetNodeList) + console.log(result, 'result') + this.ws.send(JSON.stringify({ type: parsedMessage.type, status: 'success', message: result, udid: parsedMessage.udid, index: parsedMessage.index })); + + return; + } else if (parsedMessage.type == 'CommentText') { + + this.ws.send(JSON.stringify({ type: parsedMessage.type, status: 'success', message: targetNodeList[0].text, udid: parsedMessage.udid, index: parsedMessage.index })); + return; + } else if (parsedMessage.type == 'getmesNum') { + let hasMessage = false; + console.log('获取消息数量', 1) + for (const child of targetNodeList[0].children) { + if ( + child.class === 'android.widget.TextView' && + child.text && + /^\d+$/.test(child.text) // 只判断是否是数字,如"1"、"14" + ) { + hasMessage = true; + break; + } + } + console.log('获取消息数量', 2, hasMessage ? '有消息' : '无消息') + + this.ws.send(JSON.stringify({ + type: parsedMessage.type, + status: 'success', + message: hasMessage ? 1 : 0, + udid: parsedMessage.udid, + index: parsedMessage.index + })); + if (!hasMessage) { + return; + } + + } else { + console.log('第一个节点') + bounds = targetNodeList[0].bounds; // 选择节点列表中的第一个节点 + } + const match = bounds.match(/\[(\d+),(\d+)]\[(\d+),(\d+)]/); + console.log(match, 'match') + if (bounds) { + const x1 = parseInt(match[1]); + const y1 = parseInt(match[2]); + const x2 = parseInt(match[3]); + const y2 = parseInt(match[4]); + const clickX = Math.floor((x1 + x2) / 2); + const clickY = Math.floor((y1 + y2) / 2); + console.log(clickX, clickY, '坐标'); + // Step 4: Click + console.log(parsedMessage.udid, '设备id'); + if (parsedMessage.type == 'clickCopy') { + console.log('复制坐标'); + this.ws.send(JSON.stringify({ type: parsedMessage.type, status: 'success', message: '坐标返回', udid: parsedMessage.udid, index: parsedMessage.index, x: clickX, y: clickY })); + return; + } + exec(`adb -s ${parsedMessage.udid} shell input tap ${clickX} ${clickY}`, (clickErr: Error | null) => { + if (clickErr) { + // this.ws.send(JSON.stringify({ status: 'error', message: '点击失败:' + clickErr.message })); + console.log('点击失败:' + clickErr.message); + return; + } + console.log('点击了按钮'); + this.ws.send(JSON.stringify({ type: parsedMessage.type, status: 'success', message: '点击成功', udid: parsedMessage.udid, index: parsedMessage.index, x: clickX, y: clickY })); + }); + } else { + // this.ws.send(JSON.stringify({ status: 'error', message: '解析坐标失败' })); + console.log('解析坐标失败'); + } + } catch (e) { + if (e instanceof Error) { + // this.ws.send(JSON.stringify({ status: 'error', message: '查找节点失败:' + e.message })); + console.log('查找节点失败:' + e.message) + } else { + // this.ws.send(JSON.stringify({ status: 'error', message: '查找节点失败:未知错误' })); + console.log('查找节点失败:未知错误') + } + } + + } else { + if (parsedMessage.type == 'isHost') { + this.ws.send(JSON.stringify({ type: parsedMessage.type, status: 'success', message: 0, udid: parsedMessage.udid, index: parsedMessage.index })); + return; + } else { + this.ws.send(JSON.stringify({ status: 'error', type: parsedMessage.type, message: '未找到按钮' })); + console.log('未找到按钮') + } + } + + }) + } else if (parsedMessage.action === 'test') { + dump(parsedMessage.udid).then((data) => { + console.log('dump ui树', data.xml) + }) + } + + } catch (error) { + console.error('解析消息失败:', error); + return; + } + + + } + + if (this.remoteSocket) { + // console.log('服务端接收的参数'); + this.remoteSocket.send(event.data); + } else { + this.storage.push(event); + } + + + // function bufferToString(arrayBuffer: ArrayBuffer, encoding = 'utf8') { + // return Buffer.from(arrayBuffer).toString(encoding); + // } + + + interface MessageItem { + text: string; + position: 'left' | 'right'; + } + + function parseMessages(nodeList: any[]): MessageItem[] { + // 辅助函数:解析 bounds 为对象格式 + function parseBounds(bounds: any): { left: number, right: number } | null { + if (!bounds) return null; + + // 如果是对象形式 + if (typeof bounds === 'object' && 'left' in bounds && 'right' in bounds) { + return { + left: parseInt(bounds.left), + right: parseInt(bounds.right), + }; + } + + // 如果是字符串形式:[138,1359][478,1491] + if (typeof bounds === 'string') { + const matches = bounds.match(/\[(\d+),\d+\]\[(\d+),\d+\]/); + if (matches && matches.length === 3) { + return { + left: parseInt(matches[1]), + right: parseInt(matches[2]), + }; + } + } + + return null; + } + + // 获取最大 right 值,计算中点 + const maxX = Math.max( + ...nodeList.map(item => { + const b = parseBounds(item.bounds); + return b ? b.right : 0; + }) + ); + const SCREEN_MIDDLE = maxX / 2; + + // 构造消息 + return nodeList + .filter(item => item.text) + .map(item => { + const b = parseBounds(item.bounds); + if (!b) return null; + + const centerX = (b.left + b.right) / 2; + + return { + text: item.text, + position: centerX < SCREEN_MIDDLE ? 'left' : 'right', + }; + }) + .filter(Boolean) as MessageItem[]; + } + + } + + public release(): void { + if (this.released) { + return; + } + super.release(); + this.released = true; + this.flush(); + } + + + +} diff --git a/src/server/services/BaseControlCenter.ts b/src/server/services/BaseControlCenter.ts new file mode 100644 index 0000000..674edaa --- /dev/null +++ b/src/server/services/BaseControlCenter.ts @@ -0,0 +1,13 @@ +import { ControlCenterCommand } from '../../common/ControlCenterCommand'; +import { TypedEmitter } from '../../common/TypedEmitter'; + +export interface ControlCenterEvents { + device: T; +} + +export abstract class BaseControlCenter extends TypedEmitter> { + abstract getId(): string; + abstract getName(): string; + abstract getDevices(): T[]; + abstract runCommand(command: ControlCenterCommand): Promise; +} diff --git a/src/server/services/HttpServer.ts b/src/server/services/HttpServer.ts new file mode 100644 index 0000000..f941dfc --- /dev/null +++ b/src/server/services/HttpServer.ts @@ -0,0 +1,143 @@ +import * as http from 'http'; +import * as https from 'https'; +import path from 'path'; +import { Service } from './Service'; +import { Utils } from '../Utils'; +import express, { Express } from 'express'; +import { Config } from '../Config'; +import { TypedEmitter } from '../../common/TypedEmitter'; +import * as process from 'process'; +import { EnvName } from '../EnvName'; + +const DEFAULT_STATIC_DIR = path.join(__dirname, './public'); + +const PATHNAME = process.env[EnvName.WS_SCRCPY_PATHNAME] || __PATHNAME__; + +export type ServerAndPort = { + server: https.Server | http.Server; + port: number; +}; + +interface HttpServerEvents { + started: boolean; +} + +export class HttpServer extends TypedEmitter implements Service { + private static instance: HttpServer; + private static PUBLIC_DIR = DEFAULT_STATIC_DIR; + private static SERVE_STATIC = true; + private servers: ServerAndPort[] = []; + private mainApp?: Express; + private started = false; + + protected constructor() { + super(); + } + + public static getInstance(): HttpServer { + if (!this.instance) { + this.instance = new HttpServer(); + } + return this.instance; + } + + public static hasInstance(): boolean { + return !!this.instance; + } + + public static setPublicDir(dir: string): void { + if (HttpServer.instance) { + throw Error('Unable to change value after instantiation'); + } + HttpServer.PUBLIC_DIR = dir; + } + + public static setServeStatic(enabled: boolean): void { + if (HttpServer.instance) { + throw Error('Unable to change value after instantiation'); + } + HttpServer.SERVE_STATIC = enabled; + } + + public async getServers(): Promise { + if (this.started) { + return [...this.servers]; + } + return new Promise((resolve) => { + this.once('started', () => { + resolve([...this.servers]); + }); + }); + } + + public getName(): string { + return `HTTP(s) Server Service`; + } + + public async start(): Promise { + this.mainApp = express(); + if (HttpServer.SERVE_STATIC && HttpServer.PUBLIC_DIR) { + this.mainApp.use(PATHNAME, express.static(HttpServer.PUBLIC_DIR)); + + /// #if USE_WDA_MJPEG_SERVER + + const { MjpegProxyFactory } = await import('../mw/MjpegProxyFactory'); + this.mainApp.get('/mjpeg/:udid', new MjpegProxyFactory().proxyRequest); + /// #endif + } + const config = Config.getInstance(); + config.servers.forEach((serverItem) => { + const { secure, port, redirectToSecure } = serverItem; + let proto: string; + let server: http.Server | https.Server; + if (secure) { + if (!serverItem.options) { + throw Error('Must provide option for secure server configuration'); + } + server = https.createServer(serverItem.options, this.mainApp); + proto = 'https'; + } else { + const options = serverItem.options ? { ...serverItem.options } : {}; + proto = 'http'; + let currentApp = this.mainApp; + let host = ''; + let port = 443; + let doRedirect = false; + if (redirectToSecure === true) { + doRedirect = true; + } else if (typeof redirectToSecure === 'object') { + doRedirect = true; + if (typeof redirectToSecure.port === 'number') { + port = redirectToSecure.port; + } + if (typeof redirectToSecure.host === 'string') { + host = redirectToSecure.host; + } + } + if (doRedirect) { + currentApp = express(); + currentApp.use(function (req, res) { + const url = new URL(`https://${host ? host : req.headers.host}${req.url}`); + if (port && port !== 443) { + url.port = port.toString(); + } + return res.redirect(301, url.toString()); + }); + } + server = http.createServer(options, currentApp); + } + this.servers.push({ server, port }); + server.listen(port, () => { + Utils.printListeningMsg(proto, port, PATHNAME); + }); + }); + this.started = true; + this.emit('started', true); + } + + public release(): void { + this.servers.forEach((item) => { + item.server.close(); + }); + } +} diff --git a/src/server/services/ProcessRunner.ts b/src/server/services/ProcessRunner.ts new file mode 100644 index 0000000..42fdc90 --- /dev/null +++ b/src/server/services/ProcessRunner.ts @@ -0,0 +1,84 @@ +import { Service } from './Service'; +import { TypedEmitter } from '../../common/TypedEmitter'; +import { ChildProcessByStdio, spawn } from 'child_process'; +import { Readable, Writable } from 'stream'; + +export interface ProcessRunnerEvents { + spawned: boolean; + started: boolean; + stdout: string; + stderr: string; + close: { code: number; signal: string }; + exit: { code: number | null; signal: string | null }; + error: Error; +} + +export abstract class ProcessRunner extends TypedEmitter implements Service { + protected TAG = '[ProcessRunner]'; + protected name: string; + protected cmd = ''; + protected spawned = false; + protected proc?: ChildProcessByStdio; + protected constructor() { + super(); + this.name = `${this.TAG}`; + } + + protected abstract getArgs(): Promise; + + protected async runProcess(): Promise { + if (!this.cmd) { + throw new Error('Empty command'); + } + const args = await this.getArgs(); + this.proc = spawn(this.cmd, args, { stdio: ['pipe', 'pipe', 'pipe'] }); + + this.proc.stdout.on('data', (data) => { + this.emit('stdout', data.toString()); + }); + + this.proc.stderr.on('data', (data) => { + this.emit('stderr', data); + }); + + this.proc.on('spawn', () => { + this.spawned = true; + this.emit('spawned', true); + }); + + this.proc.on('exit', (code, signal) => { + this.emit('exit', { code, signal }); + }); + + this.proc.on('error', (error) => { + console.error(this.name, `failed to spawn process.\n${error.stack}`); + this.emit('error', error); + }); + + this.proc.on('close', (code, signal) => { + this.emit('close', { code, signal }); + }); + } + + public getName(): string { + return this.name; + } + + public release(): void { + if (this.proc) { + this.proc.kill(); + this.proc = undefined; + } + } + + public start(): Promise { + return this.runProcess().catch((e) => { + console.error(this.name, e.message); + // throw e; + }); + } + + public isStarted(): boolean { + return this.spawned; + } +} diff --git a/src/server/services/Service.ts b/src/server/services/Service.ts new file mode 100644 index 0000000..2a8d8e1 --- /dev/null +++ b/src/server/services/Service.ts @@ -0,0 +1,10 @@ +export interface Service { + getName(): string; + start(): Promise; + release(): void; +} + +export interface ServiceClass { + getInstance(): Service; + hasInstance(): boolean; +} diff --git a/src/server/services/WebSocketServer.ts b/src/server/services/WebSocketServer.ts new file mode 100644 index 0000000..d6f429a --- /dev/null +++ b/src/server/services/WebSocketServer.ts @@ -0,0 +1,83 @@ +import { Server as WSServer } from 'ws'; +import WS from 'ws'; +import { Service } from './Service'; +import { HttpServer, ServerAndPort } from './HttpServer'; +import { MwFactory } from '../mw/Mw'; + +export class WebSocketServer implements Service { + private static instance?: WebSocketServer; + private servers: WSServer[] = []; + private mwFactories: Set = new Set(); + + protected constructor() { + // nothing here + } + + public static getInstance(): WebSocketServer { + if (!this.instance) { + this.instance = new WebSocketServer(); + } + return this.instance; + } + + public static hasInstance(): boolean { + return !!this.instance; + } + + public registerMw(mwFactory: MwFactory): void { + this.mwFactories.add(mwFactory); + } + + public attachToServer(item: ServerAndPort): WSServer { + const { server, port } = item; + const TAG = `WebSocket Server {tcp:${port}}`; + const wss = new WSServer({ server }); + wss.on('connection', async (ws: WS, request) => { + console.log('服务器接收到的参数') + if (!request.url) { + ws.close(4001, `[${TAG}] Invalid url`); + return; + } + const url = new URL(request.url, 'https://example.org/'); + const action = url.searchParams.get('action') || ''; + let processed = false; + for (const mwFactory of this.mwFactories.values()) { + const service = mwFactory.processRequest(ws, { action, request, url }); + if (service) { + processed = true; + } + } + if (!processed) { + ws.close(4002, `[${TAG}] Unsupported request`); + } + return; + }); + wss.on('close', () => { + console.log(`${TAG} stopped`); + }); + this.servers.push(wss); + return wss; + } + + public getServers(): WSServer[] { + return this.servers; + } + + public getName(): string { + return `WebSocket Server Service`; + } + + public async start(): Promise { + const service = HttpServer.getInstance(); + const servers = await service.getServers(); + servers.forEach((item) => { + this.attachToServer(item); + }); + } + + public release(): void { + this.servers.forEach((server) => { + server.close(); + }); + } +} diff --git a/src/style/app.css b/src/style/app.css new file mode 100644 index 0000000..e13218c --- /dev/null +++ b/src/style/app.css @@ -0,0 +1,178 @@ +:root { + --main-bg-color: hsl(0, 0%, 100%); + --stream-bg-color: hsl(0, 0%, 85%); + --shell-bg-color: hsl(0, 0%, 0%); + --text-shadow-color: hsl(218, 67%, 95%); + --header-bg-color: hsl(0, 0%, 95%); + --controls-bg-color: hsla(0, 0%, 95%, 0.8); + --control-buttons-bg-color: hsl(0, 0%, 95%); + --text-color: hsl(210, 16%, 22%); + --text-color-light: hsl(200, 16%, 52%); + --link-color: hsl(218, 85%, 43%); + --link-color-light: hsl(218, 85%, 73%); + --link-color_visited: hsl(271, 68%, 32%); + --link-color_visited-light: hsl(271, 68%, 72%); + --svg-checkbox-bg-color: hsl(172, 100%, 37%); + --svg-button-fill: hsl(199, 17%, 46%); + --kill-button-hover-color: hsl(342, 100%, 37%); + --url-color: hsl(0, 0%, 60%); + --button-text-color: hsl(214, 82%, 51%); + --button-border-color: hsl(0, 0%, 70%); + --progress-background-color: hsla(225, 100%, 50%, 0.2); + --progress-background-error-color: hsla(0, 100%, 50%, 0.2); + --font-size: 14px; +} + +@media (prefers-color-scheme: dark) { + :root { + --main-bg-color: hsl(0, 0%, 14%); + --stream-bg-color: hsl(0, 0%, 20%); + --shell-bg-color: hsl(0, 0%, 0%); + --text-shadow-color: hsl(218, 17%, 18%); + --header-bg-color: hsl(0, 0%, 20%); + --controls-bg-color: hsla(201, 18%, 19%, 0.8); + --control-buttons-bg-color: hsl(201, 18%, 19%); + --text-color: hsl(0, 0%, 90%); + --text-color-light: hsl(0, 0%, 60%); + --link-color: hsl(218, 63%, 70%); + --link-color-light: hsl(218, 63%, 50%); + --link-color_visited: hsl(267, 31%, 47%); + --link-color_visited-light: hsl(267, 31%, 27%); + --svg-checkbox-bg-color: hsl(172, 100%, 27%); + --svg-button-fill: hsl(0, 0%, 100%); + --kill-button-hover-color: hsl(342, 100%, 27%); + --url-color: hsl(0, 0%, 60%); + --device-list-stripe-color: hsl(0, 0%, 16%); + --device-list-default-color: hsl(0, 0%, 14%); + --button-text-color: hsl(214, 82%, 76%); + --button-border-color: hsl(213, 5%, 39%); + --progress-background-color: hsla(225, 100%, 50%, 0.2); + --progress-background-error-color: hsla(0, 100%, 50%, 0.2); + } +} + +html { + font-size: var(--font-size); +} + +a { + color: var(--link-color); +} + +a:visited { + color: var(--link-color_visited); +} + +body { + color: var(--text-color); + background-color: var(--main-bg-color); + position: absolute; + margin: 0; + height: 100%; + width: 100%; + overflow: hidden; +} + + +body.shell { + background-color: var(--shell-bg-color); +} + +body.stream { + background-color: var(--stream-bg-color); +} + +.terminal-container { + width: 100%; + height: 100%; + padding: 5px; +} + +:focus { + outline: none; +} + +.flex-center { + display: flex; + align-items: center; +} + +.wait { + cursor: wait; +} + +.device-view { + z-index: 1; + float: right; + display: inline-block; +} + +.video-layer { + position: absolute; + z-index: 0; +} + +.touch-layer { + position: absolute; + z-index: 1; +} + +.video { + float: right; + max-height: 100%; + max-width: 100%; + background-color: #000000; +} + + +.control-buttons-list { + float: right; + width: 3.715rem; + background-color: var(--control-buttons-bg-color); +} + +.control-button { + margin: .357rem .786rem; + padding: 0; + width: 2.143rem; + height: 2.143rem; + border: none; + opacity: 0.75; + background-color: var(--control-buttons-bg-color); +} + +.control-button:hover { + opacity: 1; +} + +.control-wrapper > input[type=checkbox] { + display: none; +} + +.control-wrapper > label { + display: inline-block; +} + +.control-button > svg { + fill: var(--svg-button-fill); +} + +.control-wrapper > input[type=checkbox].two-images:checked + label > svg.image-on { + display: block; +} + +.control-wrapper > input[type=checkbox].two-images:not(:checked) + label > svg.image-on { + display: none; +} + +.control-wrapper > input[type=checkbox].two-images:checked + label > svg.image-off { + display: none; +} + +.control-wrapper > input[type=checkbox].two-images:not(:checked) + label > svg.image-off { + display: block; +} + +.control-wrapper > input[type=checkbox]:checked + label > svg { + fill: var(--svg-checkbox-bg-color); +} diff --git a/src/style/devicelist.css b/src/style/devicelist.css new file mode 100644 index 0000000..64e4c59 --- /dev/null +++ b/src/style/devicelist.css @@ -0,0 +1,235 @@ +:root { + --device-border-color: hsl(0, 0%, 82%); + --device-list-stripe-color: hsl(0, 0%, 96%); + --device-list-default-color: hsl(0, 0%, 100%); + --device-list-hover-color: hsl(218, 67%, 95%); +} + +@media (prefers-color-scheme: dark) { + :root { + --device-border-color: hsl(0, 0%, 32%); + --device-list-stripe-color: hsl(0, 0%, 16%); + --device-list-default-color: hsl(0, 0%, 14%); + --device-list-hover-color: hsl(218, 17%, 18%); + } +} + + +body.list { + height: auto; + width: auto; + overflow: auto; +} + +#devices { + padding: 20px 0; + width: 100%; + height: calc(100% - 40px); + overflow-y: auto; +} + +body.stream #devices { + background-color: var(--device-list-default-color); + opacity: .8; + position: absolute; + top: 0; + left: 0; + z-index: 3; +} + +body.list #device_list_menu { + display: none; +} + +#device_list_menu { + display: block; + position: absolute; + bottom: 0; + left: 0; + z-index: 4; +} + +#devices .device-list button { + font-size: var(--font-size); + color: var(--button-text-color); +} + +#devices .device-list div.device:nth-child(2n+1){ + background-color: var(--device-list-default-color); +} + +#devices .device-list div.device:nth-child(2n){ + background-color: var(--device-list-stripe-color); +} + +#devices .device-header { + padding: 2px 0; +} + +#devices .device-header div { + display: inline-flex; +} + +#devices .device-name { + font-size: 120%; +} + +#devices .device-model { + font-size: 110%; +} + +#devices .device-serial { + color: var(--url-color); + font-size: 80%; + margin-left: 6px; +} + +#devices .device-version { + font-size: 100%; + margin-left: 6px; + align-items: baseline; +} + +#devices .device-version .sdk-version { + font-size: 75%; + color: var(--url-color); + margin-left: 0.2em; +} + +#devices .device-state { + border-radius: 25px; + background-color: red; + font-size: 80%; + margin-left: 6px; + width: 1em; + height: 1em; +} + +#devices .device.active .device-state { + background-color: green; +} + +#devices .device-list { + position: relative; + bottom: 0; + left: 0; + width: 100%; +} + +#devices .device-list { + border-spacing: 0; + border-collapse: collapse; + font-family: monospace; + font-size: var(--font-size); +} + +#devices .device-list div.device { + padding: 5px 20px 5px; +} + +#devices .device-list div.device:hover { + background-color: var(--device-list-hover-color) +} + +#devices .device-list div.device select { + color: var(--text-color); + background-color: var(--main-bg-color); + margin-left: 0; + border: none; +} + +#devices .device-list div.device:hover select { + background-color: var(--device-list-hover-color);; +} + +#devices .device-list div.desc-block { + margin: .3em; + display: inline-flex; +} + +#devices .device-list div.desc-block.hidden { + display: none; +} + +#devices .device-list div.desc-block.stream, +#devices .device-list div.desc-block.server_pid, +#devices .device-list div.desc-block.net_interface { + border: 1px solid var(--device-border-color); + border-radius: .3em; + overflow: hidden; + white-space: nowrap; +} + +#devices .device-list div.device div.desc-block.stream button.action-button { + color: var(--button-text-color); +} + +#devices .device-list div.desc-block button { + fill: var(--text-color) +} + +#devices .device-list div.desc-block button > span { + padding: 0 .5em; +} + +#devices .device-list div.desc-block button > span, +#devices .device-list div.desc-block button > svg { + vertical-align: middle; +} + +#devices .device-list div.desc-block button > svg { + width: var(--font-size); + height: var(--font-size); +} + +#devices .device-list div.desc-block button > svg > path { + fill: var(--text-color); +} + +#devices .device-list .device.not-active div.desc-block button > svg > path { + fill: var(--text-color-light); +} + +#devices .device-list .device.not-active select { + color: var(--text-color-light); +} + +#devices .device-list .device.not-active { + color: var(--text-color-light); +} + +#devices .device-list .device.not-active a { + color: var(--link-color-light); +} + +#devices .device-list .device.not-active a:visited { + color: var(--link-color_visited-light); +} + +#devices .device-list div.device div.desc-block .action-button { + border: none; + background-color: rgba(0, 0, 0, 0); + color: inherit; +} + +#devices .device-list div.device div.desc-block .action-button.update-interfaces-button { + margin-right: 0; +} + +#devices .device-list div.device div.desc-block .action-button.active { + cursor: pointer; +} + +#devices .device-list .device.active div.desc-block .action-button:hover { + color: var(--kill-button-hover-color); +} + +#devices .device-list .device.active div.desc-block button.action-button:hover > svg > path { + fill: var(--kill-button-hover-color); +} + +#devices .tracker-name { + padding: 5px 20px 5px; + font-size: larger; + font-weight: bolder; +} diff --git a/src/style/devtools.css b/src/style/devtools.css new file mode 100644 index 0000000..e34794b --- /dev/null +++ b/src/style/devtools.css @@ -0,0 +1,123 @@ + +body.devtools { + font-family: Ubuntu, Arial, sans-serif; + font-size: 13px; +} + +body.devtools .device { + padding: 20px; +} + +body.devtools .device-header { + -webkit-box-align: baseline; + -webkit-box-orient: horizontal; + display: -webkit-box; + margin: 10px 0 0; + padding: 2px 0; +} + +body.devtools .device-name { + font-size: 150%; +} + +body.devtools .device-serial { + color: var(--url-color); + font-size: 80%; + margin-left: 6px; +} + +body.devtools .browser-header { + align-items: center; + display: flex; + flex-flow: row wrap; + min-height: 33px; + padding-top: 10px; +} + +body.devtools .browser-header > .browser-name { + font-size: 110%; + font-weight: bold; +} + +body.devtools div.list { + margin-top: 5px; +} + +body.devtools div.list > .row { + padding: 6px 0; + position: relative; +} + +body.devtools .properties-box { + display: flex; +} + +body.devtools .properties-box > img { + flex-shrink: 0; + height: 23px; + padding-left: 2px; + padding-right: 5px; + vertical-align: top; + width: 23px; +} + +body.devtools .subrow-box { + display: inline-block; + vertical-align: top; +} + +body.devtools .subrow { + display: flex; + flex-flow: row wrap; +} + +body.devtools .subrow > div { + margin-right: 0.5em; +} + +.body.devtools url { + color: var(--url-color); + max-width: 200px; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; +} + +body.devtools .action { + color: var(--link-color); + cursor: pointer; + margin-right: 15px; +} + +body.devtools .action.disabled { + color: var(--url-color); + cursor: not-allowed; +} + +body.devtools a.action { + text-decoration: none; +} + +body.devtools a.action.copy { + cursor: copy; +} + +body.devtools .browser-header .action { + margin-left: 10px; +} + +body.devtools .open > input { + border: 1px solid #aaa; + height: 17px; + line-height: 17px; + margin-left: 20px; + padding: 0 2px; +} + +body.devtools .tooltip { + z-index: 1; + position: absolute; + padding: 2px; + color: var(--controls-bg-color); + background-color: var(--text-color); +} diff --git a/src/style/dialog.css b/src/style/dialog.css new file mode 100644 index 0000000..a8446cd --- /dev/null +++ b/src/style/dialog.css @@ -0,0 +1,140 @@ +:root { + --block-top-padding: 0.5rem; + --block-bottom-padding: 0.5rem; + --button-top-padding: 0.2rem; + --button-bottom-padding: 0.2rem; + --header-height: 3rem; + --footer-height: 1.55rem; +} + +.dialog-background { + width: 100%; + height: 100%; + position: fixed; + top: 0; + left: 0; + background-color: rgba(0, 0, 0, 0.75); + z-index: 3; +} + +.dialog-container { + font-family: monospace; + width: 75%; + max-width: 30rem; + min-width: 20rem; + background-color: var(--main-bg-color); + /*border-radius: 0.3rem;*/ + overflow: hidden; +} + +.dialog-container.ready { + height: 100%; + min-height: 100%; +} + +.dialog-container button, .dialog-container select, .dialog-container input { + font-family: monospace; +} + +.dialog-container button { + font-size: var(--font-size); +} + +.dialog-container select { + text-overflow: ellipsis; +} + +.dialog-block { +} + +.dialog-header { + background-color: var(--header-bg-color); + height: var(--header-height); + overflow: hidden; + display: flex; + align-items: center; + width: auto; + position: initial; +} + +.dialog-header span.dialog-title { + display: inline-block; + padding: 0 0.5rem; +} + +.dialog-body { + padding: var(--block-top-padding) 0.5rem var(--block-bottom-padding); + background-color: var(--control-buttons-bg-color); + overflow: auto; +} + +.dialog-body.hidden { + height: 0; + padding: 0; +} + +.dialog-body.visible { + height: calc( + 100% + - 2 * var(--block-top-padding) + - 2 * var(--block-bottom-padding) + - var(--header-height) + - var(--footer-height) + ); +} + +.dialog-footer { + /*display: flex;*/ + /*flex-direction: row-reverse;*/ + padding: var(--block-top-padding) 0.5rem var(--block-bottom-padding); + background-color: var(--stream-bg-color); + height: var(--footer-height); + overflow: hidden; +} + +.dialog-footer span.subtitle { + font-weight: lighter; + line-height: var(--footer-height); + float: left; +} + +.dialog-footer button { + padding: var(--button-top-padding) 0.5rem var(--button-bottom-padding); + margin: 0 0 0 0.5rem; + border-radius: 0.3rem; + /*background-color: var(--main-bg-color);*/ + color: var(--button-text-color); + border: 1px solid var(--button-border-color); + cursor: pointer; + background-color: rgba(0, 0, 0, 0); + height: var(--footer-height); + float: right; +} + +.dialog-footer button:disabled { + cursor: not-allowed; + color: var(--text-color-light); +} + +.controls .label { + grid-column: labels; +} + +.controls .input { + grid-column: controls; + box-sizing: border-box; + margin: 0; + /*height: 2.75ex;*/ +} + +.controls .button { + grid-column: controls; +} + +.controls { + display: grid; + grid-template-columns: [labels] 35% [controls] 65%; + padding: 1rem; + grid-gap: 0.2rem; + align-items: center; +} diff --git a/src/style/filelisting.css b/src/style/filelisting.css new file mode 100644 index 0000000..07f079e --- /dev/null +++ b/src/style/filelisting.css @@ -0,0 +1,108 @@ +body.file-listing { + display: block; + position: absolute; + width: 100%; + height: 100%; + overflow: auto; +} + +.file-listing h1 { + border-bottom: 1px solid var(--button-border-color); + margin-bottom: 10px; + padding-bottom: 10px; + white-space: nowrap; +} + +.file-listing tr:hover { + background-color: var(--controls-bg-color); +} + +.file-listing .quick-link-box { + display: inline-block; + margin-bottom: 10px; + padding-bottom: 10px; +} + +.file-listing .quick-link-box.hidden { + display: none; +} + +.file-listing a.icon { + -webkit-padding-start: 1.5em; + -moz-padding-start: 1.5em; + text-decoration: none; + user-select: auto; +} + +.file-listing a.icon:hover { + text-decoration: underline; +} + +.file-listing a.link { + background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAACAElEQVQ4y4WSzUsVYRjFf897RwUz+wK7LiSIyKCoyEUXDFzcqYhq0R+R4J/gMqilf0VC7VwUuBUxobJumyLTvjDIG0TcO96J7sw8T4t3Rp0kfDYH3o/DOc85Qj4LCwvGPpOmKfPz88zMzEhxFux+cGV8HARE/H2BqopzjpWVV4RhSPNH02YfzkqJwMwwDDHxKIKZR+ccAEmSMDExQZZldLY6Njc3J64gUNVtEswTFsTFdDodFhcXCYKAqakp9ijAjOK5AJbbKJSEYZ3N75t0u10+rK2VCVSVX+0elpfe8mxpHTCvCgM1btw8S71+hupwFYD3q6sAlCw4gXp4jno4ShA4nJNclf43mW0FWZbhHLTbHVZefgUxkiQlTf+QqZKmqd9NvuAsy/ZaqFSMx4+W2fjW5HccY5bgKg7BSNNunpLfjarutfCm8ZEXz9+xFbVRSxg8dIDr1y7m3Sii9lgQBLtb1ni9TqUiiBh9vT1MT9/hePUw/f29iI/KJ4N4S//GqKpUnJd4dzJkaGgQy5Rbt8fY2Pi50wnZ6ce2hVarxfkLJzDg6JEBxi6dBDNEfD9GRo5hlqswo9VqlQmiKKJWO83l2ilU1W9ZduIy9R+LwkVRVLYQxzFOYHLyKp8/NWk0vjA6OszAwT7MBFXfiaKjcRyXCaIo4v6De6WSPHnKvvMXLQgqHcJs3fIAAAAASUVORK5CYII=") left top no-repeat; +} +.file-listing a.file { + background : url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAABnRSTlMAAAAAAABupgeRAAABHUlEQVR42o2RMW7DIBiF3498iHRJD5JKHurL+CRVBp+i2T16tTynF2gO0KSb5ZrBBl4HHDBuK/WXACH4eO9/CAAAbdvijzLGNE1TVZXfZuHg6XCAQESAZXbOKaXO57eiKG6ft9PrKQIkCQqFoIiQFBGlFIB5nvM8t9aOX2Nd18oDzjnPgCDpn/BH4zh2XZdlWVmWiUK4IgCBoFMUz9eP6zRN75cLgEQhcmTQIbl72O0f9865qLAAsURAAgKBJKEtgLXWvyjLuFsThCSstb8rBCaAQhDYWgIZ7myM+TUBjDHrHlZcbMYYk34cN0YSLcgS+wL0fe9TXDMbY33fR2AYBvyQ8L0Gk8MwREBrTfKe4TpTzwhArXWi8HI84h/1DfwI5mhxJamFAAAAAElFTkSuQmCC ") left top no-repeat; +} + +.file-listing a.dir { + background : url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAd5JREFUeNqMU79rFUEQ/vbuodFEEkzAImBpkUabFP4ldpaJhZXYm/RiZWsv/hkWFglBUyTIgyAIIfgIRjHv3r39MePM7N3LcbxAFvZ2b2bn22/mm3XMjF+HL3YW7q28YSIw8mBKoBihhhgCsoORot9d3/ywg3YowMXwNde/PzGnk2vn6PitrT+/PGeNaecg4+qNY3D43vy16A5wDDd4Aqg/ngmrjl/GoN0U5V1QquHQG3q+TPDVhVwyBffcmQGJmSVfyZk7R3SngI4JKfwDJ2+05zIg8gbiereTZRHhJ5KCMOwDFLjhoBTn2g0ghagfKeIYJDPFyibJVBtTREwq60SpYvh5++PpwatHsxSm9QRLSQpEVSd7/TYJUb49TX7gztpjjEffnoVw66+Ytovs14Yp7HaKmUXeX9rKUoMoLNW3srqI5fWn8JejrVkK0QcrkFLOgS39yoKUQe292WJ1guUHG8K2o8K00oO1BTvXoW4yasclUTgZYJY9aFNfAThX5CZRmczAV52oAPoupHhWRIUUAOoyUIlYVaAa/VbLbyiZUiyFbjQFNwiZQSGl4IDy9sO5Wrty0QLKhdZPxmgGcDo8ejn+c/6eiK9poz15Kw7Dr/vN/z6W7q++091/AQYA5mZ8GYJ9K0AAAAAASUVORK5CYII= ") left top no-repeat; +} + +.file-listing a.up { + background : url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAmlJREFUeNpsU0toU0EUPfPysx/tTxuDH9SCWhUDooIbd7oRUUTMouqi2iIoCO6lceHWhegy4EJFinWjrlQUpVm0IIoFpVDEIthm0dpikpf3ZuZ6Z94nrXhhMjM3c8895977BBHB2PznK8WPtDgyWH5q77cPH8PpdXuhpQT4ifR9u5sfJb1bmw6VivahATDrxcRZ2njfoaMv+2j7mLDn93MPiNRMvGbL18L9IpF8h9/TN+EYkMffSiOXJ5+hkD+PdqcLpICWHOHc2CC+LEyA/K+cKQMnlQHJX8wqYG3MAJy88Wa4OLDvEqAEOpJd0LxHIMdHBziowSwVlF8D6QaicK01krw/JynwcKoEwZczewroTvZirlKJs5CqQ5CG8pb57FnJUA0LYCXMX5fibd+p8LWDDemcPZbzQyjvH+Ki1TlIciElA7ghwLKV4kRZstt2sANWRjYTAGzuP2hXZFpJ/GsxgGJ0ox1aoFWsDXyyxqCs26+ydmagFN/rRjymJ1898bzGzmQE0HCZpmk5A0RFIv8Pn0WYPsiu6t/Rsj6PauVTwffTSzGAGZhUG2F06hEc9ibS7OPMNp6ErYFlKavo7MkhmTqCxZ/jwzGA9Hx82H2BZSw1NTN9Gx8ycHkajU/7M+jInsDC7DiaEmo1bNl1AMr9ASFgqVu9MCTIzoGUimXVAnnaN0PdBBDCCYbEtMk6wkpQwIG0sn0PQIUF4GsTwLSIFKNqF6DVrQq+IWVrQDxAYQC/1SsYOI4pOxKZrfifiUSbDUisif7XlpGIPufXd/uvdvZm760M0no1FZcnrzUdjw7au3vu/BVgAFLXeuTxhTXVAAAAAElFTkSuQmCC ") left top no-repeat; +} + +.file-listing a.push { + color: var(--text-color); +} + +.file-listing .listing { + margin: 8px; +} + +.file-listing .foreground { + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + display: flex; + z-index: 1000; + background-color: var(--controls-bg-color); +} + +.file-listing .foreground-message { + flex: auto; + font-size: 30px; + color: #999; + display: flex; + justify-content: center; + align-items: center; + margin: 20px; + pointer-events: none; +} + +.file-listing .foreground-message.drop-target-message { + border: 4px dashed #ddd; +} + +.file-listing .entry-name { + position: relative; +} + +.file-listing .background-progress { + position: absolute; + left: 0; + top: 0; + background-color: var(--progress-background-color); + height: 100%; + width: 100%; + margin: 0; + padding: 0; + border-spacing: 0; +} + +.file-listing .background-progress.error { + background-color: var(--progress-background-error-color); +} diff --git a/src/style/morebox.css b/src/style/morebox.css new file mode 100644 index 0000000..0aef433 --- /dev/null +++ b/src/style/morebox.css @@ -0,0 +1,43 @@ +.text-area { + width: 100%; + resize: vertical; +} + +.more-box { + display: none; + position: absolute; + background-color: var(--controls-bg-color); + z-index: 2; + padding: 0 .714rem .714rem .714rem; +} + +.text-with-shadow, .more-box label { + color: var(--text-color); + text-shadow: var(--text-shadow-color) 0 0 .357rem; +} + +.spoiler > input ~ .box { + display: none; +} + +.spoiler > input:checked ~ .box { + display: block; +} + +.spoiler > label::before { + content: '►'; + margin-right: 5px; +} + +.spoiler > input:checked ~ label::before { + content: '▼'; +} + +.spoiler > input:checked ~ div { + display: block; + padding: 10px; +} + +.spoiler > input { + display: none; +} diff --git a/src/types/ApplDeviceDescriptor.d.ts b/src/types/ApplDeviceDescriptor.d.ts new file mode 100644 index 0000000..c337b35 --- /dev/null +++ b/src/types/ApplDeviceDescriptor.d.ts @@ -0,0 +1,8 @@ +import { BaseDeviceDescriptor } from './BaseDeviceDescriptor'; + +export default interface ApplDeviceDescriptor extends BaseDeviceDescriptor { + name: string; + model: string; + version: string; + 'last.update.timestamp': number; +} diff --git a/src/types/BaseDeviceDescriptor.d.ts b/src/types/BaseDeviceDescriptor.d.ts new file mode 100644 index 0000000..d3eed10 --- /dev/null +++ b/src/types/BaseDeviceDescriptor.d.ts @@ -0,0 +1,4 @@ +export interface BaseDeviceDescriptor { + udid: string; + state: string; +} diff --git a/src/types/Configuration.d.ts b/src/types/Configuration.d.ts new file mode 100644 index 0000000..6a9e7d1 --- /dev/null +++ b/src/types/Configuration.d.ts @@ -0,0 +1,48 @@ +import * as https from 'https'; + +export type OperatingSystem = 'android' | 'ios'; + +export interface HostItem { + type: OperatingSystem; + secure: boolean; + hostname: string; + port: number; + pathname?: string; + useProxy?: boolean; +} + +export interface HostsItem { + type: OperatingSystem | OperatingSystem[]; + secure: boolean; + hostname: string; + port: number; + pathname?: string; + useProxy?: boolean; +} + +export type ExtendedServerOption = https.ServerOptions & { + certPath?: string; + keyPath?: string; +}; + +export interface ServerItem { + secure: boolean; + port: number; + options?: ExtendedServerOption; + redirectToSecure?: + | { + port?: number; + host?: string; + } + | boolean; +} + +// The configuration file must contain a single object with this structure +export interface Configuration { + server?: ServerItem[]; + runApplTracker?: boolean; + announceApplTracker?: boolean; + runGoogTracker?: boolean; + announceGoogTracker?: boolean; + remoteHostList?: HostsItem[]; +} diff --git a/src/types/DeviceTrackerEvent.ts b/src/types/DeviceTrackerEvent.ts new file mode 100644 index 0000000..66757a2 --- /dev/null +++ b/src/types/DeviceTrackerEvent.ts @@ -0,0 +1,5 @@ +export type DeviceTrackerEvent = { + name: string; + id: string; + device: T; +}; diff --git a/src/types/DeviceTrackerEventList.ts b/src/types/DeviceTrackerEventList.ts new file mode 100644 index 0000000..9c0fdae --- /dev/null +++ b/src/types/DeviceTrackerEventList.ts @@ -0,0 +1,5 @@ +export type DeviceTrackerEventList = { + name: string; + id: string; + list: T[]; +}; diff --git a/src/types/FileStats.ts b/src/types/FileStats.ts new file mode 100644 index 0000000..bc2360c --- /dev/null +++ b/src/types/FileStats.ts @@ -0,0 +1,6 @@ +export interface FileStats { + name: string; + isDir: number; + size: number; + dateModified: number; +} diff --git a/src/types/GoogDeviceDescriptor.d.ts b/src/types/GoogDeviceDescriptor.d.ts new file mode 100644 index 0000000..cbf3632 --- /dev/null +++ b/src/types/GoogDeviceDescriptor.d.ts @@ -0,0 +1,14 @@ +import { NetInterface } from './NetInterface'; +import { BaseDeviceDescriptor } from './BaseDeviceDescriptor'; + +export default interface GoogDeviceDescriptor extends BaseDeviceDescriptor { + 'ro.build.version.release': string; + 'ro.build.version.sdk': string; + 'ro.product.cpu.abi': string; + 'ro.product.manufacturer': string; + 'ro.product.model': string; + 'wifi.interface': string; + interfaces: NetInterface[]; + pid: number; + 'last.update.timestamp': number; +} diff --git a/src/types/Message.d.ts b/src/types/Message.d.ts new file mode 100644 index 0000000..09ff62f --- /dev/null +++ b/src/types/Message.d.ts @@ -0,0 +1,5 @@ +export interface Message { + id: number; + type: string; + data: any; +} diff --git a/src/types/MessageFileListing.d.ts b/src/types/MessageFileListing.d.ts new file mode 100644 index 0000000..705f8e8 --- /dev/null +++ b/src/types/MessageFileListing.d.ts @@ -0,0 +1,5 @@ +import { Message } from './Message'; + +export interface MessageFileListing extends Message { + entry: string; +} diff --git a/src/types/MessageRunWdaResponse.ts b/src/types/MessageRunWdaResponse.ts new file mode 100644 index 0000000..7528059 --- /dev/null +++ b/src/types/MessageRunWdaResponse.ts @@ -0,0 +1,12 @@ +import { Message } from './Message'; +import { WdaStatus } from '../common/WdaStatus'; + +export interface MessageRunWdaResponse extends Message { + type: 'run-wda'; + data: { + udid: string; + status: WdaStatus; + code?: number; + text?: string; + }; +} diff --git a/src/types/MessageXtermClient.ts b/src/types/MessageXtermClient.ts new file mode 100644 index 0000000..61b5fa6 --- /dev/null +++ b/src/types/MessageXtermClient.ts @@ -0,0 +1,7 @@ +import { Message } from './Message'; +import { XtermClientMessage } from './XtermMessage'; + +export interface MessageXtermClient extends Message { + type: 'shell'; + data: XtermClientMessage; +} diff --git a/src/types/NetInterface.d.ts b/src/types/NetInterface.d.ts new file mode 100644 index 0000000..2096972 --- /dev/null +++ b/src/types/NetInterface.d.ts @@ -0,0 +1,4 @@ +export interface NetInterface { + name: string; + ipv4: string; +} diff --git a/src/types/ParamsBase.ts b/src/types/ParamsBase.ts new file mode 100644 index 0000000..60dcddb --- /dev/null +++ b/src/types/ParamsBase.ts @@ -0,0 +1,8 @@ +export interface ParamsBase { + action: string; + useProxy?: boolean; + secure?: boolean; + hostname?: string; + port?: number; + pathname?: string; +} diff --git a/src/types/ParamsDeviceTracker.ts b/src/types/ParamsDeviceTracker.ts new file mode 100644 index 0000000..ab322eb --- /dev/null +++ b/src/types/ParamsDeviceTracker.ts @@ -0,0 +1,5 @@ +import { ParamsBase } from './ParamsBase'; + +export interface ParamsDeviceTracker extends ParamsBase { + type: 'android' | 'ios'; +} diff --git a/src/types/ParamsDevtools.d.ts b/src/types/ParamsDevtools.d.ts new file mode 100644 index 0000000..5f4190e --- /dev/null +++ b/src/types/ParamsDevtools.d.ts @@ -0,0 +1,7 @@ +import { ACTION } from '../common/Action'; +import { ParamsBase } from './ParamsBase'; + +export interface ParamsDevtools extends ParamsBase { + action: ACTION.DEVTOOLS; + udid: string; +} diff --git a/src/types/ParamsFileListing.d.ts b/src/types/ParamsFileListing.d.ts new file mode 100644 index 0000000..a1f6b4d --- /dev/null +++ b/src/types/ParamsFileListing.d.ts @@ -0,0 +1,8 @@ +import { ACTION } from '../common/Action'; +import { ParamsBase } from './ParamsBase'; + +export interface ParamsFileListing extends ParamsBase { + action: ACTION.FILE_LISTING; + udid: string; + path: string; +} diff --git a/src/types/ParamsShell.d.ts b/src/types/ParamsShell.d.ts new file mode 100644 index 0000000..984b295 --- /dev/null +++ b/src/types/ParamsShell.d.ts @@ -0,0 +1,7 @@ +import { ParamsBase } from './ParamsBase'; +import { ACTION } from '../common/Action'; + +export interface ParamsShell extends ParamsBase { + action: ACTION.SHELL; + udid: string; +} diff --git a/src/types/ParamsStream.ts b/src/types/ParamsStream.ts new file mode 100644 index 0000000..4c10722 --- /dev/null +++ b/src/types/ParamsStream.ts @@ -0,0 +1,6 @@ +import { ParamsBase } from './ParamsBase'; + +export interface ParamsStream extends ParamsBase { + udid: string; + player: string; +} diff --git a/src/types/ParamsStreamScrcpy.d.ts b/src/types/ParamsStreamScrcpy.d.ts new file mode 100644 index 0000000..8e705e7 --- /dev/null +++ b/src/types/ParamsStreamScrcpy.d.ts @@ -0,0 +1,10 @@ +import { ACTION } from '../common/Action'; +import { ParamsStream } from './ParamsStream'; +import VideoSettings from '../app/VideoSettings'; + +export interface ParamsStreamScrcpy extends ParamsStream { + action: ACTION.STREAM_SCRCPY; + ws: string; + fitToScreen?: boolean; + videoSettings?: VideoSettings; +} diff --git a/src/types/ParamsWdaProxy.d.ts b/src/types/ParamsWdaProxy.d.ts new file mode 100644 index 0000000..00c14fa --- /dev/null +++ b/src/types/ParamsWdaProxy.d.ts @@ -0,0 +1,7 @@ +import { ParamsBase } from './ParamsBase'; +import { ACTION } from '../common/Action'; + +export interface ParamsWdaProxy extends ParamsBase { + action: ACTION.PROXY_WDA; + udid: string; +} diff --git a/src/types/RemoteDevtools.d.ts b/src/types/RemoteDevtools.d.ts new file mode 100644 index 0000000..86367da --- /dev/null +++ b/src/types/RemoteDevtools.d.ts @@ -0,0 +1,42 @@ +export interface VersionMetadata { + 'Android-Package': string; + Browser: string; + 'Protocol-Version': string; + 'User-Agent': string; + 'V8-Version'?: string; + 'WebKit-Version': string; + webSocketDebuggerUrl?: string; +} + +export interface TargetDescription { + attached: boolean; + empty: boolean; + height: number; + screenX: number; + screenY: number; + visible: boolean; + width: number; +} + +export interface RemoteTarget { + description: string; // JSON.stringify(TargetDescription) + devtoolsFrontendUrl: string; + faviconUrl: string; + id: string; + title: string; + type: string; + url: string; + webSocketDebuggerUrl: string; +} + +export type RemoteBrowserInfo = { + socket: string; + version: VersionMetadata; + targets: RemoteTarget[]; +}; + +export type DevtoolsInfo = { + deviceName: string; + deviceSerial: string; + browsers: RemoteBrowserInfo[]; +}; diff --git a/src/types/RemoteDevtoolsCommand.ts b/src/types/RemoteDevtoolsCommand.ts new file mode 100644 index 0000000..1dc4338 --- /dev/null +++ b/src/types/RemoteDevtoolsCommand.ts @@ -0,0 +1,3 @@ +export enum RemoteDevtoolsCommand { + LIST_DEVTOOLS = 'list_devtools', +} diff --git a/src/types/ReplyFileListing.d.ts b/src/types/ReplyFileListing.d.ts new file mode 100644 index 0000000..776d44c --- /dev/null +++ b/src/types/ReplyFileListing.d.ts @@ -0,0 +1,8 @@ +import { Message } from './Message'; +import { FileStats } from './FileStats'; + +export interface ReplyFileListing extends Message { + success: boolean; + error?: string; + list?: FileStats[]; +} diff --git a/src/types/WdaServer.d.ts b/src/types/WdaServer.d.ts new file mode 100644 index 0000000..0084cd3 --- /dev/null +++ b/src/types/WdaServer.d.ts @@ -0,0 +1,10 @@ +// This file is used instead of 'appium-xcuitest-driver/lib/server' + +import * as http from 'http'; +import { XCUITestDriver } from 'appium-xcuitest-driver'; + +declare class Server extends http.Server { + driver: XCUITestDriver; +} + +export { Server, XCUITestDriver }; diff --git a/src/types/XtermMessage.d.ts b/src/types/XtermMessage.d.ts new file mode 100644 index 0000000..5e96c12 --- /dev/null +++ b/src/types/XtermMessage.d.ts @@ -0,0 +1,17 @@ +export enum XtermServiceActions { + start, + stop, +} + +export interface XtermServiceParameters { + cols?: number; + rows?: number; + cwd?: string; + env?: { [key: string]: string }; + udid: string; +} + +export interface XtermClientMessage extends XtermServiceParameters { + type: keyof typeof XtermServiceActions; + pid?: number; +} diff --git a/src/utils/dumpHierarchy.ts b/src/utils/dumpHierarchy.ts new file mode 100644 index 0000000..6ac820e --- /dev/null +++ b/src/utils/dumpHierarchy.ts @@ -0,0 +1,187 @@ +import { execFile } from 'child_process'; +import path from 'path'; +const xml2js = require('xml2js'); + + +/** + * 调用编译后的 .exe 文件获取设备 UI 层级 + * @param udid 设备udid + * @returns Promise<{udid: string, xml: string}> + */ +// export function dumpHierarchy(udid: string): Promise { +// const exePath = path.resolve(__dirname, 'uiauto_dump.exe'); // 替换为 .exe 路径 +// return new Promise((resolve, reject) => { +// execFile(exePath, [udid], (error, stdout, stderr) => { +// if (error) { +// reject(new Error(`执行 .exe 文件失败:${error.message}`)); +// return; +// } +// if (stderr) { +// reject(new Error(`.exe 文件输出错误:${stderr}`)); +// return; +// } +// try { +// // console.log(stdout) +// const data = JSON.parse(stdout); +// resolve(data); +// } catch (e) { +// reject(new Error(`解析输出数据失败:${e}`)); +// } +// }); +// }); +// } + +/** + * 调用打包后的 .exe,监听某个 resource-id 节点是否出现 + * @param udid 设备 ID + * @param resourceId 节点 resource-id,如 com.xx:id/ok_btn + * @param timeout 超时时间,单位秒,默认 30 + * @returns Promise<{ udid: string, xml: string }> + */ +export function watchNode( + udid: string, + resourceId: string, + timeout: number +): Promise<{ udid: string; resource_id: string; found: boolean; count: number; nodes: any[] }> { + const exePath = path.resolve(__dirname, 'watch_node.exe'); // 替换为你实际打包的 exe 文件名 + + return new Promise((resolve, reject) => { + execFile(exePath, ['watch', udid, resourceId, timeout.toString()], (error, stdout, stderr) => { + if (error) { + return reject(new Error(`执行 .exe 文件失败:${error.message}`)); + } + if (stderr) { + return reject(new Error(`.exe 文件输出错误:${stderr}`)); + } + + try { + const result = JSON.parse(stdout); + resolve(result); + } catch (e) { + reject(new Error(`解析输出数据失败:${(e as Error).message}\n原始输出: ${stdout}`)); + } + }); + }); +} +export function dumpNode( + udid: string, + resourceId: string +): Promise<{ + udid: string + resource_id: string + found: boolean + count: number + nodes: any[] +}> { + const exePath = path.resolve(__dirname, 'watch_node.exe') + + return new Promise((resolve, reject) => { + execFile(exePath, ['dump', udid], async (error, stdout, stderr) => { + if (error) return reject(new Error(`执行 .exe 文件失败:${error.message}`)) + if (stderr) return reject(new Error(`.exe 文件输出错误:${stderr}`)) + + let data: any + try { + data = JSON.parse(stdout) + } catch (e) { + return reject(new Error(`解析输出数据失败:${(e as Error).message}\n原始输出: ${stdout}`)) + } + + if (!data.xml) { + return reject(new Error(`未返回 xml 内容:${JSON.stringify(data)}`)) + } + + try { + const result = await xml2js.parseStringPromise(data.xml, { + explicitArray: false, + attrkey: '$', + }) + + const nodes: any[] = [] + + // 递归提取一个节点的基本信息(及其子节点) + const extractNodeInfo = (node: any): any => { + const info: any = { + text: node.$?.text || '', + 'resource-id': node.$?.['resource-id'] || '', + class: node.$?.class || '', + bounds: node.$?.bounds || '', + children: [] + } + + // 递归提取 children + for (const value of Object.values(node)) { + if (typeof value === 'object') { + if (Array.isArray(value)) { + for (const child of value) { + if (child?.$) { + info.children.push(extractNodeInfo(child)) + } + } + } else if ((value as any)?.$) { + info.children.push(extractNodeInfo(value)) + } + } + } + + return info + } + + // 遍历整棵树查找符合 resource-id 的节点,并附加子树结构 + const walk = function (node: any) { + if (!node) return + + if (node.$?.['resource-id'] === resourceId) { + nodes.push(extractNodeInfo(node)) + } + + Object.values(node).forEach((child) => { + if (typeof child === 'object') { + if (Array.isArray(child)) { + child.forEach(walk) + } else { + walk(child) + } + } + }) + } + + walk(result) + + resolve({ + udid: data.udid, + resource_id: resourceId, + found: nodes.length > 0, + count: nodes.length, + nodes + }) + } catch (e) { + reject(new Error(`XML 解析失败:${(e as Error).message}`)) + } + }) + }) +} + +export function dump( + udid: string, +): Promise<{ udid: string; xml: string }> { + const exePath = path.resolve(__dirname, 'watch_node.exe'); // 替换为你实际打包的 exe 文件名 + + return new Promise((resolve, reject) => { + execFile(exePath, ['dump', udid], (error, stdout, stderr) => { + if (error) { + return reject(new Error(`执行 .exe 文件失败:${error.message}`)); + } + if (stderr) { + return reject(new Error(`.exe 文件输出错误:${stderr}`)); + } + + try { + const data = JSON.parse(stdout); + resolve(data); + } catch (e) { + reject(new Error(`解析输出数据失败:${(e as Error).message}\n原始输出: ${stdout}`)); + } + }); + }); +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..45485ac --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,62 @@ +{ + "compilerOptions": { + "esModuleInterop": true, + "baseUrl": ".", + "paths": { + "*": [ + "typings/*" + ] + }, + /* Basic Options */ + "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'. */ + "module": "commonjs", /* Specify module code generation: 'commonjs', 'amd', 'system', 'umd' or 'es2015'. */ + "lib": [ + "es2017", + "dom" + ], /* Specify library files to be included in the compilation: */ + // "allowJs": true, /* Allow javascript files to be compiled. */ + // "checkJs": true, /* Report errors in .js files. */ + // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ + "declaration": false, /* Generates corresponding '.d.ts' file. */ + "sourceMap": true, /* Generates corresponding '.map' file. */ + // "outFile": "./", /* Concatenate and emit output to single file. */ + "outDir": "./build", /* Redirect output structure to the directory. */ + // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ + "removeComments": true, /* Do not emit comments to output. */ + // "noEmit": true, /* Do not emit outputs. */ + "importHelpers": true, /* Import emit helpers from 'tslib'. */ + "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ + // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ + /* Strict Type-Checking Options */ + "strict": true, /* Enable all strict type-checking options. */ + // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* Enable strict null checks. */ + // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ + // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ + /* Additional Checks */ + "noUnusedLocals": true, /* Report errors on unused locals. */ + "noUnusedParameters": true, /* Report errors on unused parameters. */ + "noImplicitReturns": true /* Report error when not all code paths in function return a value. */ + // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ + /* Module Resolution Options */ + // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ + // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ + // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ + // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ + // "typeRoots": [], /* List of folders to include type definitions from. */ + // "types": [] /* Type declaration files to be included in compilation. */ + // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ + /* Source Map Options */ + // "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ + // "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ + // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ + /* Experimental Options */ + // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ + // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ + }, + "include": [ + "./typings/**/*", + "./src/**/*" + ] +} \ No newline at end of file diff --git a/typings/appium-base-driver/index.d.ts b/typings/appium-base-driver/index.d.ts new file mode 100644 index 0000000..b525ee8 --- /dev/null +++ b/typings/appium-base-driver/index.d.ts @@ -0,0 +1,7 @@ +import { DeviceSettings } from './lib/basedriver/device-settings'; + +export class server {} + +export class BaseDriver {} + +export { DeviceSettings }; diff --git a/typings/appium-base-driver/lib/basedriver/device-settings.d.ts b/typings/appium-base-driver/lib/basedriver/device-settings.d.ts new file mode 100644 index 0000000..b8a2269 --- /dev/null +++ b/typings/appium-base-driver/lib/basedriver/device-settings.d.ts @@ -0,0 +1,8 @@ +export class DeviceSettings { + constructor( + defaultSettings: Record, + onSettingsUpdate?: (name: string, newValue: any, oldValue: any) => Promise, + ); + public update(newSettings: Record): Promise; + public getSettings(): Record; +} diff --git a/typings/appium-support/build/lib/timing.d.ts b/typings/appium-support/build/lib/timing.d.ts new file mode 100644 index 0000000..a1780e2 --- /dev/null +++ b/typings/appium-support/build/lib/timing.d.ts @@ -0,0 +1,9 @@ +export class Duration {} + +export class Timer { + get startTime(): number; + public start(): Timer; + getDuration(): Duration; +} + +export default Timer; diff --git a/typings/appium-support/index.d.ts b/typings/appium-support/index.d.ts new file mode 100644 index 0000000..9181bf9 --- /dev/null +++ b/typings/appium-support/index.d.ts @@ -0,0 +1,5 @@ +import * as timing from './build/lib/timing'; + +export { timing }; + +// export default { Timer }; diff --git a/typings/appium-xcuitest-driver/build/lib/device-connections-factory.d.ts b/typings/appium-xcuitest-driver/build/lib/device-connections-factory.d.ts new file mode 100644 index 0000000..db875a1 --- /dev/null +++ b/typings/appium-xcuitest-driver/build/lib/device-connections-factory.d.ts @@ -0,0 +1,13 @@ +declare class DeviceConnectionsFactory { + listConnections (udid?: string | null, port?: string | null, strict?: boolean): string[]; + requestConnection( + udid: string, + port: number, + options: { usePortForwarding?: boolean; devicePort?: number }, + ): Promise; + releaseConnection(udid: string | null, port: number | null): void; +} +declare const DEVICE_CONNECTIONS_FACTORY: DeviceConnectionsFactory; + +export { DEVICE_CONNECTIONS_FACTORY, DeviceConnectionsFactory }; +export default DEVICE_CONNECTIONS_FACTORY; diff --git a/typings/appium-xcuitest-driver/build/lib/driver.d.ts b/typings/appium-xcuitest-driver/build/lib/driver.d.ts new file mode 100644 index 0000000..1a47665 --- /dev/null +++ b/typings/appium-xcuitest-driver/build/lib/driver.d.ts @@ -0,0 +1,33 @@ +import { BaseDriver } from 'appium-base-driver'; + +declare interface ScreenInfo { + statusBarSize: { width: number; height: number }; + scale: number; +} + +declare interface Gesture { + action: string; + options: { + x?: number; + y?: number; + ms?: number; + }; +} + +declare class XCUITestDriver extends BaseDriver { + constructor(opts: Record, shouldValidateCaps: boolean); + public createSession(...args: any): Promise; + public findElement(strategy: string, selector: string): Promise; + public getSize(element: any): Promise<{ width: number; height: number } | undefined>; + public getScreenInfo(): Promise; + public performTouch(gestures: Gesture[]): Promise; + public mobilePressButton(args: { name: string }): Promise; + public stop(): Promise; + public deleteSession(): Promise; + public updateSettings(opts: any): Promise; + public keys(value: string): Promise; + public wda: any; +} + +export default XCUITestDriver; +export { XCUITestDriver }; diff --git a/typings/appium-xcuitest-driver/build/lib/server.d.ts b/typings/appium-xcuitest-driver/build/lib/server.d.ts new file mode 100644 index 0000000..920b914 --- /dev/null +++ b/typings/appium-xcuitest-driver/build/lib/server.d.ts @@ -0,0 +1,9 @@ +import { Server as HttpServer } from 'http'; + +import { XCUITestDriver } from './driver'; + +export class Server extends HttpServer { + public driver: XCUITestDriver; +} + +export function startServer(port: string | number, address?: string): Server; diff --git a/typings/appium-xcuitest-driver/index.d.ts b/typings/appium-xcuitest-driver/index.d.ts new file mode 100644 index 0000000..5f0f06b --- /dev/null +++ b/typings/appium-xcuitest-driver/index.d.ts @@ -0,0 +1,5 @@ +import { XCUITestDriver } from './build/lib/driver'; +import { startServer } from './build/lib/server'; + +export { XCUITestDriver, startServer }; +export default XCUITestDriver; diff --git a/typings/build-config.d.ts b/typings/build-config.d.ts new file mode 100644 index 0000000..242fbd3 --- /dev/null +++ b/typings/build-config.d.ts @@ -0,0 +1 @@ +declare var __PATHNAME__: string; diff --git a/typings/custom_png.d.ts b/typings/custom_png.d.ts new file mode 100644 index 0000000..885ace8 --- /dev/null +++ b/typings/custom_png.d.ts @@ -0,0 +1,4 @@ +declare module '*.png' { + const content: any; + export default content; +} diff --git a/typings/custom_svg.d.ts b/typings/custom_svg.d.ts new file mode 100644 index 0000000..60bd434 --- /dev/null +++ b/typings/custom_svg.d.ts @@ -0,0 +1,4 @@ +declare module '*.svg' { + const content: any; + export default content; +} diff --git a/typings/node-mjpeg-proxy/index.d.ts b/typings/node-mjpeg-proxy/index.d.ts new file mode 100644 index 0000000..a9c9c23 --- /dev/null +++ b/typings/node-mjpeg-proxy/index.d.ts @@ -0,0 +1,10 @@ +import { Request, Response } from 'express'; +import { EventEmitter } from 'events'; + +declare class MjpegProxy extends EventEmitter { + constructor(mjpegUrl: string); + proxyRequest(req: Request, res: Response): void; +} + + +export = MjpegProxy; diff --git a/typings/tinyh264.d.ts b/typings/tinyh264.d.ts new file mode 100644 index 0000000..7b21f90 --- /dev/null +++ b/typings/tinyh264.d.ts @@ -0,0 +1 @@ +export const init: () => void; diff --git a/typings/worker-loader.d.ts b/typings/worker-loader.d.ts new file mode 100644 index 0000000..f019a00 --- /dev/null +++ b/typings/worker-loader.d.ts @@ -0,0 +1,10 @@ +declare module 'worker-loader!*' { + // You need to change `Worker`, if you specified a different value for the `workerType` option + class WebpackWorker extends Worker { + constructor(); + } + + // Uncomment this if you set the `esModule` option to `false` + // export = WebpackWorker; + export default WebpackWorker; +} diff --git a/vendor/Broadway/AUTHORS b/vendor/Broadway/AUTHORS new file mode 100644 index 0000000..460a065 --- /dev/null +++ b/vendor/Broadway/AUTHORS @@ -0,0 +1,9 @@ +The following authors have all licensed their contributions to the project +under the licensing terms detailed in LICENSE. + +Michael Bebenita +Alon Zakai +Andreas Gal +Mathieu 'p01' Henri +Matthias 'soliton4' Behrens +Sam Leitch @oneam - provided the webgl canvas diff --git a/vendor/Broadway/Decoder.d.ts b/vendor/Broadway/Decoder.d.ts new file mode 100644 index 0000000..682c1aa --- /dev/null +++ b/vendor/Broadway/Decoder.d.ts @@ -0,0 +1,6 @@ +declare class Avc { + public onPictureDecoded: (buffer: Uint8Array, width: number, height: number) => void; + public decode(data: Uint8Array): void; +} + +export = Avc; diff --git a/vendor/Broadway/Decoder.js b/vendor/Broadway/Decoder.js new file mode 100644 index 0000000..6f09f55 --- /dev/null +++ b/vendor/Broadway/Decoder.js @@ -0,0 +1,891 @@ +// universal module definition +(function (root, factory) { + if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module. + define([], factory); + } else if (typeof exports === 'object') { + // Node. Does not work with strict CommonJS, but + // only CommonJS-like environments that support module.exports, + // like Node. + module.exports = factory(); + } else { + // Browser globals (root is window) + root.Decoder = factory(); + } +}(this, function () { + + var global; + + function initglobal(){ + global = this; + if (!global){ + if (typeof window != "undefined"){ + global = window; + }else if (typeof self != "undefined"){ + global = self; + }; + }; + }; + initglobal(); + + + function error(message) { + console.error(message); + console.trace(); + }; + + + function assert(condition, message) { + if (!condition) { + error(message); + }; + }; + + + + + var getModule = function(par_broadwayOnHeadersDecoded, par_broadwayOnPictureDecoded){ + + + /*var ModuleX = { + 'print': function(text) { console.log('stdout: ' + text); }, + 'printErr': function(text) { console.log('stderr: ' + text); } + };*/ + + + /* + + The reason why this is all packed into one file is that this file can also function as worker. + you can integrate the file into your build system and provide the original file to be loaded into a worker. + + */ + + //var Module = (function(){ + + +var Module=typeof Module!=="undefined"?Module:{};var moduleOverrides={};var key;for(key in Module){if(Module.hasOwnProperty(key)){moduleOverrides[key]=Module[key]}}Module["arguments"]=[];Module["thisProgram"]="./this.program";Module["quit"]=(function(status,toThrow){throw toThrow});Module["preRun"]=[];Module["postRun"]=[];var ENVIRONMENT_IS_WEB=false;var ENVIRONMENT_IS_WORKER=false;var ENVIRONMENT_IS_NODE=false;var ENVIRONMENT_IS_SHELL=false;if(Module["ENVIRONMENT"]){if(Module["ENVIRONMENT"]==="WEB"){ENVIRONMENT_IS_WEB=true}else if(Module["ENVIRONMENT"]==="WORKER"){ENVIRONMENT_IS_WORKER=true}else if(Module["ENVIRONMENT"]==="NODE"){ENVIRONMENT_IS_NODE=true}else if(Module["ENVIRONMENT"]==="SHELL"){ENVIRONMENT_IS_SHELL=true}else{throw new Error("Module['ENVIRONMENT'] value is not valid. must be one of: WEB|WORKER|NODE|SHELL.")}}else{ENVIRONMENT_IS_WEB=typeof window==="object";ENVIRONMENT_IS_WORKER=typeof importScripts==="function";ENVIRONMENT_IS_NODE=typeof process==="object"&&typeof null==="function"&&!ENVIRONMENT_IS_WEB&&!ENVIRONMENT_IS_WORKER;ENVIRONMENT_IS_SHELL=!ENVIRONMENT_IS_WEB&&!ENVIRONMENT_IS_NODE&&!ENVIRONMENT_IS_WORKER}if(ENVIRONMENT_IS_NODE){var nodeFS;var nodePath;Module["read"]=function shell_read(filename,binary){var ret;if(!nodeFS)nodeFS=(null)("fs");if(!nodePath)nodePath=(null)("path");filename=nodePath["normalize"](filename);ret=nodeFS["readFileSync"](filename);return binary?ret:ret.toString()};Module["readBinary"]=function readBinary(filename){var ret=Module["read"](filename,true);if(!ret.buffer){ret=new Uint8Array(ret)}assert(ret.buffer);return ret};if(process["argv"].length>1){Module["thisProgram"]=process["argv"][1].replace(/\\/g,"/")}Module["arguments"]=process["argv"].slice(2);if(typeof module!=="undefined"){module["exports"]=Module}process["on"]("uncaughtException",(function(ex){if(!(ex instanceof ExitStatus)){throw ex}}));process["on"]("unhandledRejection",(function(reason,p){process["exit"](1)}));Module["inspect"]=(function(){return"[Emscripten Module object]"})}else if(ENVIRONMENT_IS_SHELL){if(typeof read!="undefined"){Module["read"]=function shell_read(f){return read(f)}}Module["readBinary"]=function readBinary(f){var data;if(typeof readbuffer==="function"){return new Uint8Array(readbuffer(f))}data=read(f,"binary");assert(typeof data==="object");return data};if(typeof scriptArgs!="undefined"){Module["arguments"]=scriptArgs}else if(typeof arguments!="undefined"){Module["arguments"]=arguments}if(typeof quit==="function"){Module["quit"]=(function(status,toThrow){quit(status)})}}else if(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER){Module["read"]=function shell_read(url){var xhr=new XMLHttpRequest;xhr.open("GET",url,false);xhr.send(null);return xhr.responseText};if(ENVIRONMENT_IS_WORKER){Module["readBinary"]=function readBinary(url){var xhr=new XMLHttpRequest;xhr.open("GET",url,false);xhr.responseType="arraybuffer";xhr.send(null);return new Uint8Array(xhr.response)}}Module["readAsync"]=function readAsync(url,onload,onerror){var xhr=new XMLHttpRequest;xhr.open("GET",url,true);xhr.responseType="arraybuffer";xhr.onload=function xhr_onload(){if(xhr.status==200||xhr.status==0&&xhr.response){onload(xhr.response);return}onerror()};xhr.onerror=onerror;xhr.send(null)};Module["setWindowTitle"]=(function(title){document.title=title})}else{throw new Error("not compiled for this environment")}Module["print"]=typeof console!=="undefined"?console.log.bind(console):typeof print!=="undefined"?print:null;Module["printErr"]=typeof printErr!=="undefined"?printErr:typeof console!=="undefined"&&console.warn.bind(console)||Module["print"];Module.print=Module["print"];Module.printErr=Module["printErr"];for(key in moduleOverrides){if(moduleOverrides.hasOwnProperty(key)){Module[key]=moduleOverrides[key]}}moduleOverrides=undefined;var STACK_ALIGN=16;function staticAlloc(size){assert(!staticSealed);var ret=STATICTOP;STATICTOP=STATICTOP+size+15&-16;return ret}function alignMemory(size,factor){if(!factor)factor=STACK_ALIGN;var ret=size=Math.ceil(size/factor)*factor;return ret}var asm2wasmImports={"f64-rem":(function(x,y){return x%y}),"debugger":(function(){debugger})};var functionPointers=new Array(0);var GLOBAL_BASE=1024;var ABORT=0;var EXITSTATUS=0;function assert(condition,text){if(!condition){abort("Assertion failed: "+text)}}function Pointer_stringify(ptr,length){if(length===0||!ptr)return"";var hasUtf=0;var t;var i=0;while(1){t=HEAPU8[ptr+i>>0];hasUtf|=t;if(t==0&&!length)break;i++;if(length&&i==length)break}if(!length)length=i;var ret="";if(hasUtf<128){var MAX_CHUNK=1024;var curr;while(length>0){curr=String.fromCharCode.apply(String,HEAPU8.subarray(ptr,ptr+Math.min(length,MAX_CHUNK)));ret=ret?ret+curr:curr;ptr+=MAX_CHUNK;length-=MAX_CHUNK}return ret}return UTF8ToString(ptr)}var UTF8Decoder=typeof TextDecoder!=="undefined"?new TextDecoder("utf8"):undefined;function UTF8ArrayToString(u8Array,idx){var endPtr=idx;while(u8Array[endPtr])++endPtr;if(endPtr-idx>16&&u8Array.subarray&&UTF8Decoder){return UTF8Decoder.decode(u8Array.subarray(idx,endPtr))}else{var u0,u1,u2,u3,u4,u5;var str="";while(1){u0=u8Array[idx++];if(!u0)return str;if(!(u0&128)){str+=String.fromCharCode(u0);continue}u1=u8Array[idx++]&63;if((u0&224)==192){str+=String.fromCharCode((u0&31)<<6|u1);continue}u2=u8Array[idx++]&63;if((u0&240)==224){u0=(u0&15)<<12|u1<<6|u2}else{u3=u8Array[idx++]&63;if((u0&248)==240){u0=(u0&7)<<18|u1<<12|u2<<6|u3}else{u4=u8Array[idx++]&63;if((u0&252)==248){u0=(u0&3)<<24|u1<<18|u2<<12|u3<<6|u4}else{u5=u8Array[idx++]&63;u0=(u0&1)<<30|u1<<24|u2<<18|u3<<12|u4<<6|u5}}}if(u0<65536){str+=String.fromCharCode(u0)}else{var ch=u0-65536;str+=String.fromCharCode(55296|ch>>10,56320|ch&1023)}}}}function UTF8ToString(ptr){return UTF8ArrayToString(HEAPU8,ptr)}var UTF16Decoder=typeof TextDecoder!=="undefined"?new TextDecoder("utf-16le"):undefined;var WASM_PAGE_SIZE=65536;var ASMJS_PAGE_SIZE=16777216;function alignUp(x,multiple){if(x%multiple>0){x+=multiple-x%multiple}return x}var buffer,HEAP8,HEAPU8,HEAP16,HEAPU16,HEAP32,HEAPU32,HEAPF32,HEAPF64;function updateGlobalBuffer(buf){Module["buffer"]=buffer=buf}function updateGlobalBufferViews(){Module["HEAP8"]=HEAP8=new Int8Array(buffer);Module["HEAP16"]=HEAP16=new Int16Array(buffer);Module["HEAP32"]=HEAP32=new Int32Array(buffer);Module["HEAPU8"]=HEAPU8=new Uint8Array(buffer);Module["HEAPU16"]=HEAPU16=new Uint16Array(buffer);Module["HEAPU32"]=HEAPU32=new Uint32Array(buffer);Module["HEAPF32"]=HEAPF32=new Float32Array(buffer);Module["HEAPF64"]=HEAPF64=new Float64Array(buffer)}var STATIC_BASE,STATICTOP,staticSealed;var STACK_BASE,STACKTOP,STACK_MAX;var DYNAMIC_BASE,DYNAMICTOP_PTR;STATIC_BASE=STATICTOP=STACK_BASE=STACKTOP=STACK_MAX=DYNAMIC_BASE=DYNAMICTOP_PTR=0;staticSealed=false;function abortOnCannotGrowMemory(){abort("Cannot enlarge memory arrays. Either (1) compile with -s TOTAL_MEMORY=X with X higher than the current value "+TOTAL_MEMORY+", (2) compile with -s ALLOW_MEMORY_GROWTH=1 which allows increasing the size at runtime, or (3) if you want malloc to return NULL (0) instead of this abort, compile with -s ABORTING_MALLOC=0 ")}function enlargeMemory(){abortOnCannotGrowMemory()}var TOTAL_STACK=Module["TOTAL_STACK"]||5242880;var TOTAL_MEMORY=Module["TOTAL_MEMORY"]||52428800;if(TOTAL_MEMORY0){var callback=callbacks.shift();if(typeof callback=="function"){callback();continue}var func=callback.func;if(typeof func==="number"){if(callback.arg===undefined){Module["dynCall_v"](func)}else{Module["dynCall_vi"](func,callback.arg)}}else{func(callback.arg===undefined?null:callback.arg)}}}var __ATPRERUN__=[];var __ATINIT__=[];var __ATMAIN__=[];var __ATEXIT__=[];var __ATPOSTRUN__=[];var runtimeInitialized=false;var runtimeExited=false;function preRun(){if(Module["preRun"]){if(typeof Module["preRun"]=="function")Module["preRun"]=[Module["preRun"]];while(Module["preRun"].length){addOnPreRun(Module["preRun"].shift())}}callRuntimeCallbacks(__ATPRERUN__)}function ensureInitRuntime(){if(runtimeInitialized)return;runtimeInitialized=true;callRuntimeCallbacks(__ATINIT__)}function preMain(){callRuntimeCallbacks(__ATMAIN__)}function exitRuntime(){callRuntimeCallbacks(__ATEXIT__);runtimeExited=true}function postRun(){if(Module["postRun"]){if(typeof Module["postRun"]=="function")Module["postRun"]=[Module["postRun"]];while(Module["postRun"].length){addOnPostRun(Module["postRun"].shift())}}callRuntimeCallbacks(__ATPOSTRUN__)}function addOnPreRun(cb){__ATPRERUN__.unshift(cb)}function addOnPostRun(cb){__ATPOSTRUN__.unshift(cb)}var Math_abs=Math.abs;var Math_cos=Math.cos;var Math_sin=Math.sin;var Math_tan=Math.tan;var Math_acos=Math.acos;var Math_asin=Math.asin;var Math_atan=Math.atan;var Math_atan2=Math.atan2;var Math_exp=Math.exp;var Math_log=Math.log;var Math_sqrt=Math.sqrt;var Math_ceil=Math.ceil;var Math_floor=Math.floor;var Math_pow=Math.pow;var Math_imul=Math.imul;var Math_fround=Math.fround;var Math_round=Math.round;var Math_min=Math.min;var Math_max=Math.max;var Math_clz32=Math.clz32;var Math_trunc=Math.trunc;var runDependencies=0;var runDependencyWatcher=null;var dependenciesFulfilled=null;function addRunDependency(id){runDependencies++;if(Module["monitorRunDependencies"]){Module["monitorRunDependencies"](runDependencies)}}function removeRunDependency(id){runDependencies--;if(Module["monitorRunDependencies"]){Module["monitorRunDependencies"](runDependencies)}if(runDependencies==0){if(runDependencyWatcher!==null){clearInterval(runDependencyWatcher);runDependencyWatcher=null}if(dependenciesFulfilled){var callback=dependenciesFulfilled;dependenciesFulfilled=null;callback()}}}Module["preloadedImages"]={};Module["preloadedAudios"]={};var dataURIPrefix="data:application/octet-stream;base64,";function isDataURI(filename){return String.prototype.startsWith?filename.startsWith(dataURIPrefix):filename.indexOf(dataURIPrefix)===0}function integrateWasmJS(){var wasmTextFile="avc.wast";var wasmBinaryFile="avc.wasm";var asmjsCodeFile="avc.temp.asm.js";if(typeof Module["locateFile"]==="function"){if(!isDataURI(wasmTextFile)){wasmTextFile=Module["locateFile"](wasmTextFile)}if(!isDataURI(wasmBinaryFile)){wasmBinaryFile=Module["locateFile"](wasmBinaryFile)}if(!isDataURI(asmjsCodeFile)){asmjsCodeFile=Module["locateFile"](asmjsCodeFile)}}var wasmPageSize=64*1024;var info={"global":null,"env":null,"asm2wasm":asm2wasmImports,"parent":Module};var exports=null;function mergeMemory(newBuffer){var oldBuffer=Module["buffer"];if(newBuffer.byteLength>2];return ret}),getStr:(function(){var ret=Pointer_stringify(SYSCALLS.get());return ret}),get64:(function(){var low=SYSCALLS.get(),high=SYSCALLS.get();if(low>=0)assert(high===0);else assert(high===-1);return low}),getZero:(function(){assert(SYSCALLS.get()===0)})};function ___syscall140(which,varargs){SYSCALLS.varargs=varargs;try{var stream=SYSCALLS.getStreamFromFD(),offset_high=SYSCALLS.get(),offset_low=SYSCALLS.get(),result=SYSCALLS.get(),whence=SYSCALLS.get();var offset=offset_low;FS.llseek(stream,offset,whence);HEAP32[result>>2]=stream.position;if(stream.getdents&&offset===0&&whence===0)stream.getdents=null;return 0}catch(e){if(typeof FS==="undefined"||!(e instanceof FS.ErrnoError))abort(e);return-e.errno}}function ___syscall146(which,varargs){SYSCALLS.varargs=varargs;try{var stream=SYSCALLS.get(),iov=SYSCALLS.get(),iovcnt=SYSCALLS.get();var ret=0;if(!___syscall146.buffers){___syscall146.buffers=[null,[],[]];___syscall146.printChar=(function(stream,curr){var buffer=___syscall146.buffers[stream];assert(buffer);if(curr===0||curr===10){(stream===1?Module["print"]:Module["printErr"])(UTF8ArrayToString(buffer,0));buffer.length=0}else{buffer.push(curr)}})}for(var i=0;i>2];var len=HEAP32[iov+(i*8+4)>>2];for(var j=0;j>2]=value;return value}DYNAMICTOP_PTR=staticAlloc(4);STACK_BASE=STACKTOP=alignMemory(STATICTOP);STACK_MAX=STACK_BASE+TOTAL_STACK;DYNAMIC_BASE=alignMemory(STACK_MAX);HEAP32[DYNAMICTOP_PTR>>2]=DYNAMIC_BASE;staticSealed=true;Module["wasmTableSize"]=10;Module["wasmMaxTableSize"]=10;Module.asmGlobalArg={};Module.asmLibraryArg={"abort":abort,"enlargeMemory":enlargeMemory,"getTotalMemory":getTotalMemory,"abortOnCannotGrowMemory":abortOnCannotGrowMemory,"___setErrNo":___setErrNo,"___syscall140":___syscall140,"___syscall146":___syscall146,"___syscall54":___syscall54,"___syscall6":___syscall6,"_broadwayOnHeadersDecoded":_broadwayOnHeadersDecoded,"_broadwayOnPictureDecoded":_broadwayOnPictureDecoded,"_emscripten_memcpy_big":_emscripten_memcpy_big,"DYNAMICTOP_PTR":DYNAMICTOP_PTR,"STACKTOP":STACKTOP};var asm=Module["asm"](Module.asmGlobalArg,Module.asmLibraryArg,buffer);Module["asm"]=asm;var _broadwayCreateStream=Module["_broadwayCreateStream"]=(function(){return Module["asm"]["_broadwayCreateStream"].apply(null,arguments)});var _broadwayExit=Module["_broadwayExit"]=(function(){return Module["asm"]["_broadwayExit"].apply(null,arguments)});var _broadwayGetMajorVersion=Module["_broadwayGetMajorVersion"]=(function(){return Module["asm"]["_broadwayGetMajorVersion"].apply(null,arguments)});var _broadwayGetMinorVersion=Module["_broadwayGetMinorVersion"]=(function(){return Module["asm"]["_broadwayGetMinorVersion"].apply(null,arguments)});var _broadwayInit=Module["_broadwayInit"]=(function(){return Module["asm"]["_broadwayInit"].apply(null,arguments)});var _broadwayPlayStream=Module["_broadwayPlayStream"]=(function(){return Module["asm"]["_broadwayPlayStream"].apply(null,arguments)});Module["asm"]=asm;function ExitStatus(status){this.name="ExitStatus";this.message="Program terminated with exit("+status+")";this.status=status}ExitStatus.prototype=new Error;ExitStatus.prototype.constructor=ExitStatus;var initialStackTop;dependenciesFulfilled=function runCaller(){if(!Module["calledRun"])run();if(!Module["calledRun"])dependenciesFulfilled=runCaller};function run(args){args=args||Module["arguments"];if(runDependencies>0){return}preRun();if(runDependencies>0)return;if(Module["calledRun"])return;function doRun(){if(Module["calledRun"])return;Module["calledRun"]=true;if(ABORT)return;ensureInitRuntime();preMain();if(Module["onRuntimeInitialized"])Module["onRuntimeInitialized"]();postRun()}if(Module["setStatus"]){Module["setStatus"]("Running...");setTimeout((function(){setTimeout((function(){Module["setStatus"]("")}),1);doRun()}),1)}else{doRun()}}Module["run"]=run;function exit(status,implicit){if(implicit&&Module["noExitRuntime"]&&status===0){return}if(Module["noExitRuntime"]){}else{ABORT=true;EXITSTATUS=status;STACKTOP=initialStackTop;exitRuntime();if(Module["onExit"])Module["onExit"](status)}if(ENVIRONMENT_IS_NODE){process["exit"](status)}Module["quit"](status,new ExitStatus(status))}Module["exit"]=exit;function abort(what){if(Module["onAbort"]){Module["onAbort"](what)}if(what!==undefined){Module.print(what);Module.printErr(what);what=JSON.stringify(what)}else{what=""}ABORT=true;EXITSTATUS=1;throw"abort("+what+"). Build with -s ASSERTIONS=1 for more info."}Module["abort"]=abort;if(Module["preInit"]){if(typeof Module["preInit"]=="function")Module["preInit"]=[Module["preInit"]];while(Module["preInit"].length>0){Module["preInit"].pop()()}}Module["noExitRuntime"]=true;run() + + + + // return Module; + //})(); + + var resultModule; + if (typeof global !== "undefined"){ + if (global.Module){ + resultModule = global.Module; + }; + }; + if (typeof Module != "undefined"){ + resultModule = Module; + }; + + resultModule._broadwayOnHeadersDecoded = par_broadwayOnHeadersDecoded; + resultModule._broadwayOnPictureDecoded = par_broadwayOnPictureDecoded; + + var moduleIsReady = false; + var cbFun; + var moduleReady = function(){ + moduleIsReady = true; + if (cbFun){ + cbFun(resultModule); + } + }; + + resultModule.onRuntimeInitialized = function(){ + moduleReady(resultModule); + }; + return function(callback){ + if (moduleIsReady){ + callback(resultModule); + }else{ + cbFun = callback; + }; + }; + }; + + return (function(){ + "use strict"; + + + var nowValue = function(){ + return (new Date()).getTime(); + }; + + if (typeof performance != "undefined"){ + if (performance.now){ + nowValue = function(){ + return performance.now(); + }; + }; + }; + + + var Decoder = function(parOptions){ + this.options = parOptions || {}; + + this.now = nowValue; + + var asmInstance; + + var fakeWindow = { + }; + + var toU8Array; + var toU32Array; + + var onPicFun = function ($buffer, width, height) { + var buffer = this.pictureBuffers[$buffer]; + if (!buffer) { + buffer = this.pictureBuffers[$buffer] = toU8Array($buffer, (width * height * 3) / 2); + }; + + var infos; + var doInfo = false; + if (this.infoAr.length){ + doInfo = true; + infos = this.infoAr; + }; + this.infoAr = []; + + if (this.options.rgb){ + if (!asmInstance){ + asmInstance = getAsm(width, height); + }; + asmInstance.inp.set(buffer); + asmInstance.doit(); + + var copyU8 = new Uint8Array(asmInstance.outSize); + copyU8.set( asmInstance.out ); + + if (doInfo){ + infos[0].finishDecoding = nowValue(); + }; + + this.onPictureDecoded(copyU8, width, height, infos); + return; + + }; + + if (doInfo){ + infos[0].finishDecoding = nowValue(); + }; + this.onPictureDecoded(buffer, width, height, infos); + }.bind(this); + + var ignore = false; + + if (this.options.sliceMode){ + onPicFun = function ($buffer, width, height, $sliceInfo) { + if (ignore){ + return; + }; + var buffer = this.pictureBuffers[$buffer]; + if (!buffer) { + buffer = this.pictureBuffers[$buffer] = toU8Array($buffer, (width * height * 3) / 2); + }; + var sliceInfo = this.pictureBuffers[$sliceInfo]; + if (!sliceInfo) { + sliceInfo = this.pictureBuffers[$sliceInfo] = toU32Array($sliceInfo, 18); + }; + + var infos; + var doInfo = false; + if (this.infoAr.length){ + doInfo = true; + infos = this.infoAr; + }; + this.infoAr = []; + + /*if (this.options.rgb){ + + no rgb in slice mode + + };*/ + + infos[0].finishDecoding = nowValue(); + var sliceInfoAr = []; + for (var i = 0; i < 20; ++i){ + sliceInfoAr.push(sliceInfo[i]); + }; + infos[0].sliceInfoAr = sliceInfoAr; + + this.onPictureDecoded(buffer, width, height, infos); + }.bind(this); + }; + + var ModuleCallback = getModule.apply(fakeWindow, [function () { + }, onPicFun]); + + + var MAX_STREAM_BUFFER_LENGTH = 1024 * 1024; + + var instance = this; + this.onPictureDecoded = function (buffer, width, height, infos) { + + }; + + this.onDecoderReady = function(){}; + + var bufferedCalls = []; + this.decode = function decode(typedAr, parInfo, copyDoneFun) { + bufferedCalls.push([typedAr, parInfo, copyDoneFun]); + }; + + ModuleCallback(function(Module){ + var HEAP8 = Module.HEAP8; + var HEAPU8 = Module.HEAPU8; + var HEAP16 = Module.HEAP16; + var HEAP32 = Module.HEAP32; + // from old constructor + Module._broadwayInit(); + + /** + * Creates a typed array from a HEAP8 pointer. + */ + toU8Array = function(ptr, length) { + return HEAPU8.subarray(ptr, ptr + length); + }; + toU32Array = function(ptr, length) { + //var tmp = HEAPU8.subarray(ptr, ptr + (length * 4)); + return new Uint32Array(HEAPU8.buffer, ptr, length); + }; + instance.streamBuffer = toU8Array(Module._broadwayCreateStream(MAX_STREAM_BUFFER_LENGTH), MAX_STREAM_BUFFER_LENGTH); + instance.pictureBuffers = {}; + // collect extra infos that are provided with the nal units + instance.infoAr = []; + + /** + * Decodes a stream buffer. This may be one single (unframed) NAL unit without the + * start code, or a sequence of NAL units with framing start code prefixes. This + * function overwrites stream buffer allocated by the codec with the supplied buffer. + */ + + var sliceNum = 0; + if (instance.options.sliceMode){ + sliceNum = instance.options.sliceNum; + + instance.decode = function decode(typedAr, parInfo, copyDoneFun) { + instance.infoAr.push(parInfo); + parInfo.startDecoding = nowValue(); + var nals = parInfo.nals; + var i; + if (!nals){ + nals = []; + parInfo.nals = nals; + var l = typedAr.length; + var foundSomething = false; + var lastFound = 0; + var lastStart = 0; + for (i = 0; i < l; ++i){ + if (typedAr[i] === 1){ + if ( + typedAr[i - 1] === 0 && + typedAr[i - 2] === 0 + ){ + var startPos = i - 2; + if (typedAr[i - 3] === 0){ + startPos = i - 3; + }; + // its a nal; + if (foundSomething){ + nals.push({ + offset: lastFound, + end: startPos, + type: typedAr[lastStart] & 31 + }); + }; + lastFound = startPos; + lastStart = startPos + 3; + if (typedAr[i - 3] === 0){ + lastStart = startPos + 4; + }; + foundSomething = true; + }; + }; + }; + if (foundSomething){ + nals.push({ + offset: lastFound, + end: i, + type: typedAr[lastStart] & 31 + }); + }; + }; + + var currentSlice = 0; + var playAr; + var offset = 0; + for (i = 0; i < nals.length; ++i){ + if (nals[i].type === 1 || nals[i].type === 5){ + if (currentSlice === sliceNum){ + playAr = typedAr.subarray(nals[i].offset, nals[i].end); + instance.streamBuffer[offset] = 0; + offset += 1; + instance.streamBuffer.set(playAr, offset); + offset += playAr.length; + }; + currentSlice += 1; + }else{ + playAr = typedAr.subarray(nals[i].offset, nals[i].end); + instance.streamBuffer[offset] = 0; + offset += 1; + instance.streamBuffer.set(playAr, offset); + offset += playAr.length; + Module._broadwayPlayStream(offset); + offset = 0; + }; + }; + copyDoneFun(); + Module._broadwayPlayStream(offset); + }; + + }else{ + instance.decode = function decode(typedAr, parInfo) { + // console.info("Decoding: " + buffer.length); + // collect infos + if (parInfo){ + instance.infoAr.push(parInfo); + parInfo.startDecoding = nowValue(); + }; + + instance.streamBuffer.set(typedAr); + Module._broadwayPlayStream(typedAr.length); + }; + }; + + if (bufferedCalls.length){ + var bi = 0; + for (bi = 0; bi < bufferedCalls.length; ++bi){ + instance.decode(bufferedCalls[bi][0], bufferedCalls[bi][1], bufferedCalls[bi][2]); + }; + bufferedCalls = []; + }; + + instance.onDecoderReady(instance); + + }); + + + }; + + + Decoder.prototype = { + + }; + + + + + /* + + asm.js implementation of a yuv to rgb convertor + provided by @soliton4 + + based on + http://www.wordsaretoys.com/2013/10/18/making-yuv-conversion-a-little-faster/ + + */ + + + // factory to create asm.js yuv -> rgb convertor for a given resolution + var asmInstances = {}; + var getAsm = function(parWidth, parHeight){ + var idStr = "" + parWidth + "x" + parHeight; + if (asmInstances[idStr]){ + return asmInstances[idStr]; + }; + + var lumaSize = parWidth * parHeight; + var chromaSize = (lumaSize|0) >> 2; + + var inpSize = lumaSize + chromaSize + chromaSize; + var outSize = parWidth * parHeight * 4; + var cacheSize = Math.pow(2, 24) * 4; + var size = inpSize + outSize + cacheSize; + + var chunkSize = Math.pow(2, 24); + var heapSize = chunkSize; + while (heapSize < size){ + heapSize += chunkSize; + }; + var heap = new ArrayBuffer(heapSize); + + var res = asmFactory(global, {}, heap); + res.init(parWidth, parHeight); + asmInstances[idStr] = res; + + res.heap = heap; + res.out = new Uint8Array(heap, 0, outSize); + res.inp = new Uint8Array(heap, outSize, inpSize); + res.outSize = outSize; + + return res; + }; + + + function asmFactory(stdlib, foreign, heap) { + "use asm"; + + var imul = stdlib.Math.imul; + var min = stdlib.Math.min; + var max = stdlib.Math.max; + var pow = stdlib.Math.pow; + var out = new stdlib.Uint8Array(heap); + var out32 = new stdlib.Uint32Array(heap); + var inp = new stdlib.Uint8Array(heap); + var mem = new stdlib.Uint8Array(heap); + var mem32 = new stdlib.Uint32Array(heap); + + // for double algo + /*var vt = 1.370705; + var gt = 0.698001; + var gt2 = 0.337633; + var bt = 1.732446;*/ + + var width = 0; + var height = 0; + var lumaSize = 0; + var chromaSize = 0; + var inpSize = 0; + var outSize = 0; + + var inpStart = 0; + var outStart = 0; + + var widthFour = 0; + + var cacheStart = 0; + + + function init(parWidth, parHeight){ + parWidth = parWidth|0; + parHeight = parHeight|0; + + var i = 0; + var s = 0; + + width = parWidth; + widthFour = imul(parWidth, 4)|0; + height = parHeight; + lumaSize = imul(width|0, height|0)|0; + chromaSize = (lumaSize|0) >> 2; + outSize = imul(imul(width, height)|0, 4)|0; + inpSize = ((lumaSize + chromaSize)|0 + chromaSize)|0; + + outStart = 0; + inpStart = (outStart + outSize)|0; + cacheStart = (inpStart + inpSize)|0; + + // initializing memory (to be on the safe side) + s = ~~(+pow(+2, +24)); + s = imul(s, 4)|0; + + for (i = 0|0; ((i|0) < (s|0))|0; i = (i + 4)|0){ + mem32[((cacheStart + i)|0) >> 2] = 0; + }; + }; + + function doit(){ + var ystart = 0; + var ustart = 0; + var vstart = 0; + + var y = 0; + var yn = 0; + var u = 0; + var v = 0; + + var o = 0; + + var line = 0; + var col = 0; + + var usave = 0; + var vsave = 0; + + var ostart = 0; + var cacheAdr = 0; + + ostart = outStart|0; + + ystart = inpStart|0; + ustart = (ystart + lumaSize|0)|0; + vstart = (ustart + chromaSize)|0; + + for (line = 0; (line|0) < (height|0); line = (line + 2)|0){ + usave = ustart; + vsave = vstart; + for (col = 0; (col|0) < (width|0); col = (col + 2)|0){ + y = inp[ystart >> 0]|0; + yn = inp[((ystart + width)|0) >> 0]|0; + + u = inp[ustart >> 0]|0; + v = inp[vstart >> 0]|0; + + cacheAdr = (((((y << 16)|0) + ((u << 8)|0))|0) + v)|0; + o = mem32[((cacheStart + cacheAdr)|0) >> 2]|0; + if (o){}else{ + o = yuv2rgbcalc(y,u,v)|0; + mem32[((cacheStart + cacheAdr)|0) >> 2] = o|0; + }; + mem32[ostart >> 2] = o; + + cacheAdr = (((((yn << 16)|0) + ((u << 8)|0))|0) + v)|0; + o = mem32[((cacheStart + cacheAdr)|0) >> 2]|0; + if (o){}else{ + o = yuv2rgbcalc(yn,u,v)|0; + mem32[((cacheStart + cacheAdr)|0) >> 2] = o|0; + }; + mem32[((ostart + widthFour)|0) >> 2] = o; + + //yuv2rgb5(y, u, v, ostart); + //yuv2rgb5(yn, u, v, (ostart + widthFour)|0); + ostart = (ostart + 4)|0; + + // next step only for y. u and v stay the same + ystart = (ystart + 1)|0; + y = inp[ystart >> 0]|0; + yn = inp[((ystart + width)|0) >> 0]|0; + + //yuv2rgb5(y, u, v, ostart); + cacheAdr = (((((y << 16)|0) + ((u << 8)|0))|0) + v)|0; + o = mem32[((cacheStart + cacheAdr)|0) >> 2]|0; + if (o){}else{ + o = yuv2rgbcalc(y,u,v)|0; + mem32[((cacheStart + cacheAdr)|0) >> 2] = o|0; + }; + mem32[ostart >> 2] = o; + + //yuv2rgb5(yn, u, v, (ostart + widthFour)|0); + cacheAdr = (((((yn << 16)|0) + ((u << 8)|0))|0) + v)|0; + o = mem32[((cacheStart + cacheAdr)|0) >> 2]|0; + if (o){}else{ + o = yuv2rgbcalc(yn,u,v)|0; + mem32[((cacheStart + cacheAdr)|0) >> 2] = o|0; + }; + mem32[((ostart + widthFour)|0) >> 2] = o; + ostart = (ostart + 4)|0; + + //all positions inc 1 + + ystart = (ystart + 1)|0; + ustart = (ustart + 1)|0; + vstart = (vstart + 1)|0; + }; + ostart = (ostart + widthFour)|0; + ystart = (ystart + width)|0; + + }; + + }; + + function yuv2rgbcalc(y, u, v){ + y = y|0; + u = u|0; + v = v|0; + + var r = 0; + var g = 0; + var b = 0; + + var o = 0; + + var a0 = 0; + var a1 = 0; + var a2 = 0; + var a3 = 0; + var a4 = 0; + + a0 = imul(1192, (y - 16)|0)|0; + a1 = imul(1634, (v - 128)|0)|0; + a2 = imul(832, (v - 128)|0)|0; + a3 = imul(400, (u - 128)|0)|0; + a4 = imul(2066, (u - 128)|0)|0; + + r = (((a0 + a1)|0) >> 10)|0; + g = (((((a0 - a2)|0) - a3)|0) >> 10)|0; + b = (((a0 + a4)|0) >> 10)|0; + + if ((((r & 255)|0) != (r|0))|0){ + r = min(255, max(0, r|0)|0)|0; + }; + if ((((g & 255)|0) != (g|0))|0){ + g = min(255, max(0, g|0)|0)|0; + }; + if ((((b & 255)|0) != (b|0))|0){ + b = min(255, max(0, b|0)|0)|0; + }; + + o = 255; + o = (o << 8)|0; + o = (o + b)|0; + o = (o << 8)|0; + o = (o + g)|0; + o = (o << 8)|0; + o = (o + r)|0; + + return o|0; + + }; + + + + return { + init: init, + doit: doit + }; + }; + + + /* + potential worker initialization + + */ + + + if (typeof self != "undefined"){ + var isWorker = false; + var decoder; + var reuseMemory = false; + var sliceMode = false; + var sliceNum = 0; + var sliceCnt = 0; + var lastSliceNum = 0; + var sliceInfoAr; + var lastBuf; + var awaiting = 0; + var pile = []; + var startDecoding; + var finishDecoding; + var timeDecoding; + + var memAr = []; + var getMem = function(length){ + if (memAr.length){ + var u = memAr.shift(); + while (u && u.byteLength !== length){ + u = memAr.shift(); + }; + if (u){ + return u; + }; + }; + return new ArrayBuffer(length); + }; + + var copySlice = function(source, target, infoAr, width, height){ + + var length = width * height; + var length4 = length / 4 + var plane2 = length; + var plane3 = length + length4; + + var copy16 = function(parBegin, parEnd){ + var i = 0; + for (i = 0; i < 16; ++i){ + var begin = parBegin + (width * i); + var end = parEnd + (width * i) + target.set(source.subarray(begin, end), begin); + }; + }; + var copy8 = function(parBegin, parEnd){ + var i = 0; + for (i = 0; i < 8; ++i){ + var begin = parBegin + ((width / 2) * i); + var end = parEnd + ((width / 2) * i) + target.set(source.subarray(begin, end), begin); + }; + }; + var copyChunk = function(begin, end){ + target.set(source.subarray(begin, end), begin); + }; + + var begin = infoAr[0]; + var end = infoAr[1]; + if (end > 0){ + copy16(begin, end); + copy8(infoAr[2], infoAr[3]); + copy8(infoAr[4], infoAr[5]); + }; + begin = infoAr[6]; + end = infoAr[7]; + if (end > 0){ + copy16(begin, end); + copy8(infoAr[8], infoAr[9]); + copy8(infoAr[10], infoAr[11]); + }; + + begin = infoAr[12]; + end = infoAr[15]; + if (end > 0){ + copyChunk(begin, end); + copyChunk(infoAr[13], infoAr[16]); + copyChunk(infoAr[14], infoAr[17]); + }; + + }; + + var sliceMsgFun = function(){}; + + var setSliceCnt = function(parSliceCnt){ + sliceCnt = parSliceCnt; + lastSliceNum = sliceCnt - 1; + }; + + + self.addEventListener('message', function(e) { + + if (isWorker){ + if (reuseMemory){ + if (e.data.reuse){ + memAr.push(e.data.reuse); + }; + }; + if (e.data.buf){ + if (sliceMode && awaiting !== 0){ + pile.push(e.data); + }else{ + decoder.decode( + new Uint8Array(e.data.buf, e.data.offset || 0, e.data.length), + e.data.info, + function(){ + if (sliceMode && sliceNum !== lastSliceNum){ + postMessage(e.data, [e.data.buf]); + }; + } + ); + }; + return; + }; + + if (e.data.slice){ + // update ref pic + var copyStart = nowValue(); + copySlice(new Uint8Array(e.data.slice), lastBuf, e.data.infos[0].sliceInfoAr, e.data.width, e.data.height); + // is it the one? then we need to update it + if (e.data.theOne){ + copySlice(lastBuf, new Uint8Array(e.data.slice), sliceInfoAr, e.data.width, e.data.height); + if (timeDecoding > e.data.infos[0].timeDecoding){ + e.data.infos[0].timeDecoding = timeDecoding; + }; + e.data.infos[0].timeCopy += (nowValue() - copyStart); + }; + // move on + postMessage(e.data, [e.data.slice]); + + // next frame in the pipe? + awaiting -= 1; + if (awaiting === 0 && pile.length){ + var data = pile.shift(); + decoder.decode( + new Uint8Array(data.buf, data.offset || 0, data.length), + data.info, + function(){ + if (sliceMode && sliceNum !== lastSliceNum){ + postMessage(data, [data.buf]); + }; + } + ); + }; + return; + }; + + if (e.data.setSliceCnt){ + setSliceCnt(e.data.sliceCnt); + return; + }; + + }else{ + if (e.data && e.data.type === "Broadway.js - Worker init"){ + isWorker = true; + decoder = new Decoder(e.data.options); + + if (e.data.options.sliceMode){ + reuseMemory = true; + sliceMode = true; + sliceNum = e.data.options.sliceNum; + setSliceCnt(e.data.options.sliceCnt); + + decoder.onPictureDecoded = function (buffer, width, height, infos) { + + // buffer needs to be copied because we give up ownership + var copyU8 = new Uint8Array(getMem(buffer.length)); + copySlice(buffer, copyU8, infos[0].sliceInfoAr, width, height); + + startDecoding = infos[0].startDecoding; + finishDecoding = infos[0].finishDecoding; + timeDecoding = finishDecoding - startDecoding; + infos[0].timeDecoding = timeDecoding; + infos[0].timeCopy = 0; + + postMessage({ + slice: copyU8.buffer, + sliceNum: sliceNum, + width: width, + height: height, + infos: infos + }, [copyU8.buffer]); // 2nd parameter is used to indicate transfer of ownership + + awaiting = sliceCnt - 1; + + lastBuf = buffer; + sliceInfoAr = infos[0].sliceInfoAr; + + }; + + }else if (e.data.options.reuseMemory){ + reuseMemory = true; + decoder.onPictureDecoded = function (buffer, width, height, infos) { + + // buffer needs to be copied because we give up ownership + var copyU8 = new Uint8Array(getMem(buffer.length)); + copyU8.set( buffer, 0, buffer.length ); + + postMessage({ + buf: copyU8.buffer, + length: buffer.length, + width: width, + height: height, + infos: infos + }, [copyU8.buffer]); // 2nd parameter is used to indicate transfer of ownership + + }; + + }else{ + decoder.onPictureDecoded = function (buffer, width, height, infos) { + if (buffer) { + buffer = new Uint8Array(buffer); + }; + + // buffer needs to be copied because we give up ownership + var copyU8 = new Uint8Array(buffer.length); + copyU8.set( buffer, 0, buffer.length ); + + postMessage({ + buf: copyU8.buffer, + length: buffer.length, + width: width, + height: height, + infos: infos + }, [copyU8.buffer]); // 2nd parameter is used to indicate transfer of ownership + + }; + }; + postMessage({ consoleLog: "broadway worker initialized" }); + }; + }; + + + }, false); + }; + + Decoder.nowValue = nowValue; + + return Decoder; + + })(); + + +})); + diff --git a/vendor/Broadway/LICENSE b/vendor/Broadway/LICENSE new file mode 100644 index 0000000..6ee9151 --- /dev/null +++ b/vendor/Broadway/LICENSE @@ -0,0 +1,10 @@ +Copyright (c) 2011, Project Authors (see AUTHORS file) +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + * Neither the names of the Project Authors nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/Broadway/avc.wasm.asset b/vendor/Broadway/avc.wasm.asset new file mode 100644 index 0000000..378ac32 Binary files /dev/null and b/vendor/Broadway/avc.wasm.asset differ diff --git a/vendor/Genymobile/scrcpy/LICENSE b/vendor/Genymobile/scrcpy/LICENSE new file mode 100644 index 0000000..b320f69 --- /dev/null +++ b/vendor/Genymobile/scrcpy/LICENSE @@ -0,0 +1,204 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright (C) 2018 Genymobile + Copyright (C) 2018-2021 Romain Vimont + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + diff --git a/vendor/Genymobile/scrcpy/scrcpy-server.jar b/vendor/Genymobile/scrcpy/scrcpy-server.jar new file mode 100644 index 0000000..30f78a2 Binary files /dev/null and b/vendor/Genymobile/scrcpy/scrcpy-server.jar differ diff --git a/vendor/h264-live-player/AUTHORS b/vendor/h264-live-player/AUTHORS new file mode 100644 index 0000000..576f75f --- /dev/null +++ b/vendor/h264-live-player/AUTHORS @@ -0,0 +1,22 @@ +The following authors have all licensed their contributions to the project +under the licensing terms detailed in LICENSE (MIT style) + +# h264-live-player migration to TypeScript +* Sergey Volkov + +# h264-live-player +* Francois Leurent @131 <131.js@cloudyks.org> + +# Broadway emscriptend h264 (broadway/Decoder.js) +* Michael Bebenita +* Alon Zakai +* Andreas Gal +* Mathieu 'p01' Henri +* Matthias 'soliton4' Behrens + +# WebGL canvas helpers +* Sam Leitch @oneam + +# AVC player inspiration +* Benjamin Xiao @urbenlegend + diff --git a/vendor/h264-live-player/Canvas.ts b/vendor/h264-live-player/Canvas.ts new file mode 100644 index 0000000..533254e --- /dev/null +++ b/vendor/h264-live-player/Canvas.ts @@ -0,0 +1,6 @@ +import Size from './utils/Size'; + +export default abstract class Canvas { + protected constructor(readonly canvas: HTMLCanvasElement, readonly size: Size) {} + public abstract decode(buffer: Uint8Array, width: number, height: number): void; +} diff --git a/vendor/h264-live-player/LICENSE b/vendor/h264-live-player/LICENSE new file mode 100644 index 0000000..f4431e9 --- /dev/null +++ b/vendor/h264-live-player/LICENSE @@ -0,0 +1,10 @@ +Copyright (c) 2019, Project Authors (see AUTHORS file) +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + * Neither the names of the Project Authors nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/h264-live-player/Program.ts b/vendor/h264-live-player/Program.ts new file mode 100644 index 0000000..21b80ca --- /dev/null +++ b/vendor/h264-live-player/Program.ts @@ -0,0 +1,49 @@ +import Shader from './Shader'; +import assert from './utils/assert'; + +export default class Program { + public readonly program: WebGLProgram | null; + + constructor(readonly gl: WebGLRenderingContext) { + this.program = this.gl.createProgram(); + } + + public attach(shader: Shader): void { + if (!this.program) { + throw Error(`Program type is ${typeof this.program}`); + } + if (!shader.shader) { + throw Error(`Shader type is ${typeof shader.shader}`); + } + this.gl.attachShader(this.program, shader.shader); + } + + public link(): void { + if (!this.program) { + throw Error(`Program type is ${typeof this.program}`); + } + this.gl.linkProgram(this.program); + // If creating the shader program failed, alert. + assert(this.gl.getProgramParameter(this.program, this.gl.LINK_STATUS), + 'Unable to initialize the shader program.'); + } + + public use(): void { + this.gl.useProgram(this.program); + } + + public getAttributeLocation(name: string): number { + if (!this.program) { + throw Error(`Program type is ${typeof this.program}`); + } + return this.gl.getAttribLocation(this.program, name); + } + + public setMatrixUniform(name: string, array: Float32List): void { + if (!this.program) { + throw Error(`Program type is ${typeof this.program}`); + } + const uniform = this.gl.getUniformLocation(this.program, name); + this.gl.uniformMatrix4fv(uniform, false, array); + } +} diff --git a/vendor/h264-live-player/README.md b/vendor/h264-live-player/README.md new file mode 100644 index 0000000..a94119a --- /dev/null +++ b/vendor/h264-live-player/README.md @@ -0,0 +1,3 @@ +Based on client code from [131/h264-live-player](https://github.com/131/h264-live-player/tree/6966af8/vendor) + +See [License](LICENSE) diff --git a/vendor/h264-live-player/Script.ts b/vendor/h264-live-player/Script.ts new file mode 100644 index 0000000..686c121 --- /dev/null +++ b/vendor/h264-live-player/Script.ts @@ -0,0 +1,29 @@ +export default class Script { + constructor(public type: string, public source: string) { + } + + public static createFromElementId(id: string): Script { + const script = document.getElementById(id); + + // Didn't find an element with the specified ID, abort. + if (!script) { + throw Error('Could not find shader with ID: ' + id); + } + + // Walk through the source element's children, building the shader source string. + let source = ''; + let currentChild = script.firstChild; + while (currentChild) { + if (currentChild.nodeType === 3) { + source += currentChild.textContent; + } + currentChild = currentChild.nextSibling; + } + + return new Script((script as HTMLScriptElement).type, source); + } + + public static createFromSource(type: string, source: string): Script { + return new Script(type, source); + } +} diff --git a/vendor/h264-live-player/Shader.ts b/vendor/h264-live-player/Shader.ts new file mode 100644 index 0000000..642b072 --- /dev/null +++ b/vendor/h264-live-player/Shader.ts @@ -0,0 +1,35 @@ +import Script from './Script'; +import error from './utils/error'; + +export default class Shader { + public readonly shader?: WebGLShader | null; + + constructor(readonly gl: WebGLRenderingContext, readonly script: Script) { + // Now figure out what type of shader script we have, based on its MIME type. + if (script.type === 'x-shader/x-fragment') { + this.shader = gl.createShader(gl.FRAGMENT_SHADER); + } else if (script.type === 'x-shader/x-vertex') { + this.shader = gl.createShader(gl.VERTEX_SHADER); + } else { + error(`Unknown shader type: ${script.type}`); + return; + } + + if (!this.shader) { + error(`Shader is ${typeof this.shader}`); + return; + } + + // Send the source to the shader object. + gl.shaderSource(this.shader, script.source); + + // Compile the shader program. + gl.compileShader(this.shader); + + // See if it compiled successfully. + if (!gl.getShaderParameter(this.shader, gl.COMPILE_STATUS)) { + error('An error occurred compiling the shaders: ' + gl.getShaderInfoLog(this.shader)); + return; + } + } +} diff --git a/vendor/h264-live-player/Texture.ts b/vendor/h264-live-player/Texture.ts new file mode 100644 index 0000000..a66c03d --- /dev/null +++ b/vendor/h264-live-player/Texture.ts @@ -0,0 +1,61 @@ +import Size from './utils/Size'; +import assert from './utils/assert'; +import Program from './Program'; + +export default class Texture { + public readonly texture: WebGLTexture | null; + public readonly format: GLenum; + private textureIDs: number[]; + + static create (gl: WebGLRenderingContext, format: number): Texture { + return new Texture(gl, undefined, format); + } + + constructor(readonly gl: WebGLRenderingContext, readonly size?: Size, format?: GLenum) { + this.texture = gl.createTexture(); + gl.bindTexture(gl.TEXTURE_2D, this.texture); + this.format = format ? format : gl.LUMINANCE; + if (size) { + gl.texImage2D(gl.TEXTURE_2D, 0, this.format, size.w, size.h, 0, this.format, gl.UNSIGNED_BYTE, null); + } + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); + this.textureIDs = [gl.TEXTURE0, gl.TEXTURE1, gl.TEXTURE2]; + } + + public fill(textureData: Uint8Array, useTexSubImage2D?: boolean, w?: number, h?: number): void { + if (typeof w === 'undefined' || typeof h === 'undefined') { + if (!this.size) { + return; + } + w = this.size.w; + h = this.size.h; + } + const gl = this.gl; + assert(textureData.length >= w * h, + 'Texture size mismatch, data:' + textureData.length + ', texture: ' + w * h); + gl.bindTexture(gl.TEXTURE_2D, this.texture); + if (useTexSubImage2D) { + gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, w, h, this.format, gl.UNSIGNED_BYTE, textureData); + } else { + // texImage2D seems to be faster, thus keeping it as the default + gl.texImage2D(gl.TEXTURE_2D, 0, this.format, w, h, 0, this.format, gl.UNSIGNED_BYTE, textureData); + } + } + + public image2dBuffer (buffer: Uint8Array, width: number, height: number) { + this.fill(buffer, false, width, height); + } + + public bind(n: number, program: Program, name: string): void { + const gl = this.gl; + if (!program.program) { + return; + } + gl.activeTexture(this.textureIDs[n]); + gl.bindTexture(gl.TEXTURE_2D, this.texture); + gl.uniform1i(gl.getUniformLocation(program.program, name), n); + } +} diff --git a/vendor/h264-live-player/WebGLCanvas.ts b/vendor/h264-live-player/WebGLCanvas.ts new file mode 100644 index 0000000..0b52e7f --- /dev/null +++ b/vendor/h264-live-player/WebGLCanvas.ts @@ -0,0 +1,305 @@ +import Size from './utils/Size'; +import Texture from './Texture'; +import error from './utils/error'; +// @ts-ignore +import { Matrix, Vector } from 'sylvester.js'; +import Program from './Program'; +import Shader from './Shader'; +import { makePerspective } from './utils/glUtils'; +import Script from './Script'; +import Canvas from './Canvas'; + +export default abstract class WebGLCanvas extends Canvas { + protected static vertexShaderScript: Script = Script.createFromSource('x-shader/x-vertex', ` + attribute vec3 aVertexPosition; + attribute vec2 aTextureCoord; + uniform mat4 uMVMatrix; + uniform mat4 uPMatrix; + varying highp vec2 vTextureCoord; + void main(void) { + gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0); + vTextureCoord = aTextureCoord; + } + `); + protected static fragmentShaderScript: Script = Script.createFromSource('x-shader/x-fragment', ` + precision highp float; + varying highp vec2 vTextureCoord; + uniform sampler2D texture; + void main(void) { + gl_FragColor = texture2D(texture, vTextureCoord); + } + `); + public quadVPBuffer?: WebGLBuffer | null; + public quadVTCBuffer?: WebGLBuffer | null; + public mvMatrix: Matrix; + public glNames?: Record; + public textureCoordAttribute?: number; + public vertexPositionAttribute?: number; + public perspectiveMatrix: Matrix; + protected gl?: WebGLRenderingContext | null; + protected framebuffer?: WebGLFramebuffer | null; + protected framebufferTexture?: Texture; + protected texture?: Texture; + protected program?: Program; + + constructor(readonly canvas: HTMLCanvasElement, readonly size: Size, useFrameBuffer: boolean) { + super(canvas, size); + this.canvas.width = size.w; + this.canvas.height = size.h; + + this.onInitWebGL(); + this.onInitShaders(); + this.initBuffers(); + + if (useFrameBuffer) { + this.initFramebuffer(); + } + + this.onInitTextures(); + this.initScene(); + } + + protected initFramebuffer(): void { + const gl = this.gl; + if (!gl) { + error(`gl type is ${typeof gl}`); + return; + } + + // Create framebuffer object and texture. + this.framebuffer = gl.createFramebuffer(); + gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer); + this.framebufferTexture = new Texture(gl, this.size, gl.RGBA); + + // Create and allocate renderbuffer for depth data. + const renderbuffer = gl.createRenderbuffer(); + gl.bindRenderbuffer(gl.RENDERBUFFER, renderbuffer); + gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, this.size.w, this.size.h); + + // Attach texture and renderbuffer to the framebuffer. + gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this.framebufferTexture.texture, 0); + gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, renderbuffer); + } + + protected initBuffers(): void { + let tmp; + const gl = this.gl; + + if (!gl) { + error(`gl type is ${typeof gl}`); + return; + } + + // Create vertex position buffer. + this.quadVPBuffer = gl.createBuffer(); + if (!this.quadVPBuffer) { + error(`quadVPBuffer type is ${typeof gl}`); + return; + } + gl.bindBuffer(gl.ARRAY_BUFFER, this.quadVPBuffer); + tmp = [ + 1.0, 1.0, 0.0, + -1.0, 1.0, 0.0, + 1.0, -1.0, 0.0, + -1.0, -1.0, 0.0]; + + gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(tmp), gl.STATIC_DRAW); + // (this.quadVPBuffer as any).itemSize = 3; + // (this.quadVPBuffer as any).numItems = 4; + + /* + +--------------------+ + | -1,1 (1) | 1,1 (0) + | | + | | + | | + | | + | | + | -1,-1 (3) | 1,-1 (2) + +--------------------+ + */ + + const scaleX = 1.0; + const scaleY = 1.0; + + // Create vertex texture coordinate buffer. + this.quadVTCBuffer = gl.createBuffer(); + gl.bindBuffer(gl.ARRAY_BUFFER, this.quadVTCBuffer); + tmp = [ + scaleX, 0.0, + 0.0, 0.0, + scaleX, scaleY, + 0.0, scaleY + ]; + gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(tmp), gl.STATIC_DRAW); + } + + protected mvIdentity() : void { + this.mvMatrix = Matrix.I(4); + } + + protected mvMultiply(m: number): void { + this.mvMatrix = this.mvMatrix.x(m); + } + + protected mvTranslate(m: number[]): void { + const $V = Vector.create; + this.mvMultiply(Matrix.Translation($V([m[0], m[1], m[2]])).ensure4x4()); + } + + protected setMatrixUniforms(): void { + if (!this.program) { + error(`Program type is ${typeof this.program}`); + return; + } + this.program.setMatrixUniform('uPMatrix', new Float32Array(this.perspectiveMatrix.flatten())); + this.program.setMatrixUniform('uMVMatrix', new Float32Array(this.mvMatrix.flatten())); + } + + protected initScene(): void { + const gl = this.gl; + + if (!gl) { + error(`gl type is ${typeof gl}`); + return; + } + + // Establish the perspective with which we want to view the + // scene. Our field of view is 45 degrees, with a width/height + // ratio of 640:480, and we only want to see objects between 0.1 units + // and 100 units away from the camera. + + this.perspectiveMatrix = makePerspective(45, 1, 0.1, 100.0); + + // Set the drawing position to the 'identity' point, which is + // the center of the scene. + this.mvIdentity(); + + // Now move the drawing position a bit to where we want to start + // drawing the square. + this.mvTranslate([0.0, 0.0, -2.4]); + + // Draw the cube by binding the array buffer to the cube's vertices + // array, setting attributes, and pushing it to GL. + gl.bindBuffer(gl.ARRAY_BUFFER, this.quadVPBuffer as WebGLBuffer); + gl.vertexAttribPointer(this.vertexPositionAttribute as number, 3, gl.FLOAT, false, 0, 0); + + // Set the texture coordinates attribute for the vertices. + + gl.bindBuffer(gl.ARRAY_BUFFER, this.quadVTCBuffer as WebGLBuffer); + gl.vertexAttribPointer(this.textureCoordAttribute as number, 2, gl.FLOAT, false, 0, 0); + + this.onInitSceneTextures(); + + this.setMatrixUniforms(); + + if (this.framebuffer) { + console.log('Bound Frame Buffer'); + gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer); + } + } + + public toString(): string { + return 'WebGLCanvas Size: ' + this.size; + } + + protected checkLastError(operation: string): void { + if (!this.gl || !this.glNames) { + return; + } + const err = this.gl.getError(); + if (err !== this.gl.NO_ERROR) { + let name = this.glNames[err]; + name = (name !== undefined) ? name + '(' + err + ')' : + ('Unknown WebGL ENUM (0x' + err.toString(16) + ')'); + if (operation) { + console.log('WebGL Error: %s, %s', operation, name); + } else { + console.log('WebGL Error: %s', name); + } + console.trace(); + } + } + + protected onInitWebGL(): void { + try { + this.gl = this.canvas.getContext('experimental-webgl', { + preserveDrawingBuffer: true + }) as WebGLRenderingContext; + } catch (e: any) { + } + + if (!this.gl) { + error('Unable to initialize WebGL. Your browser may not support it.'); + return; + } + if (this.glNames) { + return; + } + this.glNames = {}; + for (const propertyName in this.gl) { + if (this.gl.hasOwnProperty(propertyName)) { + const value = (this.gl as unknown as Record)[propertyName]; + if (typeof value === 'number') { + this.glNames[value] = propertyName; + } + } + } + } + + protected onInitShaders(): void { + const gl = this.gl; + if (!gl) { + error(`gl type is ${typeof gl}`); + return; + } + this.program = new Program(gl); + this.program.attach(new Shader(gl, WebGLCanvas.vertexShaderScript)); + this.program.attach(new Shader(gl, WebGLCanvas.fragmentShaderScript)); + this.program.link(); + this.program.use(); + this.vertexPositionAttribute = this.program.getAttributeLocation('aVertexPosition'); + gl.enableVertexAttribArray(this.vertexPositionAttribute as number); + this.textureCoordAttribute = this.program.getAttributeLocation('aTextureCoord'); + gl.enableVertexAttribArray(this.textureCoordAttribute as number); + } + + protected onInitTextures(): void { + const gl = this.gl; + if (!gl) { + error(`gl type is ${typeof gl}`); + return; + } + this.texture = new Texture(gl, this.size, gl.RGBA); + } + + protected onInitSceneTextures(): void { + if (!this.texture) { + error(`texture type is ${typeof this.texture}`); + return; + } + if (!this.program) { + error(`program type is ${typeof this.texture}`); + return; + } + this.texture.bind(0, this.program, 'texture'); + } + + protected drawScene(): void { + const gl = this.gl; + if (!gl) { + error(`gl type is ${typeof gl}`); + return; + } + gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); + } + + protected readPixels(buffer: Uint8Array): void { + const gl = this.gl; + if (!gl) { + error(`gl type is ${typeof gl}`); + return; + } + gl.readPixels(0, 0, this.size.w, this.size.h, gl.RGBA, gl.UNSIGNED_BYTE, buffer); + } +} diff --git a/vendor/h264-live-player/YUVCanvas.ts b/vendor/h264-live-player/YUVCanvas.ts new file mode 100644 index 0000000..9d99838 --- /dev/null +++ b/vendor/h264-live-player/YUVCanvas.ts @@ -0,0 +1,48 @@ +import Size from './utils/Size'; +import Canvas from './Canvas'; + +export default class YUVCanvas extends Canvas { + private canvasCtx: CanvasRenderingContext2D; + private canvasBuffer: ImageData; + + constructor(readonly canvas: HTMLCanvasElement, readonly size: Size) { + super(canvas, size); + this.canvasCtx = this.canvas.getContext('2d') as CanvasRenderingContext2D; + this.canvasBuffer = this.canvasCtx.createImageData(size.w, size.h); + } + + public decode(buffer: Uint8Array, width: number, height: number): void { + if (!buffer) { + return; + } + + const lumaSize = width * height; + const chromaSize = lumaSize >> 2; + + const ybuf = buffer.subarray(0, lumaSize); + const ubuf = buffer.subarray(lumaSize, lumaSize + chromaSize); + const vbuf = buffer.subarray(lumaSize + chromaSize, lumaSize + 2 * chromaSize); + + for (let y = 0; y < height; y++) { + for (let x = 0; x < width; x++) { + const yIndex = x + y * width; + const uIndex = ~~(y / 2) * ~~(width / 2) + ~~(x / 2); + const vIndex = ~~(y / 2) * ~~(width / 2) + ~~(x / 2); + const R = 1.164 * (ybuf[yIndex] - 16) + 1.596 * (vbuf[vIndex] - 128); + const G = 1.164 * (ybuf[yIndex] - 16) - 0.813 * (vbuf[vIndex] - 128) - 0.391 * (ubuf[uIndex] - 128); + const B = 1.164 * (ybuf[yIndex] - 16) + 2.018 * (ubuf[uIndex] - 128); + + const rgbIndex = yIndex * 4; + this.canvasBuffer.data[rgbIndex + 0] = R; + this.canvasBuffer.data[rgbIndex + 1] = G; + this.canvasBuffer.data[rgbIndex + 2] = B; + this.canvasBuffer.data[rgbIndex + 3] = 0xff; + } + } + + this.canvasCtx.putImageData(this.canvasBuffer, 0, 0); + + // const date = new Date(); + // console.log('WSAvcPlayer: Decode time: ' + (date.getTime() - this.rcvtime) + ' ms'); + } +} diff --git a/vendor/h264-live-player/YUVWebGLCanvas.ts b/vendor/h264-live-player/YUVWebGLCanvas.ts new file mode 100644 index 0000000..7e7e01d --- /dev/null +++ b/vendor/h264-live-player/YUVWebGLCanvas.ts @@ -0,0 +1,120 @@ +import WebGLCanvas from './WebGLCanvas'; +import Size from './utils/Size'; +import Program from './Program'; +import Shader from './Shader'; +import Script from './Script'; +import Texture from './Texture'; + +export default class YUVWebGLCanvas extends WebGLCanvas { + protected static vertexShaderScript: Script = Script.createFromSource('x-shader/x-vertex', ` + attribute vec3 aVertexPosition; + attribute vec2 aTextureCoord; + uniform mat4 uMVMatrix; + uniform mat4 uPMatrix; + varying highp vec2 vTextureCoord; + void main(void) { + gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0); + vTextureCoord = aTextureCoord; + } + `); + + protected static fragmentShaderScript: Script = Script.createFromSource('x-shader/x-fragment', ` + precision highp float; + varying highp vec2 vTextureCoord; + uniform sampler2D YTexture; + uniform sampler2D UTexture; + uniform sampler2D VTexture; + const mat4 YUV2RGB = mat4 + ( + 1.1643828125, 0, 1.59602734375, -.87078515625, + 1.1643828125, -.39176171875, -.81296875, .52959375, + 1.1643828125, 2.017234375, 0, -1.081390625, + 0, 0, 0, 1 + ); + + void main(void) { + gl_FragColor = vec4( + texture2D(YTexture, vTextureCoord).x, + texture2D(UTexture, vTextureCoord).x, + texture2D(VTexture, vTextureCoord).x, + 1 + ) * YUV2RGB; + }`); + + private YTexture?: Texture; + private UTexture?: Texture; + private VTexture?: Texture; + + constructor(readonly canvas: HTMLCanvasElement, readonly size: Size) { + super(canvas, size, false); + } + + protected onInitShaders(): void { + if (!this.gl) { + return; + } + this.program = new Program(this.gl); + this.program.attach(new Shader(this.gl, YUVWebGLCanvas.vertexShaderScript)); + this.program.attach(new Shader(this.gl, YUVWebGLCanvas.fragmentShaderScript)); + this.program.link(); + this.program.use(); + this.vertexPositionAttribute = this.program.getAttributeLocation('aVertexPosition'); + this.gl.enableVertexAttribArray(this.vertexPositionAttribute as number); + this.textureCoordAttribute = this.program.getAttributeLocation('aTextureCoord'); + this.gl.enableVertexAttribArray(this.textureCoordAttribute as number); + } + + protected onInitTextures(): void { + if (!this.gl) { + return; + } + this.YTexture = new Texture(this.gl, this.size); + this.UTexture = new Texture(this.gl, this.size.getHalfSize()); + this.VTexture = new Texture(this.gl, this.size.getHalfSize()); + } + + protected onInitSceneTextures(): void { + if (!this.program) { + return; + } + if (!this.YTexture || !this.UTexture || !this.VTexture) { + return; + } + this.YTexture.bind(0, this.program, 'YTexture'); + this.UTexture.bind(1, this.program, 'UTexture'); + this.VTexture.bind(2, this.program, 'VTexture'); + } + + protected fillYUVTextures(y: Uint8Array, u: Uint8Array, v: Uint8Array): void { + if (!this.YTexture || !this.UTexture || !this.VTexture) { + return; + } + this.YTexture.fill(y); + this.UTexture.fill(u); + this.VTexture.fill(v); + } + + public decode(buffer: Uint8Array, width: number, height: number): void { + + if (!buffer) { + return; + } + if (!this.YTexture || !this.UTexture || !this.VTexture) { + return; + } + + const lumaSize = width * height; + const chromaSize = lumaSize >> 2; + this.fillYUVTextures( + buffer.subarray(0, lumaSize), + buffer.subarray(lumaSize, lumaSize + chromaSize), + buffer.subarray(lumaSize + chromaSize, lumaSize + 2 * chromaSize) + ); + + this.drawScene(); + } + + public toString(): string { + return 'YUVCanvas Size: ' + this.size; + } +} diff --git a/vendor/h264-live-player/utils/Size.ts b/vendor/h264-live-player/utils/Size.ts new file mode 100644 index 0000000..296e627 --- /dev/null +++ b/vendor/h264-live-player/utils/Size.ts @@ -0,0 +1,16 @@ +/** + * Represents a 2-dimensional size value. + */ + +export default class Size { + constructor(public w: number, public h: number) {} + toString() { + return '(' + this.w + ', ' + this.h + ')'; + } + getHalfSize() { + return new Size(this.w >>> 1, this.h >>> 1); + } + length() { + return this.w * this.h; + } +} diff --git a/vendor/h264-live-player/utils/assert.ts b/vendor/h264-live-player/utils/assert.ts new file mode 100644 index 0000000..3d18746 --- /dev/null +++ b/vendor/h264-live-player/utils/assert.ts @@ -0,0 +1,8 @@ +import error from './error'; + +export default function assert(condition: boolean, message: string): void { + if (!condition) { + error(message); + throw new Error(message); + } +} diff --git a/vendor/h264-live-player/utils/error.ts b/vendor/h264-live-player/utils/error.ts new file mode 100644 index 0000000..6e2671d --- /dev/null +++ b/vendor/h264-live-player/utils/error.ts @@ -0,0 +1,4 @@ +export default function error(message: string): void { + console.error(message); + console.trace(); +} diff --git a/vendor/h264-live-player/utils/glUtils.ts b/vendor/h264-live-player/utils/glUtils.ts new file mode 100644 index 0000000..ff33219 --- /dev/null +++ b/vendor/h264-live-player/utils/glUtils.ts @@ -0,0 +1,113 @@ +// @ts-ignore +import { Matrix, Vector } from 'sylvester.js'; + +const $M = Matrix.create; + +// augment Sylvester some +Matrix.Translation = function(v: Matrix): Matrix { + if (v.elements.length === 2) { + const r = Matrix.I(3); + r.elements[2][0] = v.elements[0]; + r.elements[2][1] = v.elements[1]; + return r; + } + + if (v.elements.length === 3) { + const r = Matrix.I(4); + r.elements[0][3] = v.elements[0]; + r.elements[1][3] = v.elements[1]; + r.elements[2][3] = v.elements[2]; + return r; + } + + throw Error('Invalid length for Translation'); +}; + +Matrix.prototype.flatten = function(): number[] { + const result = []; + /* tslint:disable: no-invalid-this prefer-for-of */ + if (this.elements.length === 0) { + return []; + } + + for (let j = 0; j < this.elements[0].length; j++) { + for (let i = 0; i < this.elements.length; i++) { + result.push(this.elements[i][j]); + } + } + /* tslint:enable */ + return result; +}; + +Matrix.prototype.ensure4x4 = function(): Matrix | null { + /* tslint:disable: no-invalid-this */ + if (this.elements.length === 4 && this.elements[0].length === 4) { + return this; + } + + if (this.elements.length > 4 || this.elements[0].length > 4) { + return null; + } + + for (let i = 0; i < this.elements.length; i++) { + for (let j = this.elements[i].length; j < 4; j++) { + if (i === j) { + this.elements[i].push(1); + } else { + this.elements[i].push(0); + } + } + } + + for (let i = this.elements.length; i < 4; i++) { + if (i === 0) { + this.elements.push([1, 0, 0, 0]); + } else if (i === 1) { + this.elements.push([0, 1, 0, 0]); + } else if (i === 2) { + this.elements.push([0, 0, 1, 0]); + } else if (i === 3) { + this.elements.push([0, 0, 0, 1]); + } + } + + return this; + /* tslint:enable */ +}; + +Vector.prototype.flatten = function(): number[] { + /* tslint:disable: no-invalid-this */ + return this.elements; + /* tslint:enable */ +}; + +// +// gluPerspective +// +export function makePerspective(fovy: number, aspect: number, znear: number, zfar: number): Matrix { + const ymax = znear * Math.tan(fovy * Math.PI / 360.0); + const ymin = -ymax; + const xmin = ymin * aspect; + const xmax = ymax * aspect; + + return makeFrustum(xmin, xmax, ymin, ymax, znear, zfar); +} + +// +// glFrustum +// +function makeFrustum(left: number, right: number, + bottom: number, top: number, + znear: number, zfar: number): Matrix { + const X = 2 * znear / (right - left); + const Y = 2 * znear / (top - bottom); + const A = (right + left) / (right - left); + const B = (top + bottom) / (top - bottom); + const C = -(zfar + znear) / (zfar - znear); + const D = -2 * zfar * znear / (zfar - znear); + + return $M([[X, 0, A, 0], + [0, Y, B, 0], + [0, 0, C, D], + [0, 0, -1, 0]]); +} diff --git a/vendor/tinyh264/Canvas.ts b/vendor/tinyh264/Canvas.ts new file mode 100644 index 0000000..7d58063 --- /dev/null +++ b/vendor/tinyh264/Canvas.ts @@ -0,0 +1,4 @@ +export default abstract class Canvas { + constructor(protected readonly canvas: HTMLCanvasElement) {} + public abstract decode(buffer: Uint8Array, width: number, height: number): void; +} diff --git a/vendor/tinyh264/H264NALDecoder.worker.ts b/vendor/tinyh264/H264NALDecoder.worker.ts new file mode 100644 index 0000000..879684a --- /dev/null +++ b/vendor/tinyh264/H264NALDecoder.worker.ts @@ -0,0 +1,3 @@ +import { init } from 'tinyh264'; + +init(); diff --git a/vendor/tinyh264/LICENSE b/vendor/tinyh264/LICENSE new file mode 100644 index 0000000..f896f50 --- /dev/null +++ b/vendor/tinyh264/LICENSE @@ -0,0 +1,13 @@ +Copyright (c) 2019 Erik De Rijcke + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. \ No newline at end of file diff --git a/vendor/tinyh264/README.md b/vendor/tinyh264/README.md new file mode 100644 index 0000000..11bed82 --- /dev/null +++ b/vendor/tinyh264/README.md @@ -0,0 +1,3 @@ +Based on demo code from [udevbe/tinyh264](https://github.com/udevbe/tinyh264/tree/caf7142/demo) + +See [License](LICENSE) diff --git a/vendor/tinyh264/ShaderCompiler.ts b/vendor/tinyh264/ShaderCompiler.ts new file mode 100644 index 0000000..a28d0f2 --- /dev/null +++ b/vendor/tinyh264/ShaderCompiler.ts @@ -0,0 +1,39 @@ +/** + * Represents a WebGL shader object and provides a mechanism to load shaders from HTML + * script tags. + */ + +export default class ShaderCompiler { + /** + * @param {WebGLRenderingContext}gl + * @param {{type: string, source: string}}script + * @return {WebGLShader} + */ + static compile(gl: WebGLRenderingContext, script: { type: string; source: string }): WebGLShader | null { + let shader: WebGLShader | null; + // Now figure out what type of shader script we have, based on its MIME type. + if (script.type === 'x-shader/x-fragment') { + shader = gl.createShader(gl.FRAGMENT_SHADER); + } else if (script.type === 'x-shader/x-vertex') { + shader = gl.createShader(gl.VERTEX_SHADER); + } else { + throw new Error('Unknown shader type: ' + script.type); + } + if (!shader) { + throw new Error('Failed to create shader'); + } + + // Send the source to the shader object. + gl.shaderSource(shader, script.source); + + // Compile the shader program. + gl.compileShader(shader); + + // See if it compiled successfully. + if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { + throw new Error('An error occurred compiling the shaders: ' + gl.getShaderInfoLog(shader)); + } + + return shader; + } +} diff --git a/vendor/tinyh264/ShaderProgram.ts b/vendor/tinyh264/ShaderProgram.ts new file mode 100644 index 0000000..40660f4 --- /dev/null +++ b/vendor/tinyh264/ShaderProgram.ts @@ -0,0 +1,64 @@ +export default class ShaderProgram { + public program: WebGLProgram | null; + /** + * @param {WebGLRenderingContext}gl + */ + constructor(private gl: WebGLRenderingContext) { + this.program = this.gl.createProgram(); + } + + /** + * @param {WebGLShader}shader + */ + attach(shader: WebGLShader): void { + if (!this.program) { + throw Error(`Program type is ${typeof this.program}`); + } + this.gl.attachShader(this.program, shader); + } + + link(): void { + if (!this.program) { + throw Error(`Program type is ${typeof this.program}`); + } + this.gl.linkProgram(this.program); + // If creating the shader program failed, alert. + if (!this.gl.getProgramParameter(this.program, this.gl.LINK_STATUS)) { + console.error('Unable to initialize the shader program.'); + } + } + + use(): void { + this.gl.useProgram(this.program); + } + + /** + * @param {string}name + * @return {number} + */ + getAttributeLocation(name: string): number { + if (!this.program) { + throw Error(`Program type is ${typeof this.program}`); + } + return this.gl.getAttribLocation(this.program, name); + } + + /** + * @param {string}name + * @return {WebGLUniformLocation | null} + */ + getUniformLocation(name: string): WebGLUniformLocation | null { + if (!this.program) { + throw Error(`Program type is ${typeof this.program}`); + } + return this.gl.getUniformLocation(this.program, name); + } + + /** + * @param {WebGLUniformLocation}uniformLocation + * @param {Array}array + */ + setUniformM4(uniformLocation: WebGLUniformLocation, array: number[]): void { + this.gl.uniformMatrix4fv(uniformLocation, false, array); + } +} diff --git a/vendor/tinyh264/ShaderSources.ts b/vendor/tinyh264/ShaderSources.ts new file mode 100644 index 0000000..f5309f9 --- /dev/null +++ b/vendor/tinyh264/ShaderSources.ts @@ -0,0 +1,50 @@ +/** + * @type {{type: string, source: string}} + */ +export const vertexQuad = { + type: 'x-shader/x-vertex', + source: ` + precision mediump float; + + uniform mat4 u_projection; + attribute vec2 a_position; + attribute vec2 a_texCoord; + varying vec2 v_texCoord; + void main(){ + v_texCoord = a_texCoord; + gl_Position = u_projection * vec4(a_position, 0.0, 1.0); + } +`, +}; + +/** + * @type {{type: string, source: string}} + */ +export const fragmentYUV = { + type: 'x-shader/x-fragment', + source: ` + precision lowp float; + + varying vec2 v_texCoord; + + uniform sampler2D yTexture; + uniform sampler2D uTexture; + uniform sampler2D vTexture; + + const mat4 conversion = mat4( + 1.0, 0.0, 1.402, -0.701, + 1.0, -0.344, -0.714, 0.529, + 1.0, 1.772, 0.0, -0.886, + 0.0, 0.0, 0.0, 0.0 + ); + + void main(void) { + float yChannel = texture2D(yTexture, v_texCoord).x; + float uChannel = texture2D(uTexture, v_texCoord).x; + float vChannel = texture2D(vTexture, v_texCoord).x; + vec4 channels = vec4(yChannel, uChannel, vChannel, 1.0); + vec3 rgb = (channels * conversion).xyz; + gl_FragColor = vec4(rgb, 1.0); + } +`, +}; diff --git a/vendor/tinyh264/YUVCanvas.ts b/vendor/tinyh264/YUVCanvas.ts new file mode 100644 index 0000000..1bfbbc4 --- /dev/null +++ b/vendor/tinyh264/YUVCanvas.ts @@ -0,0 +1,45 @@ +import Canvas from './Canvas'; + +export default class YUVCanvas extends Canvas { + private canvasCtx: CanvasRenderingContext2D; + private canvasBuffer: ImageData | null = null; + + constructor(canvas: HTMLCanvasElement) { + super(canvas); + this.canvasCtx = this.canvas.getContext('2d') as CanvasRenderingContext2D; + } + public decode(buffer: Uint8Array, width: number, height: number): void { + if (!buffer) { + return; + } + if (!this.canvasBuffer) { + this.canvasBuffer = this.canvasCtx.createImageData(width, height); + } + + const lumaSize = width * height; + const chromaSize = lumaSize >> 2; + + const ybuf = buffer.subarray(0, lumaSize); + const ubuf = buffer.subarray(lumaSize, lumaSize + chromaSize); + const vbuf = buffer.subarray(lumaSize + chromaSize, lumaSize + 2 * chromaSize); + + for (let y = 0; y < height; y++) { + for (let x = 0; x < width; x++) { + const yIndex = x + y * width; + const uIndex = ~~(y / 2) * ~~(width / 2) + ~~(x / 2); + const vIndex = ~~(y / 2) * ~~(width / 2) + ~~(x / 2); + const R = 1.164 * (ybuf[yIndex] - 16) + 1.596 * (vbuf[vIndex] - 128); + const G = 1.164 * (ybuf[yIndex] - 16) - 0.813 * (vbuf[vIndex] - 128) - 0.391 * (ubuf[uIndex] - 128); + const B = 1.164 * (ybuf[yIndex] - 16) + 2.018 * (ubuf[uIndex] - 128); + + const rgbIndex = yIndex * 4; + this.canvasBuffer.data[rgbIndex + 0] = R; + this.canvasBuffer.data[rgbIndex + 1] = G; + this.canvasBuffer.data[rgbIndex + 2] = B; + this.canvasBuffer.data[rgbIndex + 3] = 0xff; + } + } + + this.canvasCtx.putImageData(this.canvasBuffer, 0, 0); + } +} diff --git a/vendor/tinyh264/YUVSurfaceShader.ts b/vendor/tinyh264/YUVSurfaceShader.ts new file mode 100644 index 0000000..d2bb4fb --- /dev/null +++ b/vendor/tinyh264/YUVSurfaceShader.ts @@ -0,0 +1,145 @@ +import ShaderProgram from './ShaderProgram'; +import ShaderCompiler from './ShaderCompiler'; +import { fragmentYUV, vertexQuad } from './ShaderSources'; +import Texture from '../h264-live-player/Texture'; + +type ShaderArguments = { + yTexture: WebGLUniformLocation | null; + uTexture: WebGLUniformLocation | null; + vTexture: WebGLUniformLocation | null; + u_projection: WebGLUniformLocation | null; + a_position: number; + a_texCoord: number; +}; + +export default class YUVSurfaceShader { + /** + * + * @param {WebGLRenderingContext} gl + * @returns {YUVSurfaceShader} + */ + static create(gl: WebGLRenderingContext): YUVSurfaceShader { + const program = this._initShaders(gl); + const shaderArgs = this._initShaderArgs(gl, program); + const vertexBuffer = this._initBuffers(gl); + + return new YUVSurfaceShader(gl, vertexBuffer as WebGLBuffer, shaderArgs, program); + } + + static _initShaders(gl: WebGLRenderingContext): ShaderProgram { + const program = new ShaderProgram(gl); + program.attach(ShaderCompiler.compile(gl, vertexQuad) as WebGLShader); + program.attach(ShaderCompiler.compile(gl, fragmentYUV) as WebGLShader); + program.link(); + program.use(); + + return program; + } + + static _initShaderArgs(gl: WebGLRenderingContext, program: ShaderProgram): ShaderArguments { + // find shader arguments + const shaderArgs: ShaderArguments = { + yTexture: program.getUniformLocation('yTexture'), + uTexture: program.getUniformLocation('uTexture'), + vTexture: program.getUniformLocation('vTexture'), + u_projection: program.getUniformLocation('u_projection'), + a_position: program.getAttributeLocation('a_position'), + a_texCoord: program.getAttributeLocation('a_texCoord'), + }; + + gl.enableVertexAttribArray(shaderArgs.a_position); + gl.enableVertexAttribArray(shaderArgs.a_texCoord); + + return shaderArgs; + } + + static _initBuffers(gl: WebGLRenderingContext): WebGLBuffer | null { + // Create vertex buffer object. + return gl.createBuffer(); + } + + constructor( + private gl: WebGLRenderingContext, + private vertexBuffer: WebGLBuffer, + private shaderArgs: ShaderArguments, + private program: ShaderProgram, + ) {} + + /** + * + * @param {Texture} textureY + * @param {Texture} textureU + * @param {Texture} textureV + */ + setTexture(textureY: Texture, textureU: Texture, textureV: Texture): void { + const gl = this.gl; + + gl.uniform1i(this.shaderArgs.yTexture, 0); + gl.uniform1i(this.shaderArgs.uTexture, 1); + gl.uniform1i(this.shaderArgs.vTexture, 2); + + gl.activeTexture(gl.TEXTURE0); + gl.bindTexture(gl.TEXTURE_2D, textureY.texture); + + gl.activeTexture(gl.TEXTURE1); + gl.bindTexture(gl.TEXTURE_2D, textureU.texture); + + gl.activeTexture(gl.TEXTURE2); + gl.bindTexture(gl.TEXTURE_2D, textureV.texture); + } + + use(): void { + this.program.use(); + } + + release(): void { + this.gl.useProgram(null); + } + + /** + * @param {{w:number, h:number}}encodedFrameSize + * @param {{maxXTexCoord:number, maxYTexCoord:number}} h264RenderState + */ + updateShaderData( + encodedFrameSize: { w: number; h: number }, + h264RenderState: { maxXTexCoord: number; maxYTexCoord: number }, + ): void { + const { w, h } = encodedFrameSize; + this.gl.viewport(0, 0, w, h); + // prettier-ignore + this.program.setUniformM4(this.shaderArgs.u_projection as WebGLUniformLocation, [ + 2.0 / w, 0, 0, 0, + 0, 2.0 / -h, 0, 0, + 0, 0, 1, 0, + -1, 1, 0, 1 + ]) + this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.vertexBuffer); + // prettier-ignore + this.gl.bufferData(this.gl.ARRAY_BUFFER, new Float32Array([ + // First triangle + // top left: + 0, 0, 0, 0, + // top right: + w, 0, h264RenderState.maxXTexCoord, 0, + // bottom right: + w, h, h264RenderState.maxXTexCoord, h264RenderState.maxYTexCoord, + + // Second triangle + // bottom right: + w, h, h264RenderState.maxXTexCoord, h264RenderState.maxYTexCoord, + // bottom left: + 0, h, 0, h264RenderState.maxYTexCoord, + // top left: + 0, 0, 0, 0 + ]), this.gl.DYNAMIC_DRAW); + this.gl.vertexAttribPointer(this.shaderArgs.a_position, 2, this.gl.FLOAT, false, 16, 0); + this.gl.vertexAttribPointer(this.shaderArgs.a_texCoord, 2, this.gl.FLOAT, false, 16, 8); + } + + draw(): void { + const gl = this.gl; + gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT | gl.STENCIL_BUFFER_BIT); + gl.drawArrays(this.gl.TRIANGLE_STRIP, 0, 6); + gl.bindTexture(gl.TEXTURE_2D, null); + } +} diff --git a/vendor/tinyh264/YUVWebGLCanvas.ts b/vendor/tinyh264/YUVWebGLCanvas.ts new file mode 100644 index 0000000..e9a765c --- /dev/null +++ b/vendor/tinyh264/YUVWebGLCanvas.ts @@ -0,0 +1,66 @@ +/** + * based on tinyh264 demo: https://github.com/udevbe/tinyh264/tree/master/demo + */ + +import YUVSurfaceShader from './YUVSurfaceShader'; +import Texture from '../h264-live-player/Texture'; +import Canvas from './Canvas'; + +export default class YUVWebGLCanvas extends Canvas { + private yTexture: Texture; + private uTexture: Texture; + private vTexture: Texture; + private yuvSurfaceShader: YUVSurfaceShader; + + constructor(canvas: HTMLCanvasElement) { + super(canvas); + const gl = canvas.getContext('experimental-webgl', { + preserveDrawingBuffer: true, + }) as WebGLRenderingContext | null; + if (!gl) { + throw new Error('Unable to initialize WebGL. Your browser may not support it.'); + } + this.yuvSurfaceShader = YUVSurfaceShader.create(gl); + this.yTexture = Texture.create(gl, gl.LUMINANCE); + this.uTexture = Texture.create(gl, gl.LUMINANCE); + this.vTexture = Texture.create(gl, gl.LUMINANCE); + } + + decode(buffer: Uint8Array, width: number, height: number): void { + this.canvas.width = width; + this.canvas.height = height; + + // the width & height returned are actually padded, so we have to use the frame size to get the real image dimension + // when uploading to texture + const stride = width; // stride + // height is padded with filler rows + + // if we knew the size of the video before encoding, we could cut out the black filler pixels. We don't, so just set + // it to the size after encoding + const sourceWidth = width; + const sourceHeight = height; + const maxXTexCoord = sourceWidth / stride; + const maxYTexCoord = sourceHeight / height; + + const lumaSize = stride * height; + const chromaSize = lumaSize >> 2; + + const yBuffer = buffer.subarray(0, lumaSize); + const uBuffer = buffer.subarray(lumaSize, lumaSize + chromaSize); + const vBuffer = buffer.subarray(lumaSize + chromaSize, lumaSize + 2 * chromaSize); + + const chromaHeight = height >> 1; + const chromaStride = stride >> 1; + + // we upload the entire image, including stride padding & filler rows. The actual visible image will be mapped + // from texture coordinates as to crop out stride padding & filler rows using maxXTexCoord and maxYTexCoord. + + this.yTexture.image2dBuffer(yBuffer, stride, height); + this.uTexture.image2dBuffer(uBuffer, chromaStride, chromaHeight); + this.vTexture.image2dBuffer(vBuffer, chromaStride, chromaHeight); + + this.yuvSurfaceShader.setTexture(this.yTexture, this.uTexture, this.vTexture); + this.yuvSurfaceShader.updateShaderData({ w: width, h: height }, { maxXTexCoord, maxYTexCoord }); + this.yuvSurfaceShader.draw(); + } +} diff --git a/watch_node.exe b/watch_node.exe new file mode 100644 index 0000000..627c19a Binary files /dev/null and b/watch_node.exe differ diff --git a/webpack/build.config.utils.ts b/webpack/build.config.utils.ts new file mode 100644 index 0000000..2e509e4 --- /dev/null +++ b/webpack/build.config.utils.ts @@ -0,0 +1,40 @@ +import * as fs from 'fs'; +import * as path from 'path'; + +type BuildConfig = Record; + +const DEFAULT_CONFIG_PATH = path.resolve(path.dirname(__filename), 'default.build.config.json'); +const configCache: Map = new Map(); +const mergedCache: Map = new Map(); + +export function getConfig(filename: string): BuildConfig { + let cached = configCache.get(filename); + if (!cached) { + const filtered: BuildConfig = {}; + const absolutePath = path.isAbsolute(filename) ? filename : path.resolve(process.cwd(), filename); + const rawConfig = JSON.parse(fs.readFileSync(absolutePath).toString()); + Object.keys(rawConfig).forEach((key) => { + const value = rawConfig[key]; + if (typeof value === 'boolean' || typeof value === 'string') { + filtered[key] = value; + } + }); + cached = filtered; + configCache.set(filename, cached); + } + return cached; +} + +export function mergeWithDefaultConfig(custom?: string): BuildConfig { + if (!custom) { + return getConfig(DEFAULT_CONFIG_PATH); + } + let cached = mergedCache.get(custom); + if (!cached) { + const defaultConfig = getConfig(DEFAULT_CONFIG_PATH); + const customConfig = getConfig(custom); + cached = Object.assign({}, defaultConfig, customConfig); + mergedCache.set(custom, cached); + } + return cached; +} diff --git a/webpack/default.build.config.json b/webpack/default.build.config.json new file mode 100644 index 0000000..740d3e5 --- /dev/null +++ b/webpack/default.build.config.json @@ -0,0 +1,15 @@ +{ + "SCRCPY_LISTENS_ON_ALL_INTERFACES": true, + "USE_WEBCODECS": true, + "USE_BROADWAY": true, + "USE_H264_CONVERTER": true, + "USE_TINY_H264": true, + "USE_WDA_MJPEG_SERVER": false, + "USE_QVH_SERVER": true, + "INCLUDE_DEV_TOOLS": true, + "INCLUDE_ADB_SHELL": true, + "INCLUDE_FILE_LISTING": true, + "INCLUDE_APPL": false, + "INCLUDE_GOOG": true, + "PATHNAME": "/" +} diff --git a/webpack/ws-scrcpy.common.ts b/webpack/ws-scrcpy.common.ts new file mode 100644 index 0000000..1d7cfbf --- /dev/null +++ b/webpack/ws-scrcpy.common.ts @@ -0,0 +1,160 @@ +import nodeExternals from 'webpack-node-externals'; +import fs from 'fs'; +import path from 'path'; +import webpack from 'webpack'; +import MiniCssExtractPlugin from 'mini-css-extract-plugin'; +import HtmlWebpackPlugin from 'html-webpack-plugin'; +import GeneratePackageJsonPlugin from '@dead50f7/generate-package-json-webpack-plugin'; +import { mergeWithDefaultConfig } from './build.config.utils'; + +export const PROJECT_ROOT = path.resolve(__dirname, '..'); +export const SERVER_DIST_PATH = path.join(PROJECT_ROOT, 'dist'); +export const CLIENT_DIST_PATH = path.join(PROJECT_ROOT, 'dist/public'); +const PACKAGE_JSON = path.join(PROJECT_ROOT, 'package.json'); + +const override = path.join(PROJECT_ROOT, '/build.config.override.json'); +const buildConfigOptions = mergeWithDefaultConfig(override); +const buildConfigDefinePlugin = new webpack.DefinePlugin({ + '__PATHNAME__': JSON.stringify(buildConfigOptions.PATHNAME), +}); + +export const common = () => { + return { + module: { + rules: [ + { + test: /\.css$/i, + use: [MiniCssExtractPlugin.loader, 'css-loader'], + }, + { + test: /\.tsx?$/, + use: [ + { loader: 'ts-loader' }, + { + loader: 'ifdef-loader', + options: buildConfigOptions, + }, + ], + exclude: /node_modules/, + }, + { + test: /\.worker\.js$/, + use: { loader: 'worker-loader' }, + }, + { + test: /\.svg$/, + loader: 'svg-inline-loader', + }, + { + test: /\.(png|jpe?g|gif)$/i, + use: [ + { + loader: 'file-loader', + }, + ], + }, + { + test: /\.(asset)$/i, + use: [ + { + loader: 'file-loader', + options: { + name: '[name]', + }, + }, + ], + }, + { + test: /\.jar$/, + use: [ + { + loader: 'file-loader', + options: { + name: '[path][name].[ext]', + }, + }, + ], + }, + { + test: /LICENSE/i, + use: [ + { + loader: 'file-loader', + options: { + name: '[path][name]', + }, + }, + ], + }, + ], + }, + resolve: { + extensions: ['.tsx', '.ts', '.js'], + }, + }; +}; + +const front: webpack.Configuration = { + entry: path.join(PROJECT_ROOT, './src/app/index.ts'), + externals: ['fs'], + plugins: [ + new HtmlWebpackPlugin({ + template: path.join(PROJECT_ROOT, '/src/public/index.html'), + inject: 'head', + }), + new MiniCssExtractPlugin(), + new webpack.ProvidePlugin({ + Buffer: ['buffer', 'Buffer'], + }), + ], + resolve: { + fallback: { + path: 'path-browserify', + }, + extensions: ['.tsx', '.ts', '.js'], + }, + output: { + filename: 'bundle.js', + path: CLIENT_DIST_PATH, + }, +}; + +export const frontend = () => { + return Object.assign({}, common(), front); +}; + +const packageJson = JSON.parse(fs.readFileSync(PACKAGE_JSON).toString()); +const { name, version, description, author, license, scripts } = packageJson; +const basePackage = { + name, + version, + description, + author, + license, + scripts: { start: scripts['script:dist:start'] }, +}; +delete packageJson.dependencies; +delete packageJson.devDependencies; + +const back: webpack.Configuration = { + entry: path.join(PROJECT_ROOT, './src/server/index.ts'), + externals: [nodeExternals()], + plugins: [ + new GeneratePackageJsonPlugin(basePackage), + buildConfigDefinePlugin, + ], + node: { + global: false, + __filename: false, + __dirname: false, + }, + output: { + filename: 'index.js', + path: SERVER_DIST_PATH, + }, + target: 'node', +}; + +export const backend = () => { + return Object.assign({}, common(), back); +}; diff --git a/webpack/ws-scrcpy.dev.ts b/webpack/ws-scrcpy.dev.ts new file mode 100644 index 0000000..a138e74 --- /dev/null +++ b/webpack/ws-scrcpy.dev.ts @@ -0,0 +1,16 @@ +import { frontend, backend } from './ws-scrcpy.common'; +import webpack from 'webpack'; + +const devOpts: webpack.Configuration = { + devtool: 'inline-source-map', + mode: 'development', +}; + +const front = () => { + return Object.assign({}, frontend(), devOpts); +}; +const back = () => { + return Object.assign({}, backend(), devOpts); +}; + +module.exports = [front, back]; diff --git a/webpack/ws-scrcpy.prod.ts b/webpack/ws-scrcpy.prod.ts new file mode 100644 index 0000000..2ee342f --- /dev/null +++ b/webpack/ws-scrcpy.prod.ts @@ -0,0 +1,15 @@ +import { backend, frontend } from './ws-scrcpy.common'; +import webpack from 'webpack'; + +const prodOpts: webpack.Configuration = { + mode: 'production', +}; + +const front = () => { + return Object.assign({}, frontend(), prodOpts); +}; +const back = () => { + return Object.assign({}, backend(), prodOpts); +}; + +module.exports = [front, back];