├── .eslintignore ├── .eslintrc ├── .gitignore ├── .npmrc ├── .prettierrc ├── LICENSE ├── README.md ├── SECURITY.md ├── build.config.override.json ├── config.example.yaml ├── docs ├── Devtools.md ├── debug.md └── scheme.md ├── package-lock.json ├── package.json ├── src ├── app │ ├── Attribute.ts │ ├── DisplayInfo.ts │ ├── ErrorHandler.ts │ ├── MotionEvent.ts │ ├── Point.ts │ ├── Position.ts │ ├── Rect.ts │ ├── ScreenInfo.ts │ ├── Size.ts │ ├── UIEventsCode.ts │ ├── Util.ts │ ├── VideoSettings.ts │ ├── applDevice │ │ ├── client │ │ │ ├── DeviceTracker.ts │ │ │ ├── StreamClient.ts │ │ │ ├── StreamClientMJPEG.ts │ │ │ ├── StreamClientQVHack.ts │ │ │ ├── StreamReceiverQVHack.ts │ │ │ └── WdaProxyClient.ts │ │ └── toolbox │ │ │ ├── ApplMjpegMoreBox.ts │ │ │ ├── ApplMoreBox.ts │ │ │ └── ApplToolBox.ts │ ├── client │ │ ├── BaseClient.ts │ │ ├── BaseDeviceTracker.ts │ │ ├── HostTracker.ts │ │ ├── ManagerClient.ts │ │ ├── StreamReceiver.ts │ │ └── Tool.d.ts │ ├── controlMessage │ │ ├── CommandControlMessage.ts │ │ ├── ControlMessage.ts │ │ ├── KeyCodeControlMessage.ts │ │ ├── ScrollControlMessage.ts │ │ ├── TextControlMessage.ts │ │ └── TouchControlMessage.ts │ ├── googDevice │ │ ├── DeviceMessage.ts │ │ ├── DragAndDropHandler.ts │ │ ├── DragAndPushLogger.ts │ │ ├── Entry.ts │ │ ├── KeyInputHandler.ts │ │ ├── KeyToCodeMap.ts │ │ ├── Stats.ts │ │ ├── android │ │ │ ├── KeyEvent.ts │ │ │ └── MediaFormat.ts │ │ ├── client │ │ │ ├── ConfigureScrcpy.ts │ │ │ ├── DeviceTracker.ts │ │ │ ├── DevtoolsClient.ts │ │ │ ├── FileListingClient.ts │ │ │ ├── ShellClient.ts │ │ │ ├── StreamClientScrcpy.ts │ │ │ └── StreamReceiverScrcpy.ts │ │ ├── filePush │ │ │ ├── AdbkitFilePushStream.ts │ │ │ ├── FilePushHandler.ts │ │ │ ├── FilePushResponseStatus.ts │ │ │ ├── FilePushStream.ts │ │ │ └── ScrcpyFilePushStream.ts │ │ └── toolbox │ │ │ ├── GoogMoreBox.ts │ │ │ └── GoogToolBox.ts │ ├── index.ts │ ├── interactionHandler │ │ ├── FeaturedInteractionHandler.ts │ │ ├── InteractionHandler.ts │ │ └── SimpleInteractionHandler.ts │ ├── player │ │ ├── BaseCanvasBasedPlayer.ts │ │ ├── BasePlayer.ts │ │ ├── BroadwayPlayer.ts │ │ ├── MjpegPlayer.ts │ │ ├── MsePlayer.ts │ │ ├── MsePlayerForQVHack.ts │ │ ├── TinyH264Player.ts │ │ └── WebCodecsPlayer.ts │ ├── toolbox │ │ ├── ToolBox.ts │ │ ├── ToolBoxButton.ts │ │ ├── ToolBoxCheckbox.ts │ │ └── ToolBoxElement.ts │ └── ui │ │ ├── HtmlTag.ts │ │ └── SvgImage.ts ├── common │ ├── Action.ts │ ├── ChannelCode.ts │ ├── Constants.ts │ ├── ControlCenterCommand.ts │ ├── DeviceState.ts │ ├── HostTrackerMessage.ts │ ├── ProductType.ts │ ├── TypedEmitter.ts │ ├── WDAMethod.ts │ └── WdaStatus.ts ├── packages │ └── multiplexer │ │ ├── CloseEventClass.ts │ │ ├── ErrorEventClass.ts │ │ ├── Event.ts │ │ ├── Message.ts │ │ ├── MessageEventClass.ts │ │ ├── MessageType.ts │ │ └── Multiplexer.ts ├── public │ ├── images │ │ ├── buttons │ │ │ ├── arrow_back.svg │ │ │ ├── cancel.svg │ │ │ ├── menu.svg │ │ │ ├── offline.svg │ │ │ ├── refresh.svg │ │ │ ├── settings.svg │ │ │ ├── toggle_off.svg │ │ │ └── toggle_on.svg │ │ ├── multitouch │ │ │ ├── SOURCE │ │ │ ├── center_point.png │ │ │ ├── center_point_2x.png │ │ │ ├── touch_point.png │ │ │ └── touch_point_2x.png │ │ └── skin-light │ │ │ ├── SOURCE │ │ │ ├── System_Back_678.svg │ │ │ ├── System_Home_678.svg │ │ │ ├── System_Overview_678.svg │ │ │ ├── ic_keyboard_678_48dp.svg │ │ │ ├── ic_more_horiz_678_48dp.svg │ │ │ ├── ic_photo_camera_678_48dp.svg │ │ │ ├── ic_power_settings_new_678_48px.svg │ │ │ ├── ic_volume_down_678_48px.svg │ │ │ └── ic_volume_up_678_48px.svg │ └── index.html ├── server │ ├── Config.ts │ ├── EnvName.ts │ ├── Utils.ts │ ├── appl-device │ │ ├── mw │ │ │ ├── DeviceTracker.ts │ │ │ ├── QVHStreamProxy.ts │ │ │ └── WebDriverAgentProxy.ts │ │ └── services │ │ │ ├── ControlCenter.ts │ │ │ ├── QvhackRunner.ts │ │ │ └── WDARunner.ts │ ├── goog-device │ │ ├── AdbUtils.ts │ │ ├── Device.ts │ │ ├── Properties.ts │ │ ├── ScrcpyServer.ts │ │ ├── ServerVersion.ts │ │ ├── adb │ │ │ ├── ExtendedClient.ts │ │ │ ├── ExtendedSync.ts │ │ │ ├── command │ │ │ │ └── host-transport │ │ │ │ │ └── sync.ts │ │ │ └── index.ts │ │ ├── filePush │ │ │ ├── FilePushReader.ts │ │ │ └── ReadStream.ts │ │ ├── mw │ │ │ ├── DeviceTracker.ts │ │ │ ├── FileListing.ts │ │ │ ├── RemoteDevtools.ts │ │ │ ├── RemoteShell.ts │ │ │ └── WebsocketProxyOverAdb.ts │ │ └── services │ │ │ └── ControlCenter.ts │ ├── index.ts │ ├── mw │ │ ├── HostTracker.ts │ │ ├── MjpegProxyFactory.ts │ │ ├── Mw.ts │ │ ├── WebsocketMultiplexer.ts │ │ └── WebsocketProxy.ts │ └── services │ │ ├── BaseControlCenter.ts │ │ ├── HttpServer.ts │ │ ├── ProcessRunner.ts │ │ ├── Service.ts │ │ └── WebSocketServer.ts ├── style │ ├── app.css │ ├── devicelist.css │ ├── devtools.css │ ├── dialog.css │ ├── filelisting.css │ └── morebox.css └── types │ ├── ApplDeviceDescriptor.d.ts │ ├── BaseDeviceDescriptor.d.ts │ ├── Configuration.d.ts │ ├── DeviceTrackerEvent.ts │ ├── DeviceTrackerEventList.ts │ ├── FileStats.ts │ ├── GoogDeviceDescriptor.d.ts │ ├── Message.d.ts │ ├── MessageFileListing.d.ts │ ├── MessageRunWdaResponse.ts │ ├── MessageXtermClient.ts │ ├── NetInterface.d.ts │ ├── ParamsBase.ts │ ├── ParamsDeviceTracker.ts │ ├── ParamsDevtools.d.ts │ ├── ParamsFileListing.d.ts │ ├── ParamsShell.d.ts │ ├── ParamsStream.ts │ ├── ParamsStreamScrcpy.d.ts │ ├── ParamsWdaProxy.d.ts │ ├── RemoteDevtools.d.ts │ ├── RemoteDevtoolsCommand.ts │ ├── ReplyFileListing.d.ts │ ├── WdaServer.d.ts │ └── XtermMessage.d.ts ├── tsconfig.json ├── typings ├── appium-base-driver │ ├── index.d.ts │ └── lib │ │ └── basedriver │ │ └── device-settings.d.ts ├── appium-support │ ├── build │ │ └── lib │ │ │ └── timing.d.ts │ └── index.d.ts ├── appium-xcuitest-driver │ ├── build │ │ └── lib │ │ │ ├── device-connections-factory.d.ts │ │ │ ├── driver.d.ts │ │ │ └── server.d.ts │ └── index.d.ts ├── build-config.d.ts ├── custom_png.d.ts ├── custom_svg.d.ts ├── node-mjpeg-proxy │ └── index.d.ts ├── tinyh264.d.ts └── worker-loader.d.ts ├── vendor ├── Broadway │ ├── AUTHORS │ ├── Decoder.d.ts │ ├── Decoder.js │ ├── LICENSE │ └── avc.wasm.asset ├── Genymobile │ └── scrcpy │ │ ├── LICENSE │ │ └── scrcpy-server.jar ├── h264-live-player │ ├── AUTHORS │ ├── Canvas.ts │ ├── LICENSE │ ├── Program.ts │ ├── README.md │ ├── Script.ts │ ├── Shader.ts │ ├── Texture.ts │ ├── WebGLCanvas.ts │ ├── YUVCanvas.ts │ ├── YUVWebGLCanvas.ts │ └── utils │ │ ├── Size.ts │ │ ├── assert.ts │ │ ├── error.ts │ │ └── glUtils.ts └── tinyh264 │ ├── Canvas.ts │ ├── H264NALDecoder.worker.ts │ ├── LICENSE │ ├── README.md │ ├── ShaderCompiler.ts │ ├── ShaderProgram.ts │ ├── ShaderSources.ts │ ├── YUVCanvas.ts │ ├── YUVSurfaceShader.ts │ └── YUVWebGLCanvas.ts └── webpack ├── build.config.utils.ts ├── default.build.config.json ├── ws-scrcpy.common.ts ├── ws-scrcpy.dev.ts └── ws-scrcpy.prod.ts /.eslintignore: -------------------------------------------------------------------------------- 1 | src/public/**/*.js 2 | vendor/**/*.js 3 | vendor/**/*.ts 4 | src/app/Util.ts 5 | *.js 6 | typings/**/*.d.ts 7 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "plugin:@typescript-eslint/recommended", 4 | "plugin:prettier/recommended", 5 | "prettier" 6 | ], 7 | "plugins": [ 8 | "progress", 9 | "@typescript-eslint", 10 | "prettier" 11 | ], 12 | "parserOptions": { 13 | "ecmaVersion": 2020, 14 | "sourceType": "module" 15 | }, 16 | "rules": { 17 | "progress/activate": 1, 18 | "import/no-absolute-path": "off" 19 | }, 20 | "overrides": [ 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /dist 3 | /build 4 | /.idea 5 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | include=dev 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "trailingComma": "all", 4 | "singleQuote": true, 5 | "printWidth": 120, 6 | "tabWidth": 4 7 | } 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2021 by Netris, JSC. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Reporting Security Issues 2 | 3 | If you discover a security issue, please report it by sending an 4 | email to [drauggres@gmail.com](mailto:drauggres@gmail.com). 5 | -------------------------------------------------------------------------------- /build.config.override.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /config.example.yaml: -------------------------------------------------------------------------------- 1 | # Run configuration example. See full config file spec in src/types/Configuration.d.ts 2 | 3 | # Device trackers 4 | ## track android devices (default: true, if was INCLUDE_GOOG enabled in build config) 5 | runGoogTracker: false 6 | ## track iOS devices (default: true, if was INCLUDE_APPL enabled in build config) 7 | runApplTracker: false; 8 | 9 | # HTTP[s] servers configuration 10 | server: 11 | - secure: false 12 | port: 8000 13 | redirectToSecure: 14 | port: 8443 15 | host: first-mobile-stand.example.com 16 | - secure: true 17 | port: 8443 18 | options: 19 | certPath: /Users/example/ssl/STAR_example_com.crt 20 | keyPath: /Users/example/ssl/STAR_example_com.key 21 | 22 | # Announce remote device trackers. The server doesn't check their availability. 23 | remoteHostList: 24 | - useProxy: true # optional, default: false 25 | type: android # required, "android" | "ios" | ["android", "ios"] 26 | secure: true # required, false for HTTP, true for HTTPS 27 | hostname: second-mobile-stand.example.com 28 | port: 8443 29 | - useProxy: true 30 | type: ios 31 | secure: true 32 | hostname: second-mobile-stand.example.com 33 | port: 8443 34 | - useProxy: true 35 | type: # short variant 36 | - ios 37 | - android 38 | secure: true 39 | hostname: third-mobile-stand.example.com 40 | port: 8443 41 | -------------------------------------------------------------------------------- /docs/Devtools.md: -------------------------------------------------------------------------------- 1 | # Devtools 2 | Forward and proxy a WebKit debug-socket from an android device to your browser 3 | 4 | ## How it works 5 | 6 | ### Server 7 | 1. Find devtools sockets: `adb shell 'grep -a devtools_remote /proc/net/unix'` 8 | 2. For each socket request `/json` and `/json/version` 9 | 3. Replace websocket address in response with our hostname 10 | 4. Combine all data and send to a client 11 | 12 | ### Client 13 | Though each debuggable page explicitly specifies `devtoolsFrontendUrl` it is 14 | possible that provided version of devtools frontend will not work in your 15 | browser. To ensure that you will be able to debug webpage/webview, client 16 | creates three links: 17 | - `inspect` - this is a link provided by a remote browser in the answer for 18 | `/json` request (only WebSocket address is changed). When this link points to 19 | a local version of devtools (bundled with debuggable browser) you will not able 20 | to open it, because only WebSocket forwarding is implemented at the moment. 21 | - `bundled` - link to a version of devtools bundled with your (chromium based) 22 | browser without specifying revision or version of the remote target. You will 23 | get same link in the `chrome://inspect` page of Chromium browser. 24 | e.g. `devtools://devtools/bundled/inspector.html?ws=` 25 | - `remote` - link to a bundled devtools but with specified revision and version 26 | of remote target. This link is visible only when original link in 27 | `devtoolsFrontendUrl` contains revision. You will get same link in the 28 | `chrome://inspect` page of Chrome browser. 29 | e.g. `devtools://devtools/remote/serve_rev/@/inspector.html?remoteVersion=&remoteFrontend=true&ws=` 30 | 31 | **You can't open two last links with click or `open link in new tab`.** 32 | 33 | You must copy link and open it manually. This is browser restriction. 34 | -------------------------------------------------------------------------------- /docs/debug.md: -------------------------------------------------------------------------------- 1 | ### Client 2 | 3 | 1. Build dev version (will include source maps): 4 | > npm run dist:dev 5 | 6 | 2. Run from `dist` directory: 7 | > npm run start 8 | 9 | 3. Use the browser's built-in developer tools or your favorite IDE. 10 | 11 | ### Node.js server 12 | 13 | 1. `npm run dist:dev` 14 | 2. `cd dist` 15 | 3. `node --inspect-brk ./index.js` 16 | 17 | __HINT__: you might want to set `DEBUG` environment variable (see [debug](https://github.com/visionmedia/debug)): 18 | > DEBUG=* node --inspect-brk ./index.js 19 | 20 | ### Android server (`scrcpy-server.jar`) 21 | 22 | Source code is available [here](https://github.com/NetrisTV/scrcpy/tree/feature/websocket-server) 23 | __HINT__: you might want to build a dev version. 24 | 25 | To debug the server: 26 | 1. start node server 27 | 2. kill server from UI (click button with cross and PID number). 28 | 3. upload server package to a device: 29 | > adb push server/build/outputs/apk/debug/server-debug.apk /data/local/tmp/scrcpy-server.jar 30 | 31 | 4. setup port forwarding: 32 | > adb forward tcp:5005 tcp:5005 33 | 34 | 5. connect to device with adb shell: 35 | > adb shell 36 | 37 | 6.1. for Android 8 and below run this in adb shell (single line): 38 | > 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 39 | 40 | 6.2. for Android 9 and above: 41 | > 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 42 | 43 | 7. Open project (scrcpy, not ws-scrcpy) in Android Studio, create `Remote` Debug configuration with: 44 | > Host: localhost, Port: 5005 45 | 46 | Connect the debugger to the remote server on the device. 47 | -------------------------------------------------------------------------------- /docs/scheme.md: -------------------------------------------------------------------------------- 1 | ``` 2 | +--------------------------+ +------------------------------+ 3 | | Android device | | Server | 4 | | | | | 5 | | +----------------------+ | | +--------------------------+ | 6 | | | adb | | Run scrcpy | | adb (client) | | 7 | | | |<---------------| | | 8 | | | (usb/tcp) | | | | | | 9 | | +----------------------+ | | +--------------------------+ | 10 | | | | | 11 | | +----------------------+ | | +--------------------------+ | 12 | | | scrcpy | | | | nodejs | | 13 | | | | | | | | | 14 | ----| (ws://0.0.0.0:8886/) | | | | (http://0.0.0.0:8000/) |---- 15 | | | +----------------------+ | | +--------------------------+ | | 16 | | +--------------------------+ +------------------------------+ | 17 | | | 18 | | | 19 | | | 20 | | | 21 | | | 22 | | HTTP: | 23 | | +------------------------------+ < static (html, js...)| 24 | |Web-socket: | Client | | 25 | |< Input events | | Web-socket: | 26 | |> Video stream | +--------------------------+ | < Device list | 27 | -------------------| Web-browser |----------------------------- 28 | | +--------------------------+ | 29 | +------------------------------+ 30 | ``` 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ws-scrcpy", 3 | "version": "0.9.0-dev", 4 | "description": "Web client for scrcpy and more", 5 | "scripts": { 6 | "clean": "npx rimraf dist", 7 | "dist:dev": "webpack --config webpack/ws-scrcpy.dev.ts --stats-error-details", 8 | "dist:prod": "webpack --config webpack/ws-scrcpy.prod.ts --stats-error-details", 9 | "dist": "npm run dist:prod", 10 | "start": "npm run dist && cd dist && npm start", 11 | "script:dist:start": "node ./index.js", 12 | "lint": "eslint src/ --ext .ts", 13 | "format": "eslint src/ --fix --ext .ts", 14 | "test": "echo \"Error: no test specified\" && exit 1" 15 | }, 16 | "author": "Sergey Volkov ", 17 | "license": "MIT", 18 | "dependencies": { 19 | "@dead50f7/adbkit": "^2.11.4", 20 | "express": "^4.21.2", 21 | "ios-device-lib": "^0.9.2", 22 | "node-mjpeg-proxy": "^0.3.2", 23 | "node-pty": "^0.10.1", 24 | "portfinder": "^1.0.28", 25 | "tslib": "^2.3.1", 26 | "ws": "^8.18.0", 27 | "yaml": "^2.2.2" 28 | }, 29 | "devDependencies": { 30 | "@dead50f7/generate-package-json-webpack-plugin": "^2.6.1", 31 | "@types/bluebird": "^3.5.36", 32 | "@types/dom-webcodecs": "^0.1.3", 33 | "@types/express": "^4.17.13", 34 | "@types/node": "^12.20.47", 35 | "@types/node-forge": "^0.10.0", 36 | "@types/npmlog": "^4.1.4", 37 | "@types/webpack-node-externals": "^2.5.3", 38 | "@types/ws": "^7.4.7", 39 | "@typescript-eslint/eslint-plugin": "^5.18.0", 40 | "@typescript-eslint/parser": "^5.18.0", 41 | "buffer": "^6.0.3", 42 | "cross-env": "^7.0.3", 43 | "css-loader": "^6.8.1", 44 | "eslint": "^8.12.0", 45 | "eslint-config-prettier": "^8.5.0", 46 | "eslint-plugin-prettier": "^4.0.0", 47 | "eslint-plugin-progress": "0.0.1", 48 | "file-loader": "^6.2.0", 49 | "h264-converter": "^0.1.4", 50 | "html-webpack-plugin": "^5.5.0", 51 | "ifdef-loader": "^2.3.2", 52 | "mini-css-extract-plugin": "^2.6.1", 53 | "mkdirp": "^1.0.4", 54 | "path-browserify": "^1.0.1", 55 | "prettier": "^2.6.2", 56 | "recursive-copy": "^2.0.14", 57 | "rimraf": "^3.0.0", 58 | "svg-inline-loader": "^0.8.2", 59 | "sylvester.js": "^0.1.1", 60 | "tinyh264": "^0.0.7", 61 | "ts-loader": "^9.3.1", 62 | "ts-node": "^10.9.1", 63 | "typescript": "^4.7.4", 64 | "webpack": "^5.94.0", 65 | "webpack-cli": "^4.10.0", 66 | "webpack-node-externals": "^2.5.2", 67 | "worker-loader": "^3.0.8", 68 | "xterm": "^4.5.0", 69 | "xterm-addon-attach": "^0.6.0", 70 | "xterm-addon-fit": "^0.5.0" 71 | }, 72 | "optionalDependencies": { 73 | "appium-xcuitest-driver": "^3.62.0" 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/app/Attribute.ts: -------------------------------------------------------------------------------- 1 | export const Attribute = { 2 | COMMAND: 'data-command', 3 | FULL_NAME: 'data-full-name', 4 | NAME: 'data-name', 5 | PID: 'data-pid', 6 | UDID: 'data-udid', 7 | URL: 'data-url', 8 | USE_PROXY: 'data-use-proxy', 9 | SECURE: 'data-secure', 10 | HOSTNAME: 'data-hostname', 11 | PORT: 'data-port', 12 | PATHNAME: 'data-pathname', 13 | VALUE: 'data-value', 14 | }; 15 | -------------------------------------------------------------------------------- /src/app/DisplayInfo.ts: -------------------------------------------------------------------------------- 1 | import Size from './Size'; 2 | 3 | export class DisplayInfo { 4 | public static readonly DEFAULT_DISPLAY = 0x00000000; 5 | public static readonly FLAG_ROUND = 0b10000; 6 | public static readonly FLAG_PRESENTATION = 0b1000; 7 | public static readonly FLAG_PRIVATE = 0b100; 8 | public static readonly FLAG_SECURE = 0b10; 9 | public static readonly FLAG_SUPPORTS_PROTECTED_BUFFERS = 0b1; 10 | public static readonly INVALID_DISPLAY = -1; 11 | public static readonly BUFFER_LENGTH = 24; 12 | 13 | constructor( 14 | public readonly displayId: number, 15 | public readonly size: Size, 16 | public readonly rotation: number, 17 | public readonly layerStack: number, 18 | public readonly flags: number, 19 | ) {} 20 | 21 | public toBuffer(): Buffer { 22 | const temp = Buffer.alloc(DisplayInfo.BUFFER_LENGTH); 23 | let offset = 0; 24 | offset = temp.writeInt32BE(this.displayId, offset); 25 | offset = temp.writeInt32BE(this.size.width, offset); 26 | offset = temp.writeInt32BE(this.size.height, offset); 27 | offset = temp.writeInt32BE(this.rotation, offset); 28 | offset = temp.writeInt32BE(this.layerStack, offset); 29 | temp.writeInt32BE(this.flags, offset); 30 | return temp; 31 | } 32 | 33 | public toString(): string { 34 | // prettier-ignore 35 | return `DisplayInfo{displayId=${ 36 | this.displayId}, size=${ 37 | this.size}, rotation=${ 38 | this.rotation}, layerStack=${ 39 | this.layerStack}, flags=${ 40 | this.flags}}`; 41 | } 42 | 43 | public static fromBuffer(buffer: Buffer): DisplayInfo { 44 | if (buffer.length !== DisplayInfo.BUFFER_LENGTH) { 45 | throw Error(`Incorrect buffer length. Expected: ${DisplayInfo.BUFFER_LENGTH}, received: ${buffer.length}`); 46 | } 47 | let offset = 0; 48 | const displayId = buffer.readInt32BE(offset); 49 | offset += 4; 50 | const width = buffer.readInt32BE(offset); 51 | offset += 4; 52 | const height = buffer.readInt32BE(offset); 53 | offset += 4; 54 | const rotation = buffer.readInt32BE(offset); 55 | offset += 4; 56 | const layerStack = buffer.readInt32BE(offset); 57 | offset += 4; 58 | const flags = buffer.readInt32BE(offset); 59 | return new DisplayInfo(displayId, new Size(width, height), rotation, layerStack, flags); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/app/ErrorHandler.ts: -------------------------------------------------------------------------------- 1 | export default class ErrorHandler { 2 | constructor(readonly OnError: (ev: string | Event) => void) {} 3 | } 4 | -------------------------------------------------------------------------------- /src/app/MotionEvent.ts: -------------------------------------------------------------------------------- 1 | export default class MotionEvent { 2 | public static ACTION_DOWN = 0; 3 | public static ACTION_UP = 1; 4 | public static ACTION_MOVE = 2; 5 | /** 6 | * Button constant: Primary button (left mouse button). 7 | */ 8 | public static BUTTON_PRIMARY: number = 1 << 0; 9 | 10 | /** 11 | * Button constant: Secondary button (right mouse button). 12 | */ 13 | public static BUTTON_SECONDARY: number = 1 << 1; 14 | 15 | /** 16 | * Button constant: Tertiary button (middle mouse button). 17 | */ 18 | public static BUTTON_TERTIARY: number = 1 << 2; 19 | } 20 | -------------------------------------------------------------------------------- /src/app/Point.ts: -------------------------------------------------------------------------------- 1 | export interface PointInterface { 2 | x: number; 3 | y: number; 4 | } 5 | 6 | export default class Point { 7 | readonly x: number; 8 | readonly y: number; 9 | constructor(x: number, y: number) { 10 | this.x = Math.round(x); 11 | this.y = Math.round(y); 12 | } 13 | 14 | public equals(o: Point): boolean { 15 | if (this === o) { 16 | return true; 17 | } 18 | if (o === null) { 19 | return false; 20 | } 21 | return this.x === o.x && this.y === o.y; 22 | } 23 | 24 | public distance(to: Point): number { 25 | const x = this.x - to.x; 26 | const y = this.y - to.y; 27 | return Math.sqrt(x * x + y * y); 28 | } 29 | 30 | public toString(): string { 31 | return `Point{x=${this.x}, y=${this.y}}`; 32 | } 33 | 34 | public toJSON(): PointInterface { 35 | return { 36 | x: this.x, 37 | y: this.y, 38 | }; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/app/Position.ts: -------------------------------------------------------------------------------- 1 | import Point, { PointInterface } from './Point'; 2 | import Size, { SizeInterface } from './Size'; 3 | 4 | export interface PositionInterface { 5 | point: PointInterface; 6 | screenSize: SizeInterface; 7 | } 8 | 9 | export default class Position { 10 | public constructor(readonly point: Point, readonly screenSize: Size) {} 11 | 12 | public equals(o: Position): boolean { 13 | if (this === o) { 14 | return true; 15 | } 16 | if (o === null) { 17 | return false; 18 | } 19 | 20 | return this.point.equals(o.point) && this.screenSize.equals(o.screenSize); 21 | } 22 | 23 | public rotate(rotation: number): Position { 24 | switch (rotation) { 25 | case 1: 26 | return new Position( 27 | new Point(this.screenSize.height - this.point.y, this.point.x), 28 | this.screenSize.rotate(), 29 | ); 30 | case 2: 31 | return new Position( 32 | new Point(this.screenSize.width - this.point.x, this.screenSize.height - this.point.y), 33 | this.screenSize, 34 | ); 35 | case 3: 36 | return new Position( 37 | new Point(this.point.y, this.screenSize.width - this.point.x), 38 | this.screenSize.rotate(), 39 | ); 40 | default: 41 | return this; 42 | } 43 | } 44 | 45 | public toString(): string { 46 | return `Position{point=${this.point}, screenSize=${this.screenSize}}`; 47 | } 48 | 49 | public toJSON(): PositionInterface { 50 | return { 51 | point: this.point.toJSON(), 52 | screenSize: this.screenSize.toJSON(), 53 | }; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/app/Rect.ts: -------------------------------------------------------------------------------- 1 | interface RectInterface { 2 | left: number; 3 | top: number; 4 | right: number; 5 | bottom: number; 6 | } 7 | 8 | export default class Rect { 9 | constructor(readonly left: number, readonly top: number, readonly right: number, readonly bottom: number) { 10 | this.left = left; 11 | this.top = top; 12 | this.right = right; 13 | this.bottom = bottom; 14 | } 15 | public static equals(a?: Rect | null, b?: Rect | null): boolean { 16 | if (!a && !b) { 17 | return true; 18 | } 19 | return !!a && !!b && a.equals(b); 20 | } 21 | public static copy(a?: Rect | null): Rect | null { 22 | if (!a) { 23 | return null; 24 | } 25 | return new Rect(a.left, a.top, a.right, a.bottom); 26 | } 27 | public equals(o: Rect | null): boolean { 28 | if (this === o) { 29 | return true; 30 | } 31 | if (!o) { 32 | return false; 33 | } 34 | return this.left === o.left && this.top === o.top && this.right === o.right && this.bottom === o.bottom; 35 | } 36 | 37 | public getWidth(): number { 38 | return this.right - this.left; 39 | } 40 | 41 | public getHeight(): number { 42 | return this.bottom - this.top; 43 | } 44 | 45 | public toString(): string { 46 | // prettier-ignore 47 | return `Rect{left=${ 48 | this.left}, top=${ 49 | this.top}, right=${ 50 | this.right}, bottom=${ 51 | this.bottom}}`; 52 | } 53 | 54 | public toJSON(): RectInterface { 55 | return { 56 | left: this.left, 57 | right: this.right, 58 | top: this.top, 59 | bottom: this.bottom, 60 | }; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/app/ScreenInfo.ts: -------------------------------------------------------------------------------- 1 | import Rect from './Rect'; 2 | import Size from './Size'; 3 | 4 | export default class ScreenInfo { 5 | public static readonly BUFFER_LENGTH: number = 25; 6 | constructor(readonly contentRect: Rect, readonly videoSize: Size, readonly deviceRotation: number) {} 7 | 8 | public static fromBuffer(buffer: Buffer): ScreenInfo { 9 | const left = buffer.readInt32BE(0); 10 | const top = buffer.readInt32BE(4); 11 | const right = buffer.readInt32BE(8); 12 | const bottom = buffer.readInt32BE(12); 13 | const width = buffer.readInt32BE(16); 14 | const height = buffer.readInt32BE(20); 15 | const deviceRotation = buffer.readUInt8(24); 16 | return new ScreenInfo(new Rect(left, top, right, bottom), new Size(width, height), deviceRotation); 17 | } 18 | 19 | public equals(o?: ScreenInfo | null): boolean { 20 | if (!o) { 21 | return false; 22 | } 23 | return ( 24 | this.contentRect.equals(o.contentRect) && 25 | this.videoSize.equals(o.videoSize) && 26 | this.deviceRotation === o.deviceRotation 27 | ); 28 | } 29 | 30 | public toString(): string { 31 | return `ScreenInfo{contentRect=${this.contentRect}, videoSize=${this.videoSize}, deviceRotation=${this.deviceRotation}}`; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/app/Size.ts: -------------------------------------------------------------------------------- 1 | export interface SizeInterface { 2 | width: number; 3 | height: number; 4 | } 5 | 6 | export default class Size { 7 | public readonly w: number; 8 | public readonly h: number; 9 | 10 | constructor(readonly width: number, readonly height: number) { 11 | this.w = width; 12 | this.h = height; 13 | } 14 | 15 | public static equals(a?: Size | null, b?: Size | null): boolean { 16 | if (!a && !b) { 17 | return true; 18 | } 19 | return !!a && !!b && a.equals(b); 20 | } 21 | 22 | public static copy(a?: Size | null): Size | null { 23 | if (!a) { 24 | return null; 25 | } 26 | return new Size(a.width, a.height); 27 | } 28 | 29 | length(): number { 30 | return this.w * this.h; 31 | } 32 | 33 | public rotate(): Size { 34 | return new Size(this.height, this.width); 35 | } 36 | 37 | public equals(o: Size | null | undefined): boolean { 38 | if (this === o) { 39 | return true; 40 | } 41 | if (!o) { 42 | return false; 43 | } 44 | return this.width === o.width && this.height === o.height; 45 | } 46 | 47 | public intersect(o: Size | undefined | null): Size { 48 | if (!o) { 49 | return this; 50 | } 51 | const minH = Math.min(this.height, o.height); 52 | const minW = Math.min(this.width, o.width); 53 | return new Size(minW, minH); 54 | } 55 | 56 | public getHalfSize(): Size { 57 | return new Size(this.width >>> 1, this.height >>> 1); 58 | } 59 | 60 | public toString(): string { 61 | return `Size{width=${this.width}, height=${this.height}}`; 62 | } 63 | 64 | public toJSON(): SizeInterface { 65 | return { 66 | width: this.width, 67 | height: this.height, 68 | }; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/app/applDevice/client/DeviceTracker.ts: -------------------------------------------------------------------------------- 1 | import { BaseDeviceTracker } from '../../client/BaseDeviceTracker'; 2 | import { ACTION } from '../../../common/Action'; 3 | import ApplDeviceDescriptor from '../../../types/ApplDeviceDescriptor'; 4 | import Util from '../../Util'; 5 | import { html } from '../../ui/HtmlTag'; 6 | import { DeviceState } from '../../../common/DeviceState'; 7 | import { HostItem } from '../../../types/Configuration'; 8 | import { ChannelCode } from '../../../common/ChannelCode'; 9 | import { Tool } from '../../client/Tool'; 10 | 11 | export class DeviceTracker extends BaseDeviceTracker { 12 | public static ACTION = ACTION.APPL_DEVICE_LIST; 13 | protected static tools: Set = new Set(); 14 | private static instancesByUrl: Map = new Map(); 15 | 16 | public static start(hostItem: HostItem): DeviceTracker { 17 | const url = this.buildUrlForTracker(hostItem).toString(); 18 | let instance = this.instancesByUrl.get(url); 19 | if (!instance) { 20 | instance = new DeviceTracker(hostItem, url); 21 | } 22 | return instance; 23 | } 24 | 25 | public static getInstance(hostItem: HostItem): DeviceTracker { 26 | return this.start(hostItem); 27 | } 28 | protected tableId = 'appl_devices_list'; 29 | constructor(params: HostItem, directUrl: string) { 30 | super({ ...params, action: DeviceTracker.ACTION }, directUrl); 31 | DeviceTracker.instancesByUrl.set(directUrl, this); 32 | this.buildDeviceTable(); 33 | this.openNewConnection(); 34 | } 35 | 36 | protected onSocketOpen(): void { 37 | // do nothing; 38 | } 39 | 40 | protected buildDeviceRow(tbody: Element, device: ApplDeviceDescriptor): void { 41 | const blockClass = 'desc-block'; 42 | const fullName = `${this.id}_${Util.escapeUdid(device.udid)}`; 43 | const isActive = device.state === DeviceState.CONNECTED; 44 | const servicesId = `device_services_${fullName}`; 45 | const row = html`
46 |
47 |
"${device.name}"
48 |
${device.model}
49 |
${device.udid}
50 |
51 |
${device.version}
52 |
53 |
54 |
55 |
56 |
`.content; 57 | const services = row.getElementById(servicesId); 58 | if (!services) { 59 | return; 60 | } 61 | 62 | DeviceTracker.tools.forEach((tool) => { 63 | const entry = tool.createEntryForDeviceList(device, blockClass, this.params); 64 | if (entry) { 65 | if (Array.isArray(entry)) { 66 | entry.forEach((item) => { 67 | item && services.appendChild(item); 68 | }); 69 | } else { 70 | services.appendChild(entry); 71 | } 72 | } 73 | }); 74 | tbody.appendChild(row); 75 | } 76 | 77 | protected getChannelCode(): string { 78 | return ChannelCode.ATRC; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/app/applDevice/client/StreamClientMJPEG.ts: -------------------------------------------------------------------------------- 1 | import { ParamsStream } from '../../../types/ParamsStream'; 2 | import { ACTION } from '../../../common/Action'; 3 | import { StreamClient } from './StreamClient'; 4 | import { BasePlayer, PlayerClass } from '../../player/BasePlayer'; 5 | import { WdaStatus } from '../../../common/WdaStatus'; 6 | import { ApplMjpegMoreBox } from '../toolbox/ApplMjpegMoreBox'; 7 | 8 | const TAG = '[StreamClientMJPEG]'; 9 | 10 | export class StreamClientMJPEG extends StreamClient { 11 | public static ACTION = ACTION.STREAM_MJPEG; 12 | protected static players: Map = new Map(); 13 | 14 | public static start(params: ParamsStream): StreamClientMJPEG { 15 | return new StreamClientMJPEG(params); 16 | } 17 | 18 | constructor(params: ParamsStream) { 19 | super(params); 20 | this.name = `[${TAG}:${this.udid}]`; 21 | this.udid = this.params.udid; 22 | this.runWebDriverAgent().then(() => { 23 | this.startStream(); 24 | this.player?.play(); 25 | }); 26 | this.on('wda:status', (status) => { 27 | if (status === WdaStatus.STOPPED) { 28 | this.player?.stop(); 29 | } else if (status === WdaStatus.STARTED) { 30 | this.player?.play(); 31 | } 32 | }); 33 | } 34 | 35 | public static get action(): string { 36 | return StreamClientMJPEG.ACTION; 37 | } 38 | 39 | public createPlayer(udid: string, playerName?: string): BasePlayer { 40 | return StreamClientMJPEG.createPlayer(udid, playerName); 41 | } 42 | 43 | public getDeviceName(): string { 44 | return this.deviceName; 45 | } 46 | 47 | protected createMoreBox(udid: string, player: BasePlayer): ApplMjpegMoreBox { 48 | return new ApplMjpegMoreBox(udid, player, this.wdaProxy); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/app/applDevice/client/StreamClientQVHack.ts: -------------------------------------------------------------------------------- 1 | import { StreamReceiver } from '../../client/StreamReceiver'; 2 | import { BasePlayer, PlayerClass } from '../../player/BasePlayer'; 3 | import { ACTION } from '../../../common/Action'; 4 | import { StreamReceiverQVHack } from './StreamReceiverQVHack'; 5 | import { StreamClient } from './StreamClient'; 6 | import { ParamsStream } from '../../../types/ParamsStream'; 7 | 8 | const TAG = '[StreamClientQVHack]'; 9 | 10 | export class StreamClientQVHack extends StreamClient { 11 | public static ACTION = ACTION.STREAM_WS_QVH; 12 | protected static players: Map = new Map(); 13 | 14 | public static start(params: ParamsStream): StreamClientQVHack { 15 | return new StreamClientQVHack(params); 16 | } 17 | 18 | private readonly streamReceiver: StreamReceiver; 19 | 20 | constructor(params: ParamsStream) { 21 | super(params); 22 | 23 | this.name = `[${TAG}:${this.udid}]`; 24 | this.udid = this.params.udid; 25 | let udid = this.udid; 26 | // Workaround for qvh v0.5-beta 27 | if (udid.indexOf('-') !== -1) { 28 | udid = udid.replace('-', ''); 29 | udid = udid + '\0'.repeat(16); 30 | } 31 | this.streamReceiver = new StreamReceiverQVHack({ ...this.params, udid }); 32 | this.startStream(); 33 | this.setTitle(`${this.udid} stream`); 34 | this.setBodyClass('stream'); 35 | } 36 | 37 | public static get action(): string { 38 | return StreamClientQVHack.ACTION; 39 | } 40 | 41 | public createPlayer(udid: string, playerName?: string): BasePlayer { 42 | return StreamClientQVHack.createPlayer(udid, playerName); 43 | } 44 | 45 | protected onViewVideoResize = (): void => { 46 | this.runWebDriverAgent(); 47 | }; 48 | 49 | public onStop(ev?: string | Event): void { 50 | super.onStop(ev); 51 | this.streamReceiver.stop(); 52 | } 53 | 54 | protected startStream(inputPlayer?: BasePlayer): void { 55 | super.startStream(inputPlayer); 56 | const player = this.player; 57 | if (player) { 58 | player.on('video-view-resize', this.onViewVideoResize); 59 | this.streamReceiver.on('video', (data) => { 60 | const STATE = BasePlayer.STATE; 61 | if (player.getState() === STATE.PAUSED) { 62 | player.play(); 63 | } 64 | if (player.getState() === STATE.PLAYING) { 65 | player.pushFrame(new Uint8Array(data)); 66 | } 67 | }); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/app/applDevice/client/StreamReceiverQVHack.ts: -------------------------------------------------------------------------------- 1 | import { StreamReceiver } from '../../client/StreamReceiver'; 2 | import { ACTION } from '../../../common/Action'; 3 | import Util from '../../Util'; 4 | import { ParamsStream } from '../../../types/ParamsStream'; 5 | import { ChannelCode } from '../../../common/ChannelCode'; 6 | 7 | export class StreamReceiverQVHack extends StreamReceiver { 8 | public static parseParameters(params: URLSearchParams): ParamsStream { 9 | const typedParams = super.parseParameters(params); 10 | const { action } = typedParams; 11 | if (action !== ACTION.STREAM_WS_QVH) { 12 | throw Error('Incorrect action'); 13 | } 14 | return { 15 | ...typedParams, 16 | action, 17 | player: Util.parseString(params, 'player', true), 18 | udid: Util.parseString(params, 'udid', true), 19 | }; 20 | } 21 | 22 | protected supportMultiplexing(): boolean { 23 | return true; 24 | } 25 | 26 | protected getChannelInitData(): Buffer { 27 | const udid = Util.stringToUtf8ByteArray(this.params.udid); 28 | const buffer = Buffer.alloc(4 + 4 + udid.byteLength); 29 | buffer.write(ChannelCode.QVHS, 'ascii'); 30 | buffer.writeUInt32LE(udid.length, 4); 31 | buffer.set(udid, 8); 32 | return buffer; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/app/applDevice/toolbox/ApplToolBox.ts: -------------------------------------------------------------------------------- 1 | import { ToolBox } from '../../toolbox/ToolBox'; 2 | import SvgImage from '../../ui/SvgImage'; 3 | import { BasePlayer } from '../../player/BasePlayer'; 4 | import { ToolBoxButton } from '../../toolbox/ToolBoxButton'; 5 | import { ToolBoxElement } from '../../toolbox/ToolBoxElement'; 6 | import { ToolBoxCheckbox } from '../../toolbox/ToolBoxCheckbox'; 7 | import { WdaProxyClient } from '../client/WdaProxyClient'; 8 | 9 | const BUTTONS = [ 10 | { 11 | title: 'Home', 12 | name: 'home', 13 | icon: SvgImage.Icon.HOME, 14 | }, 15 | ]; 16 | 17 | export interface StreamClient { 18 | getDeviceName(): string; 19 | } 20 | 21 | export class ApplToolBox extends ToolBox { 22 | protected constructor(list: ToolBoxElement[]) { 23 | super(list); 24 | } 25 | 26 | public static createToolBox( 27 | udid: string, 28 | player: BasePlayer, 29 | client: StreamClient, 30 | wdaConnection: WdaProxyClient, 31 | moreBox?: HTMLElement, 32 | ): ApplToolBox { 33 | const playerName = player.getName(); 34 | const list = BUTTONS.slice(); 35 | const handler = ( 36 | _: K, 37 | element: ToolBoxElement, 38 | ) => { 39 | if (!element.optional?.name) { 40 | return; 41 | } 42 | const { name } = element.optional; 43 | wdaConnection.pressButton(name); 44 | }; 45 | const elements: ToolBoxElement[] = list.map((item) => { 46 | const button = new ToolBoxButton(item.title, item.icon, { 47 | name: item.name, 48 | }); 49 | button.addEventListener('click', handler); 50 | return button; 51 | }); 52 | if (player.supportsScreenshot) { 53 | const screenshot = new ToolBoxButton('Take screenshot', SvgImage.Icon.CAMERA); 54 | screenshot.addEventListener('click', () => { 55 | player.createScreenshot(client.getDeviceName()); 56 | }); 57 | elements.push(screenshot); 58 | } 59 | 60 | if (moreBox) { 61 | const more = new ToolBoxCheckbox('More', SvgImage.Icon.MORE, `show_more_${udid}_${playerName}`); 62 | more.addEventListener('click', (_, el) => { 63 | const element = el.getElement(); 64 | moreBox.style.display = element.checked ? 'block' : 'none'; 65 | }); 66 | elements.unshift(more); 67 | } 68 | return new ApplToolBox(elements); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/app/client/BaseClient.ts: -------------------------------------------------------------------------------- 1 | import { EventMap, TypedEmitter } from '../../common/TypedEmitter'; 2 | import { ParamsBase } from '../../types/ParamsBase'; 3 | import Util from '../Util'; 4 | 5 | export class BaseClient

extends TypedEmitter { 6 | protected title = 'BaseClient'; 7 | protected params: P; 8 | 9 | protected constructor(params: P) { 10 | super(); 11 | this.params = params; 12 | } 13 | 14 | public static parseParameters(query: URLSearchParams): ParamsBase { 15 | const action = Util.parseStringEnv(query.get('action')); 16 | if (!action) { 17 | throw TypeError('Invalid action'); 18 | } 19 | return { 20 | action: action, 21 | useProxy: Util.parseBooleanEnv(query.get('useProxy')), 22 | secure: Util.parseBooleanEnv(query.get('secure')), 23 | hostname: Util.parseStringEnv(query.get('hostname')), 24 | port: Util.parseIntEnv(query.get('port')), 25 | pathname: Util.parseStringEnv(query.get('pathname')), 26 | }; 27 | } 28 | 29 | public setTitle(text = this.title): void { 30 | let titleTag: HTMLTitleElement | null = document.querySelector('head > title'); 31 | if (!titleTag) { 32 | titleTag = document.createElement('title'); 33 | } 34 | titleTag.innerText = text; 35 | } 36 | 37 | public setBodyClass(text: string): void { 38 | document.body.className = text; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/app/client/Tool.d.ts: -------------------------------------------------------------------------------- 1 | import { ParamsDeviceTracker } from '../../types/ParamsDeviceTracker'; 2 | import { BaseDeviceDescriptor } from '../../types/BaseDeviceDescriptor'; 3 | 4 | type Entry = HTMLElement | DocumentFragment; 5 | 6 | export interface Tool { 7 | createEntryForDeviceList( 8 | descriptor: BaseDeviceDescriptor, 9 | blockClass: string, 10 | params: ParamsDeviceTracker, 11 | ): Array | Entry | undefined; 12 | } 13 | -------------------------------------------------------------------------------- /src/app/controlMessage/ControlMessage.ts: -------------------------------------------------------------------------------- 1 | export interface ControlMessageInterface { 2 | type: number; 3 | } 4 | 5 | export class ControlMessage { 6 | public static TYPE_KEYCODE = 0; 7 | public static TYPE_TEXT = 1; 8 | public static TYPE_TOUCH = 2; 9 | public static TYPE_SCROLL = 3; 10 | public static TYPE_BACK_OR_SCREEN_ON = 4; 11 | public static TYPE_EXPAND_NOTIFICATION_PANEL = 5; 12 | public static TYPE_EXPAND_SETTINGS_PANEL = 6; 13 | public static TYPE_COLLAPSE_PANELS = 7; 14 | public static TYPE_GET_CLIPBOARD = 8; 15 | public static TYPE_SET_CLIPBOARD = 9; 16 | public static TYPE_SET_SCREEN_POWER_MODE = 10; 17 | public static TYPE_ROTATE_DEVICE = 11; 18 | public static TYPE_CHANGE_STREAM_PARAMETERS = 101; 19 | public static TYPE_PUSH_FILE = 102; 20 | 21 | constructor(readonly type: number) {} 22 | 23 | public toBuffer(): Buffer { 24 | throw Error('Not implemented'); 25 | } 26 | 27 | public toString(): string { 28 | return 'ControlMessage'; 29 | } 30 | 31 | public toJSON(): ControlMessageInterface { 32 | return { 33 | type: this.type, 34 | }; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/app/controlMessage/KeyCodeControlMessage.ts: -------------------------------------------------------------------------------- 1 | import { Buffer } from 'buffer'; 2 | import { ControlMessage, ControlMessageInterface } from './ControlMessage'; 3 | 4 | export interface KeyCodeControlMessageInterface extends ControlMessageInterface { 5 | action: number; 6 | keycode: number; 7 | repeat: number; 8 | metaState: number; 9 | } 10 | 11 | export class KeyCodeControlMessage extends ControlMessage { 12 | public static PAYLOAD_LENGTH = 13; 13 | 14 | constructor( 15 | readonly action: number, 16 | readonly keycode: number, 17 | readonly repeat: number, 18 | readonly metaState: number, 19 | ) { 20 | super(ControlMessage.TYPE_KEYCODE); 21 | } 22 | 23 | /** 24 | * @override 25 | */ 26 | public toBuffer(): Buffer { 27 | const buffer = Buffer.alloc(KeyCodeControlMessage.PAYLOAD_LENGTH + 1); 28 | let offset = 0; 29 | offset = buffer.writeInt8(this.type, offset); 30 | offset = buffer.writeInt8(this.action, offset); 31 | offset = buffer.writeInt32BE(this.keycode, offset); 32 | offset = buffer.writeInt32BE(this.repeat, offset); 33 | buffer.writeInt32BE(this.metaState, offset); 34 | return buffer; 35 | } 36 | 37 | public toString(): string { 38 | return `KeyCodeControlMessage{action=${this.action}, keycode=${this.keycode}, metaState=${this.metaState}}`; 39 | } 40 | 41 | public toJSON(): KeyCodeControlMessageInterface { 42 | return { 43 | type: this.type, 44 | action: this.action, 45 | keycode: this.keycode, 46 | metaState: this.metaState, 47 | repeat: this.repeat, 48 | }; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/app/controlMessage/ScrollControlMessage.ts: -------------------------------------------------------------------------------- 1 | import { ControlMessage, ControlMessageInterface } from './ControlMessage'; 2 | import Position, { PositionInterface } from '../Position'; 3 | 4 | export interface ScrollControlMessageInterface extends ControlMessageInterface { 5 | position: PositionInterface; 6 | hScroll: number; 7 | vScroll: number; 8 | } 9 | 10 | export class ScrollControlMessage extends ControlMessage { 11 | public static PAYLOAD_LENGTH = 20; 12 | 13 | constructor(readonly position: Position, readonly hScroll: number, readonly vScroll: number) { 14 | super(ControlMessage.TYPE_SCROLL); 15 | } 16 | 17 | /** 18 | * @override 19 | */ 20 | public toBuffer(): Buffer { 21 | const buffer = Buffer.alloc(ScrollControlMessage.PAYLOAD_LENGTH + 1); 22 | let offset = 0; 23 | offset = buffer.writeUInt8(this.type, offset); 24 | offset = buffer.writeUInt32BE(this.position.point.x, offset); 25 | offset = buffer.writeUInt32BE(this.position.point.y, offset); 26 | offset = buffer.writeUInt16BE(this.position.screenSize.width, offset); 27 | offset = buffer.writeUInt16BE(this.position.screenSize.height, offset); 28 | offset = buffer.writeInt32BE(this.hScroll, offset); 29 | buffer.writeInt32BE(this.vScroll, offset); 30 | return buffer; 31 | } 32 | 33 | public toString(): string { 34 | return `ScrollControlMessage{hScroll=${this.hScroll}, vScroll=${this.vScroll}, position=${this.position}}`; 35 | } 36 | 37 | public toJSON(): ScrollControlMessageInterface { 38 | return { 39 | type: this.type, 40 | position: this.position.toJSON(), 41 | hScroll: this.hScroll, 42 | vScroll: this.vScroll, 43 | }; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/app/controlMessage/TextControlMessage.ts: -------------------------------------------------------------------------------- 1 | import { Buffer } from 'buffer'; 2 | import { ControlMessage, ControlMessageInterface } from './ControlMessage'; 3 | 4 | export interface TextControlMessageInterface extends ControlMessageInterface { 5 | text: string; 6 | } 7 | 8 | export class TextControlMessage extends ControlMessage { 9 | private static TEXT_SIZE_FIELD_LENGTH = 4; 10 | constructor(readonly text: string) { 11 | super(ControlMessage.TYPE_TEXT); 12 | } 13 | 14 | public getText(): string { 15 | return this.text; 16 | } 17 | 18 | /** 19 | * @override 20 | */ 21 | public toBuffer(): Buffer { 22 | const length = this.text.length; 23 | const buffer = Buffer.alloc(length + 1 + TextControlMessage.TEXT_SIZE_FIELD_LENGTH); 24 | let offset = 0; 25 | offset = buffer.writeUInt8(this.type, offset); 26 | offset = buffer.writeUInt32BE(length, offset); 27 | buffer.write(this.text, offset); 28 | return buffer; 29 | } 30 | 31 | public toString(): string { 32 | return `TextControlMessage{text=${this.text}}`; 33 | } 34 | 35 | public toJSON(): TextControlMessageInterface { 36 | return { 37 | type: this.type, 38 | text: this.text, 39 | }; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/app/controlMessage/TouchControlMessage.ts: -------------------------------------------------------------------------------- 1 | import { ControlMessage, ControlMessageInterface } from './ControlMessage'; 2 | import Position, { PositionInterface } from '../Position'; 3 | 4 | export interface TouchControlMessageInterface extends ControlMessageInterface { 5 | type: number; 6 | action: number; 7 | pointerId: number; 8 | position: PositionInterface; 9 | pressure: number; 10 | buttons: number; 11 | } 12 | 13 | export class TouchControlMessage extends ControlMessage { 14 | public static PAYLOAD_LENGTH = 28; 15 | /** 16 | * - For a touch screen or touch pad, reports the approximate pressure 17 | * applied to the surface by a finger or other tool. The value is 18 | * normalized to a range from 0 (no pressure at all) to 1 (normal pressure), 19 | * although values higher than 1 may be generated depending on the 20 | * calibration of the input device. 21 | * - For a trackball, the value is set to 1 if the trackball button is pressed 22 | * or 0 otherwise. 23 | * - For a mouse, the value is set to 1 if the primary mouse button is pressed 24 | * or 0 otherwise. 25 | * 26 | * - scrcpy server expects signed short (2 bytes) for a pressure value 27 | * - in browser TouchEvent has `force` property (values in 0..1 range), we 28 | * use it as "pressure" for scrcpy 29 | */ 30 | public static readonly MAX_PRESSURE_VALUE = 0xffff; 31 | 32 | constructor( 33 | readonly action: number, 34 | readonly pointerId: number, 35 | readonly position: Position, 36 | readonly pressure: number, 37 | readonly buttons: number, 38 | ) { 39 | super(ControlMessage.TYPE_TOUCH); 40 | } 41 | 42 | /** 43 | * @override 44 | */ 45 | public toBuffer(): Buffer { 46 | const buffer: Buffer = Buffer.alloc(TouchControlMessage.PAYLOAD_LENGTH + 1); 47 | let offset = 0; 48 | offset = buffer.writeUInt8(this.type, offset); 49 | offset = buffer.writeUInt8(this.action, offset); 50 | offset = buffer.writeUInt32BE(0, offset); // pointerId is `long` (8 bytes) on java side 51 | offset = buffer.writeUInt32BE(this.pointerId, offset); 52 | offset = buffer.writeUInt32BE(this.position.point.x, offset); 53 | offset = buffer.writeUInt32BE(this.position.point.y, offset); 54 | offset = buffer.writeUInt16BE(this.position.screenSize.width, offset); 55 | offset = buffer.writeUInt16BE(this.position.screenSize.height, offset); 56 | offset = buffer.writeUInt16BE(this.pressure * TouchControlMessage.MAX_PRESSURE_VALUE, offset); 57 | buffer.writeUInt32BE(this.buttons, offset); 58 | return buffer; 59 | } 60 | 61 | public toString(): string { 62 | return `TouchControlMessage{action=${this.action}, pointerId=${this.pointerId}, position=${this.position}, pressure=${this.pressure}, buttons=${this.buttons}}`; 63 | } 64 | 65 | public toJSON(): TouchControlMessageInterface { 66 | return { 67 | type: this.type, 68 | action: this.action, 69 | pointerId: this.pointerId, 70 | position: this.position.toJSON(), 71 | pressure: this.pressure, 72 | buttons: this.buttons, 73 | }; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/app/googDevice/DeviceMessage.ts: -------------------------------------------------------------------------------- 1 | import Util from '../Util'; 2 | 3 | export default class DeviceMessage { 4 | public static TYPE_CLIPBOARD = 0; 5 | public static TYPE_PUSH_RESPONSE = 101; 6 | 7 | public static readonly MAGIC_BYTES_MESSAGE = Util.stringToUtf8ByteArray('scrcpy_message'); 8 | 9 | constructor(public readonly type: number, protected readonly buffer: Buffer) {} 10 | 11 | public static fromBuffer(data: ArrayBuffer): DeviceMessage { 12 | const magicSize = this.MAGIC_BYTES_MESSAGE.length; 13 | const buffer = Buffer.from(data, magicSize, data.byteLength - magicSize); 14 | const type = buffer.readUInt8(0); 15 | return new DeviceMessage(type, buffer); 16 | } 17 | 18 | public getText(): string { 19 | if (this.type !== DeviceMessage.TYPE_CLIPBOARD) { 20 | throw TypeError(`Wrong message type: ${this.type}`); 21 | } 22 | if (!this.buffer) { 23 | throw Error('Empty buffer'); 24 | } 25 | let offset = 1; 26 | const length = this.buffer.readInt32BE(offset); 27 | offset += 4; 28 | const textBytes = this.buffer.slice(offset, offset + length); 29 | return Util.utf8ByteArrayToString(textBytes); 30 | } 31 | 32 | public getPushStats(): { id: number; code: number } { 33 | if (this.type !== DeviceMessage.TYPE_PUSH_RESPONSE) { 34 | throw TypeError(`Wrong message type: ${this.type}`); 35 | } 36 | if (!this.buffer) { 37 | throw Error('Empty buffer'); 38 | } 39 | const id = this.buffer.readInt16BE(1); 40 | const code = this.buffer.readInt8(3); 41 | return { id, code }; 42 | } 43 | 44 | public toString(): string { 45 | let desc: string; 46 | if (this.type === DeviceMessage.TYPE_CLIPBOARD && this.buffer) { 47 | desc = `, text=[${this.getText()}]`; 48 | } else { 49 | desc = this.buffer ? `, buffer=[${this.buffer.join(',')}]` : ''; 50 | } 51 | return `DeviceMessage{type=${this.type}${desc}}`; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/app/googDevice/Entry.ts: -------------------------------------------------------------------------------- 1 | import { Stats } from './Stats'; 2 | 3 | export class Entry extends Stats { 4 | constructor(public name: string, mode: number, size: number, mtime: number) { 5 | super(mode, size, mtime); 6 | } 7 | 8 | public toString(): string { 9 | return this.name; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/app/googDevice/KeyInputHandler.ts: -------------------------------------------------------------------------------- 1 | import { KeyCodeControlMessage } from '../controlMessage/KeyCodeControlMessage'; 2 | import KeyEvent from './android/KeyEvent'; 3 | import { KeyToCodeMap } from './KeyToCodeMap'; 4 | 5 | export interface KeyEventListener { 6 | onKeyEvent: (event: KeyCodeControlMessage) => void; 7 | } 8 | 9 | export class KeyInputHandler { 10 | private static readonly repeatCounter: Map = new Map(); 11 | private static readonly listeners: Set = new Set(); 12 | private static handler = (event: Event): void => { 13 | const keyboardEvent = event as KeyboardEvent; 14 | const keyCode = KeyToCodeMap.get(keyboardEvent.code); 15 | if (!keyCode) { 16 | return; 17 | } 18 | let action: typeof KeyEvent.ACTION_DOWN | typeof KeyEvent.ACTION_DOWN; 19 | let repeatCount = 0; 20 | if (keyboardEvent.type === 'keydown') { 21 | action = KeyEvent.ACTION_DOWN; 22 | if (keyboardEvent.repeat) { 23 | let count = KeyInputHandler.repeatCounter.get(keyCode); 24 | if (typeof count !== 'number') { 25 | count = 1; 26 | } else { 27 | count++; 28 | } 29 | repeatCount = count; 30 | KeyInputHandler.repeatCounter.set(keyCode, count); 31 | } 32 | } else if (keyboardEvent.type === 'keyup') { 33 | action = KeyEvent.ACTION_UP; 34 | KeyInputHandler.repeatCounter.delete(keyCode); 35 | } else { 36 | return; 37 | } 38 | const metaState = 39 | (keyboardEvent.getModifierState('Alt') ? KeyEvent.META_ALT_ON : 0) | 40 | (keyboardEvent.getModifierState('Shift') ? KeyEvent.META_SHIFT_ON : 0) | 41 | (keyboardEvent.getModifierState('Control') ? KeyEvent.META_CTRL_ON : 0) | 42 | (keyboardEvent.getModifierState('Meta') ? KeyEvent.META_META_ON : 0) | 43 | (keyboardEvent.getModifierState('CapsLock') ? KeyEvent.META_CAPS_LOCK_ON : 0) | 44 | (keyboardEvent.getModifierState('ScrollLock') ? KeyEvent.META_SCROLL_LOCK_ON : 0) | 45 | (keyboardEvent.getModifierState('NumLock') ? KeyEvent.META_NUM_LOCK_ON : 0); 46 | 47 | const controlMessage: KeyCodeControlMessage = new KeyCodeControlMessage( 48 | action, 49 | keyCode, 50 | repeatCount, 51 | metaState, 52 | ); 53 | KeyInputHandler.listeners.forEach((listener) => { 54 | listener.onKeyEvent(controlMessage); 55 | }); 56 | event.preventDefault(); 57 | }; 58 | private static attachListeners(): void { 59 | document.body.addEventListener('keydown', this.handler); 60 | document.body.addEventListener('keyup', this.handler); 61 | } 62 | private static detachListeners(): void { 63 | document.body.removeEventListener('keydown', this.handler); 64 | document.body.removeEventListener('keyup', this.handler); 65 | } 66 | public static addEventListener(listener: KeyEventListener): void { 67 | if (!this.listeners.size) { 68 | this.attachListeners(); 69 | } 70 | this.listeners.add(listener); 71 | } 72 | public static removeEventListener(listener: KeyEventListener): void { 73 | this.listeners.delete(listener); 74 | if (!this.listeners.size) { 75 | this.detachListeners(); 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/app/googDevice/Stats.ts: -------------------------------------------------------------------------------- 1 | export class Stats { 2 | // The following constant were extracted from `man 2 stat` on Ubuntu 12.10. 3 | public static S_IFMT = 0o170000; // bit mask for the file type bit fields 4 | 5 | public static S_IFSOCK = 0o140000; // socket 6 | 7 | public static S_IFLNK = 0o120000; // symbolic link 8 | 9 | public static S_IFREG = 0o100000; // regular file 10 | 11 | public static S_IFBLK = 0o060000; // block device 12 | 13 | public static S_IFDIR = 0o040000; // directory 14 | 15 | public static S_IFCHR = 0o020000; // character device 16 | 17 | public static S_IFIFO = 0o010000; // FIFO 18 | 19 | public static S_ISUID = 0o004000; // set UID bit 20 | 21 | public static S_ISGID = 0o002000; // set-group-ID bit (see below) 22 | 23 | public static S_ISVTX = 0o001000; // sticky bit (see below) 24 | 25 | public static S_IRWXU = 0o0700; // mask for file owner permissions 26 | 27 | public static S_IRUSR = 0o0400; // owner has read permission 28 | 29 | public static S_IWUSR = 0o0200; // owner has write permission 30 | 31 | public static S_IXUSR = 0o0100; // owner has execute permission 32 | 33 | public static S_IRWXG = 0o0070; // mask for group permissions 34 | 35 | public static S_IRGRP = 0o0040; // group has read permission 36 | 37 | public readonly mtime: Date; 38 | 39 | constructor(public readonly mode: number, public readonly size: number, mtime: number) { 40 | this.mtime = new Date(mtime * 1000); 41 | } 42 | 43 | private checkModeProperty(property: number): boolean { 44 | return (this.mode & Stats.S_IFMT) === property; 45 | } 46 | public isBlockDevice(): boolean { 47 | return this.checkModeProperty(Stats.S_IFBLK); 48 | } 49 | public isCharacterDevice(): boolean { 50 | return this.checkModeProperty(Stats.S_IFCHR); 51 | } 52 | public isDirectory(): boolean { 53 | return this.checkModeProperty(Stats.S_IFDIR); 54 | } 55 | public isFIFO(): boolean { 56 | return this.checkModeProperty(Stats.S_IFIFO); 57 | } 58 | public isisSocket(): boolean { 59 | return this.checkModeProperty(Stats.S_IFSOCK); 60 | } 61 | public isSymbolicLink(): boolean { 62 | return this.checkModeProperty(Stats.S_IFLNK); 63 | } 64 | public isFile(): boolean { 65 | return this.checkModeProperty(Stats.S_IFREG); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/app/googDevice/client/StreamReceiverScrcpy.ts: -------------------------------------------------------------------------------- 1 | import { StreamReceiver } from '../../client/StreamReceiver'; 2 | import { ParamsStreamScrcpy } from '../../../types/ParamsStreamScrcpy'; 3 | import { ACTION } from '../../../common/Action'; 4 | import Util from '../../Util'; 5 | 6 | export class StreamReceiverScrcpy extends StreamReceiver { 7 | public static parseParameters(params: URLSearchParams): ParamsStreamScrcpy { 8 | const typedParams = super.parseParameters(params); 9 | const { action } = typedParams; 10 | if (action !== ACTION.STREAM_SCRCPY) { 11 | throw Error('Incorrect action'); 12 | } 13 | return { 14 | ...typedParams, 15 | action, 16 | udid: Util.parseString(params, 'udid', true), 17 | ws: Util.parseString(params, 'ws', true), 18 | player: Util.parseString(params, 'player', true), 19 | }; 20 | } 21 | protected buildDirectWebSocketUrl(): URL { 22 | return new URL((this.params as ParamsStreamScrcpy).ws); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/app/googDevice/filePush/FilePushResponseStatus.ts: -------------------------------------------------------------------------------- 1 | export enum FilePushResponseStatus { 2 | NEW_PUSH_ID = 1, 3 | NO_ERROR = 0, 4 | ERROR_INVALID_NAME = -1, 5 | ERROR_NO_SPACE = -2, 6 | ERROR_FAILED_TO_DELETE = -3, 7 | ERROR_FAILED_TO_CREATE = -4, 8 | ERROR_FILE_NOT_FOUND = -5, 9 | ERROR_FAILED_TO_WRITE = -6, 10 | ERROR_FILE_IS_BUSY = -7, 11 | ERROR_INVALID_STATE = -8, 12 | ERROR_UNKNOWN_ID = -9, 13 | ERROR_NO_FREE_ID = -10, 14 | ERROR_INCORRECT_SIZE = -11, 15 | ERROR_OTHER = -12, 16 | } 17 | -------------------------------------------------------------------------------- /src/app/googDevice/filePush/FilePushStream.ts: -------------------------------------------------------------------------------- 1 | import { TypedEmitter } from '../../../common/TypedEmitter'; 2 | 3 | export type PushResponse = { id: number; code: number }; 4 | 5 | interface FilePushStreamEvents { 6 | response: PushResponse; 7 | error: { id: number; error: Error }; 8 | } 9 | 10 | export abstract class FilePushStream extends TypedEmitter { 11 | public abstract hasConnection(): boolean; 12 | public abstract isAllowedFile(file: File): boolean; 13 | public abstract sendEventNew(params: { id: number }): void; 14 | public abstract sendEventStart(params: { id: number; fileName: string; fileSize: number }): void; 15 | public abstract sendEventFinish(params: { id: number }): void; 16 | public abstract sendEventAppend(params: { id: number; chunk: Uint8Array }): void; 17 | public abstract release(): void; 18 | } 19 | -------------------------------------------------------------------------------- /src/app/googDevice/filePush/ScrcpyFilePushStream.ts: -------------------------------------------------------------------------------- 1 | import { FilePushStream } from './FilePushStream'; 2 | import { StreamReceiverScrcpy } from '../client/StreamReceiverScrcpy'; 3 | import DeviceMessage from '../DeviceMessage'; 4 | import { CommandControlMessage, FilePushState } from '../../controlMessage/CommandControlMessage'; 5 | 6 | const ALLOWED_TYPES = ['application/vnd.android.package-archive']; 7 | const ALLOWED_NAME_RE = /\.apk$/i; 8 | 9 | export class ScrcpyFilePushStream extends FilePushStream { 10 | constructor(private readonly streamReceiver: StreamReceiverScrcpy) { 11 | super(); 12 | streamReceiver.on('deviceMessage', this.onDeviceMessage); 13 | } 14 | public hasConnection(): boolean { 15 | return this.streamReceiver.hasConnection(); 16 | } 17 | 18 | public isAllowedFile(file: File): boolean { 19 | const { type, name } = file; 20 | return (type && ALLOWED_TYPES.includes(type)) || (!type && ALLOWED_NAME_RE.test(name)); 21 | } 22 | 23 | public sendEventAppend({ id, chunk }: { id: number; chunk: Uint8Array }): void { 24 | const appendParams = { id, chunk, state: FilePushState.APPEND }; 25 | this.streamReceiver.sendEvent(CommandControlMessage.createPushFileCommand(appendParams)); 26 | } 27 | 28 | public sendEventFinish({ id }: { id: number }): void { 29 | const finishParams = { id, state: FilePushState.FINISH }; 30 | this.streamReceiver.sendEvent(CommandControlMessage.createPushFileCommand(finishParams)); 31 | } 32 | 33 | public sendEventNew({ id }: { id: number }): void { 34 | const newParams = { id, state: FilePushState.NEW }; 35 | this.streamReceiver.sendEvent(CommandControlMessage.createPushFileCommand(newParams)); 36 | } 37 | 38 | public sendEventStart({ id, fileName, fileSize }: { id: number; fileName: string; fileSize: number }): void { 39 | const startParams = { id, fileName, fileSize, state: FilePushState.START }; 40 | this.streamReceiver.sendEvent(CommandControlMessage.createPushFileCommand(startParams)); 41 | } 42 | 43 | public release(): void { 44 | this.streamReceiver.off('deviceMessage', this.onDeviceMessage); 45 | } 46 | 47 | onDeviceMessage = (ev: DeviceMessage): void => { 48 | if (ev.type !== DeviceMessage.TYPE_PUSH_RESPONSE) { 49 | return; 50 | } 51 | const stats = ev.getPushStats(); 52 | this.emit('response', stats); 53 | }; 54 | } 55 | -------------------------------------------------------------------------------- /src/app/interactionHandler/SimpleInteractionHandler.ts: -------------------------------------------------------------------------------- 1 | import { InteractionEvents, InteractionHandler } from './InteractionHandler'; 2 | import { BasePlayer } from '../player/BasePlayer'; 3 | import ScreenInfo from '../ScreenInfo'; 4 | import Position from '../Position'; 5 | 6 | export interface TouchHandlerListener { 7 | performClick: (position: Position) => void; 8 | performScroll: (from: Position, to: Position) => void; 9 | } 10 | 11 | const TAG = '[SimpleTouchHandler]'; 12 | 13 | export class SimpleInteractionHandler extends InteractionHandler { 14 | private startPosition?: Position; 15 | private endPosition?: Position; 16 | private static readonly touchEventsNames: InteractionEvents[] = ['mousedown', 'mouseup', 'mousemove']; 17 | private storage = new Map(); 18 | 19 | constructor(player: BasePlayer, private readonly listener: TouchHandlerListener) { 20 | super(player, SimpleInteractionHandler.touchEventsNames, []); 21 | } 22 | 23 | protected onInteraction(event: MouseEvent | TouchEvent): void { 24 | let handled = false; 25 | if (!(event instanceof MouseEvent)) { 26 | return; 27 | } 28 | if (event.target === this.tag) { 29 | const screenInfo: ScreenInfo = this.player.getScreenInfo() as ScreenInfo; 30 | if (!screenInfo) { 31 | return; 32 | } 33 | const events = this.buildTouchEvent(event, screenInfo, this.storage); 34 | if (events.length > 1) { 35 | console.warn(TAG, 'Too many events', events); 36 | return; 37 | } 38 | const downEventName = 'mousedown'; 39 | if (events.length === 1) { 40 | handled = true; 41 | if (event.type === downEventName) { 42 | this.startPosition = events[0].position; 43 | } else { 44 | if (this.startPosition) { 45 | this.endPosition = events[0].position; 46 | } else { 47 | console.warn(TAG, `Received "${event.type}" before "${downEventName}"`); 48 | } 49 | } 50 | if (this.startPosition) { 51 | this.drawPointer(this.startPosition.point); 52 | } 53 | if (this.endPosition) { 54 | this.drawPointer(this.endPosition.point); 55 | if (this.startPosition) { 56 | this.drawLine(this.startPosition.point, this.endPosition.point); 57 | } 58 | } 59 | if (event.type === 'mouseup') { 60 | if (this.startPosition && this.endPosition) { 61 | this.clearCanvas(); 62 | if (this.startPosition.point.distance(this.endPosition.point) < 10) { 63 | this.listener.performClick(this.endPosition); 64 | } else { 65 | this.listener.performScroll(this.startPosition, this.endPosition); 66 | } 67 | } 68 | } 69 | } 70 | if (handled) { 71 | if (event.cancelable) { 72 | event.preventDefault(); 73 | } 74 | event.stopPropagation(); 75 | } 76 | } 77 | if (event.type === 'mouseup') { 78 | this.startPosition = undefined; 79 | this.endPosition = undefined; 80 | } 81 | } 82 | 83 | protected onKey(): void { 84 | throw Error(`${TAG} Unsupported`); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/app/player/BroadwayPlayer.ts: -------------------------------------------------------------------------------- 1 | import '../../../vendor/Broadway/avc.wasm.asset'; 2 | import { BaseCanvasBasedPlayer } from './BaseCanvasBasedPlayer'; 3 | import Size from '../Size'; 4 | import YUVCanvas from '../../../vendor/h264-live-player/YUVCanvas'; 5 | import YUVWebGLCanvas from '../../../vendor/h264-live-player/YUVWebGLCanvas'; 6 | import Avc from '../../../vendor/Broadway/Decoder'; 7 | import VideoSettings from '../VideoSettings'; 8 | import Canvas from '../../../vendor/h264-live-player/Canvas'; 9 | import { DisplayInfo } from '../DisplayInfo'; 10 | 11 | export class BroadwayPlayer extends BaseCanvasBasedPlayer { 12 | public static readonly storageKeyPrefix = 'BroadwayDecoder'; 13 | public static readonly playerFullName = 'Broadway.js'; 14 | public static readonly playerCodeName = 'broadway'; 15 | public static readonly preferredVideoSettings: VideoSettings = new VideoSettings({ 16 | lockedVideoOrientation: -1, 17 | bitrate: 524288, 18 | maxFps: 24, 19 | iFrameInterval: 5, 20 | bounds: new Size(480, 480), 21 | sendFrameMeta: false, 22 | }); 23 | 24 | protected canvas?: Canvas; 25 | private avc?: Avc; 26 | public readonly supportsScreenshot: boolean = true; 27 | 28 | public static isSupported(): boolean { 29 | return typeof WebAssembly === 'object' && typeof WebAssembly.instantiate === 'function'; 30 | } 31 | 32 | constructor(udid: string, displayInfo?: DisplayInfo, name = BroadwayPlayer.playerFullName) { 33 | super(udid, displayInfo, name, BroadwayPlayer.storageKeyPrefix); 34 | } 35 | 36 | protected initCanvas(width: number, height: number): void { 37 | super.initCanvas(width, height); 38 | if (BaseCanvasBasedPlayer.hasWebGLSupport()) { 39 | this.canvas = new YUVWebGLCanvas(this.tag, new Size(width, height)); 40 | } else { 41 | this.canvas = new YUVCanvas(this.tag, new Size(width, height)); 42 | } 43 | if (!this.avc) { 44 | this.avc = new Avc(); 45 | } 46 | this.avc.onPictureDecoded = (buffer: Uint8Array, width: number, height: number) => { 47 | this.onFrameDecoded(width, height, buffer); 48 | }; 49 | } 50 | 51 | protected decode(data: Uint8Array): void { 52 | if (!this.avc) { 53 | return; 54 | } 55 | this.avc.decode(data); 56 | } 57 | 58 | public getPreferredVideoSetting(): VideoSettings { 59 | return BroadwayPlayer.preferredVideoSettings; 60 | } 61 | 62 | public getFitToScreenStatus(): boolean { 63 | return BroadwayPlayer.getFitToScreenStatus(this.udid, this.displayInfo); 64 | } 65 | 66 | public loadVideoSettings(): VideoSettings { 67 | return BroadwayPlayer.loadVideoSettings(this.udid, this.displayInfo); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/app/player/MjpegPlayer.ts: -------------------------------------------------------------------------------- 1 | import { BasePlayer } from './BasePlayer'; 2 | import VideoSettings from '../VideoSettings'; 3 | import { DisplayInfo } from '../DisplayInfo'; 4 | 5 | export class MjpegPlayer extends BasePlayer { 6 | private static dummyVideoSettings = new VideoSettings(); 7 | public static storageKeyPrefix = 'MjpegDecoder'; 8 | public static playerFullName = 'Mjpeg Http Player'; 9 | public static playerCodeName = 'mjpeghttp'; 10 | 11 | public static createElement(id?: string): HTMLImageElement { 12 | const tag = document.createElement('img') as HTMLImageElement; 13 | if (typeof id === 'string') { 14 | tag.id = id; 15 | } 16 | tag.className = 'video-layer'; 17 | return tag; 18 | } 19 | 20 | public static isSupported(): boolean { 21 | // I guess everything supports MJPEG? 22 | return true; 23 | } 24 | 25 | public readonly supportsScreenshot = true; 26 | public readonly resizeVideoToBounds: boolean = true; 27 | 28 | constructor( 29 | udid: string, 30 | displayInfo?: DisplayInfo, 31 | name = 'MJPEG_Player', 32 | storageKeyPrefix = 'MJPEG', 33 | protected tag: HTMLImageElement = MjpegPlayer.createElement(), 34 | ) { 35 | super(udid, displayInfo, name, storageKeyPrefix, tag); 36 | this.tag.onload = () => { 37 | this.checkVideoResize(); 38 | }; 39 | } 40 | 41 | public play(): void { 42 | super.play(); 43 | this.tag.setAttribute('src', `${location.protocol}//${location.host}/mjpeg/${this.udid}`); 44 | } 45 | 46 | public pause(): void { 47 | super.pause(); 48 | this.tag.removeAttribute('src'); 49 | } 50 | 51 | public stop(): void { 52 | super.stop(); 53 | this.tag.removeAttribute('src'); 54 | } 55 | 56 | protected needScreenInfoBeforePlay(): boolean { 57 | return false; 58 | } 59 | 60 | protected calculateMomentumStats(): void { 61 | // not implemented 62 | } 63 | 64 | getFitToScreenStatus(): boolean { 65 | return false; 66 | } 67 | 68 | getImageDataURL(): string { 69 | const canvas = document.createElement('canvas'); 70 | canvas.width = this.videoWidth; 71 | canvas.height = this.videoHeight; 72 | canvas.getContext('2d')?.drawImage(this.tag, 0, 0); 73 | return canvas.toDataURL('image/png'); 74 | } 75 | 76 | getPreferredVideoSetting(): VideoSettings { 77 | return MjpegPlayer.dummyVideoSettings; 78 | } 79 | 80 | loadVideoSettings(): VideoSettings { 81 | return MjpegPlayer.dummyVideoSettings; 82 | } 83 | 84 | checkVideoResize = (): void => { 85 | if (!this.tag) { 86 | return; 87 | } 88 | const { height, width } = this.tag; 89 | if (this.videoHeight !== height || this.videoWidth !== width) { 90 | this.calculateScreenInfoForBounds(width, height); 91 | } 92 | }; 93 | } 94 | -------------------------------------------------------------------------------- /src/app/player/MsePlayerForQVHack.ts: -------------------------------------------------------------------------------- 1 | import { MsePlayer } from './MsePlayer'; 2 | import Size from '../Size'; 3 | import VideoSettings from '../VideoSettings'; 4 | import { DisplayInfo } from '../DisplayInfo'; 5 | 6 | export class MsePlayerForQVHack extends MsePlayer { 7 | public static readonly preferredVideoSettings: VideoSettings = new VideoSettings({ 8 | lockedVideoOrientation: -1, 9 | bitrate: 8000000, 10 | maxFps: 30, 11 | iFrameInterval: 10, 12 | bounds: new Size(720, 720), 13 | sendFrameMeta: false, 14 | }); 15 | 16 | public readonly resizeVideoToBounds: boolean = true; 17 | constructor( 18 | udid: string, 19 | displayInfo?: DisplayInfo, 20 | name = 'MSE_Player_For_QVHack', 21 | tag = MsePlayerForQVHack.createElement(), 22 | ) { 23 | super(udid, displayInfo, name, tag); 24 | } 25 | 26 | protected needScreenInfoBeforePlay(): boolean { 27 | return false; 28 | } 29 | 30 | public getPreferredVideoSetting(): VideoSettings { 31 | return MsePlayerForQVHack.preferredVideoSettings; 32 | } 33 | 34 | public setVideoSettings(): void { 35 | return; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/app/toolbox/ToolBox.ts: -------------------------------------------------------------------------------- 1 | import { ToolBoxElement } from './ToolBoxElement'; 2 | 3 | export class ToolBox { 4 | private readonly holder: HTMLElement; 5 | 6 | constructor(list: ToolBoxElement[]) { 7 | this.holder = document.createElement('div'); 8 | this.holder.classList.add('control-buttons-list', 'control-wrapper'); 9 | list.forEach((item) => { 10 | item.getAllElements().forEach((el) => { 11 | this.holder.appendChild(el); 12 | }); 13 | }); 14 | } 15 | 16 | public getHolderElement(): HTMLElement { 17 | return this.holder; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/app/toolbox/ToolBoxButton.ts: -------------------------------------------------------------------------------- 1 | import { Optional, ToolBoxElement } from './ToolBoxElement'; 2 | import SvgImage, { Icon } from '../ui/SvgImage'; 3 | 4 | export class ToolBoxButton extends ToolBoxElement { 5 | private readonly btn: HTMLButtonElement; 6 | constructor(title: string, icon: Icon, optional?: Optional) { 7 | super(title, optional); 8 | const btn = document.createElement('button'); 9 | btn.classList.add('control-button'); 10 | btn.title = title; 11 | btn.appendChild(SvgImage.create(icon)); 12 | this.btn = btn; 13 | } 14 | 15 | public getElement(): HTMLButtonElement { 16 | return this.btn; 17 | } 18 | public getAllElements(): HTMLElement[] { 19 | return [this.btn]; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/app/toolbox/ToolBoxCheckbox.ts: -------------------------------------------------------------------------------- 1 | import { Optional, ToolBoxElement } from './ToolBoxElement'; 2 | import SvgImage, { Icon } from '../ui/SvgImage'; 3 | 4 | type Icons = { 5 | on?: Icon; 6 | off: Icon; 7 | }; 8 | 9 | export class ToolBoxCheckbox extends ToolBoxElement { 10 | private readonly input: HTMLInputElement; 11 | private readonly label: HTMLLabelElement; 12 | private readonly imageOn?: Element; 13 | private readonly imageOff: Element; 14 | constructor(title: string, icons: Icons | Icon, opt_id?: string, optional?: Optional) { 15 | super(title, optional); 16 | const input = document.createElement('input'); 17 | input.type = 'checkbox'; 18 | const label = document.createElement('label'); 19 | label.title = title; 20 | label.classList.add('control-button'); 21 | let iconOff: Icon; 22 | let iconOn: Icon | undefined; 23 | if (typeof icons !== 'number') { 24 | iconOff = icons.off; 25 | iconOn = icons.on; 26 | } else { 27 | iconOff = icons; 28 | } 29 | this.imageOff = SvgImage.create(iconOff); 30 | this.imageOff.classList.add('image', 'image-off'); 31 | label.appendChild(this.imageOff); 32 | if (iconOn) { 33 | this.imageOn = SvgImage.create(iconOn); 34 | this.imageOn.classList.add('image', 'image-on'); 35 | label.appendChild(this.imageOn); 36 | input.classList.add('two-images'); 37 | } 38 | const id = opt_id || title.toLowerCase().replace(' ', '_'); 39 | label.htmlFor = input.id = `input_${id}`; 40 | this.input = input; 41 | this.label = label; 42 | } 43 | 44 | public getElement(): HTMLInputElement { 45 | return this.input; 46 | } 47 | 48 | public getAllElements(): HTMLElement[] { 49 | return [this.input, this.label]; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/app/toolbox/ToolBoxElement.ts: -------------------------------------------------------------------------------- 1 | export type Optional = { 2 | [index: string]: any; 3 | }; 4 | 5 | // type Listener = (type: K, el: ToolBoxElement) => any; 6 | 7 | export abstract class ToolBoxElement { 8 | private listeners: Map(type: K, el: ToolBoxElement) => any>> = 9 | new Map(); 10 | protected constructor(public readonly title: string, public readonly optional?: Optional) {} 11 | 12 | public abstract getElement(): T; 13 | public abstract getAllElements(): HTMLElement[]; 14 | 15 | public addEventListener( 16 | type: K, 17 | listener: (type: K, el: ToolBoxElement) => any, 18 | options?: boolean | AddEventListenerOptions, 19 | ): void { 20 | const set = this.listeners.get(type) || new Set(); 21 | if (!set.size) { 22 | const element = this.getElement(); 23 | element.addEventListener(type, this.onEvent, options); 24 | } 25 | set.add(listener); 26 | this.listeners.set(type, set); 27 | } 28 | public removeEventListener( 29 | type: K, 30 | listener: (type: K, el: ToolBoxElement) => any, 31 | ): void { 32 | const set = this.listeners.get(type); 33 | if (!set) { 34 | return; 35 | } 36 | set.delete(listener); 37 | if (!set.size) { 38 | this.listeners.delete(type); 39 | const element = this.getElement(); 40 | element.removeEventListener(type, this.onEvent); 41 | } 42 | } 43 | onEvent = (ev: HTMLElementEventMap[K]): void => { 44 | const set = this.listeners.get(ev.type); 45 | if (!set) { 46 | return; 47 | } 48 | const type = ev.type as K; 49 | set.forEach((listener) => { 50 | listener(type, this); 51 | }); 52 | }; 53 | } 54 | -------------------------------------------------------------------------------- /src/app/ui/HtmlTag.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 2 | type Value = any; 3 | 4 | function htmlValue(value: Value): string { 5 | if (value instanceof HTMLTemplateElement) { 6 | return value.innerHTML; 7 | } 8 | if (typeof value === 'undefined') { 9 | return 'undefined'; 10 | } 11 | if (value === null) { 12 | return 'null'; 13 | } 14 | const e = document.createElement('dummy'); 15 | e.innerText = value.toString(); 16 | return e.innerHTML; 17 | } 18 | 19 | export const html = function html(strings: TemplateStringsArray, ...values: ReadonlyArray): HTMLTemplateElement { 20 | const template = document.createElement('template') as HTMLTemplateElement; 21 | template.innerHTML = values.reduce((acc, v, idx) => acc + htmlValue(v) + strings[idx + 1], strings[0]).toString(); 22 | return template; 23 | }; 24 | -------------------------------------------------------------------------------- /src/app/ui/SvgImage.ts: -------------------------------------------------------------------------------- 1 | import KeyboardSVG from '../../public/images/skin-light/ic_keyboard_678_48dp.svg'; 2 | import MoreSVG from '../../public/images/skin-light/ic_more_horiz_678_48dp.svg'; 3 | import CameraSVG from '../../public/images/skin-light/ic_photo_camera_678_48dp.svg'; 4 | import PowerSVG from '../../public/images/skin-light/ic_power_settings_new_678_48px.svg'; 5 | import VolumeDownSVG from '../../public/images/skin-light/ic_volume_down_678_48px.svg'; 6 | import VolumeUpSVG from '../../public/images/skin-light/ic_volume_up_678_48px.svg'; 7 | import BackSVG from '../../public/images/skin-light/System_Back_678.svg'; 8 | import HomeSVG from '../../public/images/skin-light/System_Home_678.svg'; 9 | import OverviewSVG from '../../public/images/skin-light/System_Overview_678.svg'; 10 | import CancelSVG from '../../public/images/buttons/cancel.svg'; 11 | import OfflineSVG from '../../public/images/buttons/offline.svg'; 12 | import RefreshSVG from '../../public/images/buttons/refresh.svg'; 13 | import SettingsSVG from '../../public/images/buttons/settings.svg'; 14 | import MenuSVG from '../../public/images/buttons/menu.svg'; 15 | import ArrowBackSVG from '../../public/images/buttons/arrow_back.svg'; 16 | import ToggleOnSVG from '../../public/images/buttons/toggle_on.svg'; 17 | import ToggleOffSVG from '../../public/images/buttons/toggle_off.svg'; 18 | 19 | export enum Icon { 20 | BACK, 21 | HOME, 22 | OVERVIEW, 23 | POWER, 24 | VOLUME_UP, 25 | VOLUME_DOWN, 26 | MORE, 27 | CAMERA, 28 | KEYBOARD, 29 | CANCEL, 30 | OFFLINE, 31 | REFRESH, 32 | SETTINGS, 33 | MENU, 34 | ARROW_BACK, 35 | TOGGLE_ON, 36 | TOGGLE_OFF, 37 | } 38 | 39 | export default class SvgImage { 40 | static Icon = Icon; 41 | private static getSvgString(type: Icon): string { 42 | switch (type) { 43 | case Icon.KEYBOARD: 44 | return KeyboardSVG; 45 | case Icon.MORE: 46 | return MoreSVG; 47 | case Icon.CAMERA: 48 | return CameraSVG; 49 | case Icon.POWER: 50 | return PowerSVG; 51 | case Icon.VOLUME_DOWN: 52 | return VolumeDownSVG; 53 | case Icon.VOLUME_UP: 54 | return VolumeUpSVG; 55 | case Icon.BACK: 56 | return BackSVG; 57 | case Icon.HOME: 58 | return HomeSVG; 59 | case Icon.OVERVIEW: 60 | return OverviewSVG; 61 | case Icon.CANCEL: 62 | return CancelSVG; 63 | case Icon.OFFLINE: 64 | return OfflineSVG; 65 | case Icon.REFRESH: 66 | return RefreshSVG; 67 | case Icon.SETTINGS: 68 | return SettingsSVG; 69 | case Icon.MENU: 70 | return MenuSVG; 71 | case Icon.ARROW_BACK: 72 | return ArrowBackSVG; 73 | case Icon.TOGGLE_ON: 74 | return ToggleOnSVG; 75 | case Icon.TOGGLE_OFF: 76 | return ToggleOffSVG; 77 | default: 78 | return ''; 79 | } 80 | } 81 | public static create(type: Icon): Element { 82 | const dummy = document.createElement('div'); 83 | dummy.innerHTML = this.getSvgString(type); 84 | const svg = dummy.children[0]; 85 | const titles = svg.getElementsByTagName('title'); 86 | for (let i = 0, l = titles.length; i < l; i++) { 87 | svg.removeChild(titles[i]); 88 | } 89 | return svg; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/common/Action.ts: -------------------------------------------------------------------------------- 1 | export enum ACTION { 2 | LIST_HOSTS = 'list-hosts', 3 | APPL_DEVICE_LIST = 'appl-device-list', 4 | GOOG_DEVICE_LIST = 'goog-device-list', 5 | MULTIPLEX = 'multiplex', 6 | SHELL = 'shell', 7 | PROXY_WS = 'proxy-ws', 8 | PROXY_ADB = 'proxy-adb', 9 | DEVTOOLS = 'devtools', 10 | STREAM_SCRCPY = 'stream', 11 | STREAM_WS_QVH = 'stream-qvh', 12 | STREAM_MJPEG = 'stream-mjpeg', 13 | PROXY_WDA = 'proxy-wda', 14 | FILE_LISTING = 'list-files', 15 | } 16 | -------------------------------------------------------------------------------- /src/common/ChannelCode.ts: -------------------------------------------------------------------------------- 1 | export enum ChannelCode { 2 | FSLS = 'FSLS', // File System LiSt 3 | HSTS = 'HSTS', // HoSTS List 4 | SHEL = 'SHEL', // SHELl 5 | GTRC = 'GTRC', // Goog device TRaCer 6 | ATRC = 'ATRC', // Appl device TRaCer 7 | WDAP = 'WDAP', // WebDriverAgent Proxy 8 | QVHS = 'QVHS', // Quicktime_Video_Hack Stream 9 | } 10 | -------------------------------------------------------------------------------- /src/common/Constants.ts: -------------------------------------------------------------------------------- 1 | export const SERVER_PACKAGE = 'com.genymobile.scrcpy.Server'; 2 | export const SERVER_PORT = 8886; 3 | export const SERVER_VERSION = '1.19-ws6'; 4 | 5 | export const SERVER_TYPE = 'web'; 6 | 7 | export const LOG_LEVEL = 'ERROR'; 8 | 9 | let SCRCPY_LISTENS_ON_ALL_INTERFACES; 10 | /// #if SCRCPY_LISTENS_ON_ALL_INTERFACES 11 | SCRCPY_LISTENS_ON_ALL_INTERFACES = true; 12 | /// #else 13 | SCRCPY_LISTENS_ON_ALL_INTERFACES = false; 14 | /// #endif 15 | 16 | const ARGUMENTS = [SERVER_VERSION, SERVER_TYPE, LOG_LEVEL, SERVER_PORT, SCRCPY_LISTENS_ON_ALL_INTERFACES]; 17 | 18 | export const SERVER_PROCESS_NAME = 'app_process'; 19 | 20 | export const ARGS_STRING = `/ ${SERVER_PACKAGE} ${ARGUMENTS.join(' ')} 2>&1 > /dev/null`; 21 | -------------------------------------------------------------------------------- /src/common/ControlCenterCommand.ts: -------------------------------------------------------------------------------- 1 | import { WDAMethod } from './WDAMethod'; 2 | 3 | export class ControlCenterCommand { 4 | public static KILL_SERVER = 'kill_server'; 5 | public static START_SERVER = 'start_server'; 6 | public static UPDATE_INTERFACES = 'update_interfaces'; 7 | public static CONFIGURE_STREAM = 'configure_stream'; 8 | public static RUN_WDA = 'run-wda'; 9 | public static REQUEST_WDA = 'request-wda'; 10 | 11 | private id = -1; 12 | private type = ''; 13 | private pid = 0; 14 | private udid = ''; 15 | private method = ''; 16 | private args?: any; 17 | private data?: any; 18 | 19 | public static fromJSON(json: string): ControlCenterCommand { 20 | const body = JSON.parse(json); 21 | if (!body) { 22 | throw new Error('Invalid input'); 23 | } 24 | const command = new ControlCenterCommand(); 25 | const data = (command.data = body.data); 26 | command.id = body.id; 27 | command.type = body.type; 28 | 29 | if (typeof data.udid === 'string') { 30 | command.udid = data.udid; 31 | } 32 | switch (body.type) { 33 | case this.KILL_SERVER: 34 | if (typeof data.pid !== 'number' && data.pid <= 0) { 35 | throw new Error('Invalid "pid" value'); 36 | } 37 | command.pid = data.pid; 38 | return command; 39 | case this.REQUEST_WDA: 40 | if (typeof data.method !== 'string') { 41 | throw new Error('Invalid "method" value'); 42 | } 43 | command.method = data.method; 44 | command.args = data.args; 45 | return command; 46 | case this.START_SERVER: 47 | case this.UPDATE_INTERFACES: 48 | case this.CONFIGURE_STREAM: 49 | case this.RUN_WDA: 50 | return command; 51 | default: 52 | throw new Error(`Unknown command "${body.command}"`); 53 | } 54 | } 55 | 56 | public getType(): string { 57 | return this.type; 58 | } 59 | public getPid(): number { 60 | return this.pid; 61 | } 62 | public getUdid(): string { 63 | return this.udid; 64 | } 65 | public getId(): number { 66 | return this.id; 67 | } 68 | public getMethod(): WDAMethod | string { 69 | return this.method; 70 | } 71 | public getData(): any { 72 | return this.data; 73 | } 74 | public getArgs(): any { 75 | return this.args; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/common/DeviceState.ts: -------------------------------------------------------------------------------- 1 | export enum DeviceState { 2 | DEVICE = 'device', 3 | DISCONNECTED = 'disconnected', 4 | 5 | CONNECTED = 'Connected', 6 | } 7 | -------------------------------------------------------------------------------- /src/common/HostTrackerMessage.ts: -------------------------------------------------------------------------------- 1 | import { Message } from '../types/Message'; 2 | import { HostItem } from '../types/Configuration'; 3 | 4 | export enum MessageType { 5 | HOSTS = 'hosts', 6 | ERROR = 'error', 7 | } 8 | 9 | export interface MessageHosts extends Message { 10 | type: 'hosts'; 11 | data: { 12 | local?: { type: string }[]; 13 | remote?: HostItem[]; 14 | }; 15 | } 16 | 17 | export interface MessageError extends Message { 18 | type: 'error'; 19 | data: string; 20 | } 21 | -------------------------------------------------------------------------------- /src/common/TypedEmitter.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from 'events'; 2 | 3 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 4 | export type EventMap = Record; 5 | export type EventKey = string & keyof T; 6 | export type EventReceiver = (params: T) => void; 7 | 8 | interface Emitter { 9 | on>(eventName: K, fn: EventReceiver): void; 10 | off>(eventName: K, fn: EventReceiver): void; 11 | emit>(eventName: K, params: T[K]): void; 12 | } 13 | 14 | export class TypedEmitter implements Emitter { 15 | private emitter = new EventEmitter(); 16 | addEventListener>(eventName: K, fn: EventReceiver): void { 17 | this.emitter.on(eventName, fn); 18 | } 19 | 20 | removeEventListener>(eventName: K, fn: EventReceiver): void { 21 | this.emitter.off(eventName, fn); 22 | } 23 | 24 | dispatchEvent(event: Event): boolean { 25 | return this.emitter.emit(event.type, event); 26 | } 27 | 28 | on>(eventName: K, fn: EventReceiver): void { 29 | this.emitter.on(eventName, fn); 30 | } 31 | 32 | once>(eventName: K, fn: EventReceiver): void { 33 | this.emitter.once(eventName, fn); 34 | } 35 | 36 | off>(eventName: K, fn: EventReceiver): void { 37 | this.emitter.off(eventName, fn); 38 | } 39 | 40 | emit>(eventName: K, params: T[K]): boolean { 41 | return this.emitter.emit(eventName, params); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/common/WDAMethod.ts: -------------------------------------------------------------------------------- 1 | export enum WDAMethod { 2 | CLICK = 'CLICK', 3 | SCROLL = 'SCROLL', 4 | PRESS_BUTTON = 'PRESS_BUTTON', 5 | GET_SCREEN_WIDTH = 'GET_SCREEN_WIDTH', 6 | APPIUM_SETTINGS = 'APPIUM_SETTINGS', 7 | SEND_KEYS = 'SEND_KEYS', 8 | } 9 | -------------------------------------------------------------------------------- /src/common/WdaStatus.ts: -------------------------------------------------------------------------------- 1 | export enum WdaStatus { 2 | STARTING = 'STARTING', 3 | STARTED = 'STARTED', 4 | STOPPED = 'STOPPED', 5 | } 6 | -------------------------------------------------------------------------------- /src/packages/multiplexer/CloseEventClass.ts: -------------------------------------------------------------------------------- 1 | import { Event2 } from './Event'; 2 | 3 | export class CloseEvent2 extends Event2 implements CloseEvent { 4 | readonly code: number; 5 | readonly reason: string; 6 | readonly wasClean: boolean; 7 | constructor(type: string, { code, reason }: CloseEventInit = {}) { 8 | super(type); 9 | this.code = code || 0; 10 | this.reason = reason || ''; 11 | this.wasClean = this.code === 0; 12 | } 13 | } 14 | 15 | export const CloseEventClass = typeof CloseEvent !== 'undefined' ? CloseEvent : CloseEvent2; 16 | -------------------------------------------------------------------------------- /src/packages/multiplexer/ErrorEventClass.ts: -------------------------------------------------------------------------------- 1 | import { Event2 } from './Event'; 2 | 3 | export class ErrorEvent2 extends Event2 implements ErrorEvent { 4 | readonly colno: number; 5 | readonly error: any; 6 | readonly filename: string; 7 | readonly lineno: number; 8 | readonly message: string; 9 | 10 | constructor(type: string, { colno, error, filename, lineno, message }: ErrorEventInit = {}) { 11 | super(type); 12 | this.error = error; 13 | this.colno = colno || 0; 14 | this.filename = filename || ''; 15 | this.lineno = lineno || 0; 16 | this.message = message || ''; 17 | } 18 | } 19 | 20 | export const ErrorEventClass = typeof ErrorEvent !== 'undefined' ? ErrorEvent : ErrorEvent2; 21 | -------------------------------------------------------------------------------- /src/packages/multiplexer/Event.ts: -------------------------------------------------------------------------------- 1 | export class Event2 { 2 | static NONE = 0; 3 | static CAPTURING_PHASE = 1; 4 | static AT_TARGET = 2; 5 | static BUBBLING_PHASE = 3; 6 | 7 | public cancelable: boolean; 8 | public bubbles: boolean; 9 | public composed: boolean; 10 | public type: string; 11 | public defaultPrevented: boolean; 12 | public timeStamp: number; 13 | public target: any; 14 | public readonly isTrusted: boolean = true; 15 | readonly AT_TARGET: number = 0; 16 | readonly BUBBLING_PHASE: number = 0; 17 | readonly CAPTURING_PHASE: number = 0; 18 | readonly NONE: number = 0; 19 | 20 | constructor(type: string, options = { cancelable: true, bubbles: true, composed: false }) { 21 | const { cancelable, bubbles, composed } = { ...options }; 22 | this.cancelable = !!cancelable; 23 | this.bubbles = !!bubbles; 24 | this.composed = !!composed; 25 | this.type = `${type}`; 26 | this.defaultPrevented = false; 27 | this.timeStamp = Date.now(); 28 | this.target = null; 29 | } 30 | 31 | stopImmediatePropagation() { 32 | // this[kStop] = true; 33 | } 34 | 35 | preventDefault() { 36 | this.defaultPrevented = true; 37 | } 38 | 39 | get currentTarget() { 40 | return this.target; 41 | } 42 | get srcElement() { 43 | return this.target; 44 | } 45 | 46 | composedPath() { 47 | return this.target ? [this.target] : []; 48 | } 49 | get returnValue() { 50 | return !this.defaultPrevented; 51 | } 52 | get eventPhase() { 53 | return this.target ? Event.AT_TARGET : Event.NONE; 54 | } 55 | get cancelBubble() { 56 | return false; 57 | // return this.propagationStopped; 58 | } 59 | set cancelBubble(value: any) { 60 | if (value) { 61 | this.stopPropagation(); 62 | } 63 | } 64 | stopPropagation() { 65 | // this.propagationStopped = true; 66 | } 67 | initEvent(type: string, bubbles?: boolean, cancelable?: boolean): void { 68 | this.type = type; 69 | if (arguments.length > 1) { 70 | this.bubbles = !!bubbles; 71 | } 72 | if (arguments.length > 2) { 73 | this.cancelable = !!cancelable; 74 | } 75 | } 76 | } 77 | 78 | export const EventClass = typeof Event !== 'undefined' ? Event : Event2; 79 | -------------------------------------------------------------------------------- /src/packages/multiplexer/Message.ts: -------------------------------------------------------------------------------- 1 | import { MessageType } from './MessageType'; 2 | import Util from '../../app/Util'; 3 | import { CloseEventClass } from './CloseEventClass'; 4 | 5 | export class Message { 6 | public static parse(buffer: ArrayBuffer): Message { 7 | const view = Buffer.from(buffer); 8 | 9 | const type: MessageType = view.readUInt8(0); 10 | const channelId = view.readUInt32LE(1); 11 | const data: ArrayBuffer = buffer.slice(5); 12 | 13 | return new Message(type, channelId, data); 14 | } 15 | 16 | public static fromCloseEvent(id: number, code: number, reason?: string): Message { 17 | const reasonBuffer = reason ? Util.stringToUtf8ByteArray(reason) : Buffer.alloc(0); 18 | const buffer = Buffer.alloc(2 + 4 + reasonBuffer.byteLength); 19 | buffer.writeUInt16LE(code, 0); 20 | if (reasonBuffer.byteLength) { 21 | buffer.writeUInt32LE(reasonBuffer.byteLength, 2); 22 | buffer.set(reasonBuffer, 6); 23 | } 24 | return new Message(MessageType.CloseChannel, id, buffer); 25 | } 26 | 27 | public static createBuffer(type: MessageType, channelId: number, data?: ArrayBuffer): Buffer { 28 | const result = Buffer.alloc(5 + (data ? data.byteLength : 0)); 29 | result.writeUInt8(type, 0); 30 | result.writeUInt32LE(channelId, 1); 31 | if (data?.byteLength) { 32 | result.set(Buffer.from(data), 5); 33 | } 34 | return result; 35 | } 36 | 37 | public constructor( 38 | public readonly type: MessageType, 39 | public readonly channelId: number, 40 | public readonly data: ArrayBuffer, 41 | ) {} 42 | 43 | public toCloseEvent(): CloseEvent { 44 | let code: number | undefined; 45 | let reason: string | undefined; 46 | if (this.data && this.data.byteLength) { 47 | const buffer = Buffer.from(this.data); 48 | code = buffer.readUInt16LE(0); 49 | if (buffer.byteLength > 6) { 50 | const length = buffer.readUInt32LE(2); 51 | reason = Util.utf8ByteArrayToString(buffer.slice(6, 6 + length)); 52 | } 53 | } 54 | return new CloseEventClass('close', { 55 | code, 56 | reason, 57 | wasClean: code === 1000, 58 | }); 59 | } 60 | 61 | public toBuffer(): ArrayBuffer { 62 | return Message.createBuffer(this.type, this.channelId, this.data); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/packages/multiplexer/MessageEventClass.ts: -------------------------------------------------------------------------------- 1 | import { Event2 } from './Event'; 2 | 3 | export class MessageEvent2 extends Event2 implements MessageEvent { 4 | public readonly data: any; 5 | public readonly origin: string; 6 | public readonly lastEventId: string; 7 | public readonly source: any; 8 | public readonly ports: ReadonlyArray; 9 | constructor( 10 | type: string, 11 | { data = null, origin = '', lastEventId = '', source = null, ports = [] }: MessageEventInit = {}, 12 | ) { 13 | super(type); 14 | this.data = data; 15 | this.origin = `${origin}`; 16 | this.lastEventId = `${lastEventId}`; 17 | this.source = source; 18 | this.ports = [...ports]; 19 | } 20 | 21 | initMessageEvent(): void { 22 | throw Error('Deprecated method'); 23 | } 24 | } 25 | 26 | export const MessageEventClass = typeof MessageEvent !== 'undefined' ? MessageEvent : MessageEvent2; 27 | -------------------------------------------------------------------------------- /src/packages/multiplexer/MessageType.ts: -------------------------------------------------------------------------------- 1 | export enum MessageType { 2 | CreateChannel = 4, 3 | CloseChannel = 8, 4 | RawBinaryData = 16, 5 | RawStringData = 32, 6 | Data = 64, 7 | } 8 | -------------------------------------------------------------------------------- /src/public/images/buttons/arrow_back.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/public/images/buttons/cancel.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/public/images/buttons/menu.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/public/images/buttons/offline.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/public/images/buttons/refresh.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/public/images/buttons/settings.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/public/images/buttons/toggle_off.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/public/images/buttons/toggle_on.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/public/images/multitouch/SOURCE: -------------------------------------------------------------------------------- 1 | https://android.googlesource.com/platform/external/qemu/+/emu-2.0-release/android/skin/qt/images/multitouch/ 2 | -------------------------------------------------------------------------------- /src/public/images/multitouch/center_point.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NetrisTV/ws-scrcpy/9136dd930eaad27a874c0807476e338b3a1ed5f9/src/public/images/multitouch/center_point.png -------------------------------------------------------------------------------- /src/public/images/multitouch/center_point_2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NetrisTV/ws-scrcpy/9136dd930eaad27a874c0807476e338b3a1ed5f9/src/public/images/multitouch/center_point_2x.png -------------------------------------------------------------------------------- /src/public/images/multitouch/touch_point.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NetrisTV/ws-scrcpy/9136dd930eaad27a874c0807476e338b3a1ed5f9/src/public/images/multitouch/touch_point.png -------------------------------------------------------------------------------- /src/public/images/multitouch/touch_point_2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NetrisTV/ws-scrcpy/9136dd930eaad27a874c0807476e338b3a1ed5f9/src/public/images/multitouch/touch_point_2x.png -------------------------------------------------------------------------------- /src/public/images/skin-light/SOURCE: -------------------------------------------------------------------------------- 1 | https://android.googlesource.com/platform/external/qemu/+/emu-2.0-release/android/skin/qt/images/light/ 2 | -------------------------------------------------------------------------------- /src/public/images/skin-light/System_Back_678.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Artboard 1 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/public/images/skin-light/System_Home_678.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | System_Home 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/public/images/skin-light/System_Overview_678.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | System_Overview 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/public/images/skin-light/ic_keyboard_678_48dp.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/public/images/skin-light/ic_more_horiz_678_48dp.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/public/images/skin-light/ic_photo_camera_678_48dp.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/public/images/skin-light/ic_power_settings_new_678_48px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/public/images/skin-light/ic_volume_down_678_48px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/public/images/skin-light/ic_volume_up_678_48px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | WS scrcpy 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/server/EnvName.ts: -------------------------------------------------------------------------------- 1 | export enum EnvName { 2 | CONFIG_PATH = 'WS_SCRCPY_CONFIG', 3 | WS_SCRCPY_PATHNAME = 'WS_SCRCPY_PATHNAME', 4 | } 5 | -------------------------------------------------------------------------------- /src/server/Utils.ts: -------------------------------------------------------------------------------- 1 | import * as os from 'os'; 2 | 3 | export class Utils { 4 | public static printListeningMsg(proto: string, port: number, pathname: string): void { 5 | const ipv4List: string[] = []; 6 | const ipv6List: string[] = []; 7 | const formatAddress = (ip: string, scopeid: number | undefined): void => { 8 | if (typeof scopeid === 'undefined') { 9 | ipv4List.push(`${proto}://${ip}:${port}${pathname}`); 10 | return; 11 | } 12 | if (scopeid === 0) { 13 | ipv6List.push(`${proto}://[${ip}]:${port}${pathname}`); 14 | } else { 15 | return; 16 | // skip 17 | // ipv6List.push(`${proto}://[${ip}%${scopeid}]:${port}`); 18 | } 19 | }; 20 | Object.keys(os.networkInterfaces()) 21 | .map((key) => os.networkInterfaces()[key]) 22 | .forEach((info) => { 23 | info.forEach((iface) => { 24 | let scopeid: number | undefined; 25 | if (iface.family === 'IPv6') { 26 | scopeid = iface.scopeid; 27 | } else if (iface.family === 'IPv4') { 28 | scopeid = undefined; 29 | } else { 30 | return; 31 | } 32 | formatAddress(iface.address, scopeid); 33 | }); 34 | }); 35 | const nameList = [ 36 | encodeURI(`${proto}://${os.hostname()}:${port}${pathname}`), 37 | encodeURI(`${proto}://localhost:${port}${pathname}`), 38 | ]; 39 | console.log('Listening on:\n\t' + nameList.join(' ')); 40 | if (ipv4List.length) { 41 | console.log('\t' + ipv4List.join(' ')); 42 | } 43 | if (ipv6List.length) { 44 | console.log('\t' + ipv6List.join(' ')); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/server/appl-device/mw/DeviceTracker.ts: -------------------------------------------------------------------------------- 1 | import WS from 'ws'; 2 | import { Mw, RequestParameters } from '../../mw/Mw'; 3 | import { ControlCenterCommand } from '../../../common/ControlCenterCommand'; 4 | import { ACTION } from '../../../common/Action'; 5 | import { DeviceTrackerEvent } from '../../../types/DeviceTrackerEvent'; 6 | import { DeviceTrackerEventList } from '../../../types/DeviceTrackerEventList'; 7 | import { ControlCenter } from '../services/ControlCenter'; 8 | import ApplDeviceDescriptor from '../../../types/ApplDeviceDescriptor'; 9 | import { Multiplexer } from '../../../packages/multiplexer/Multiplexer'; 10 | import { ChannelCode } from '../../../common/ChannelCode'; 11 | 12 | export class DeviceTracker extends Mw { 13 | public static readonly TAG = 'IosDeviceTracker'; 14 | public static readonly type = 'ios'; 15 | private icc: ControlCenter = ControlCenter.getInstance(); 16 | private readonly id: string; 17 | 18 | public static processChannel(ws: Multiplexer, code: string): Mw | undefined { 19 | if (code !== ChannelCode.ATRC) { 20 | return; 21 | } 22 | return new DeviceTracker(ws); 23 | } 24 | 25 | public static processRequest(ws: WS, params: RequestParameters): DeviceTracker | undefined { 26 | if (params.action !== ACTION.APPL_DEVICE_LIST) { 27 | return; 28 | } 29 | return new DeviceTracker(ws); 30 | } 31 | 32 | constructor(ws: WS | Multiplexer) { 33 | super(ws); 34 | 35 | this.id = this.icc.getId(); 36 | this.icc 37 | .init() 38 | .then(() => { 39 | this.icc.on('device', this.sendDeviceMessage); 40 | this.buildAndSendMessage(this.icc.getDevices()); 41 | }) 42 | .catch((error: Error) => { 43 | console.error(`[${DeviceTracker.TAG}] Error: ${error.message}`); 44 | }); 45 | } 46 | 47 | private sendDeviceMessage = (device: ApplDeviceDescriptor): void => { 48 | const data: DeviceTrackerEvent = { 49 | device, 50 | id: this.id, 51 | name: this.icc.getName(), 52 | }; 53 | this.sendMessage({ 54 | id: -1, 55 | type: 'device', 56 | data, 57 | }); 58 | }; 59 | 60 | private buildAndSendMessage = (list: ApplDeviceDescriptor[]): void => { 61 | const data: DeviceTrackerEventList = { 62 | list, 63 | id: this.id, 64 | name: this.icc.getName(), 65 | }; 66 | this.sendMessage({ 67 | id: -1, 68 | type: 'devicelist', 69 | data, 70 | }); 71 | }; 72 | 73 | protected onSocketMessage(event: WS.MessageEvent): void { 74 | let command: ControlCenterCommand; 75 | try { 76 | command = ControlCenterCommand.fromJSON(event.data.toString()); 77 | } catch (error: any) { 78 | console.error(`[${DeviceTracker.TAG}], Received message: ${event.data}. Error: ${error.message}`); 79 | return; 80 | } 81 | this.icc.runCommand(command).catch((error) => { 82 | console.error(`[${DeviceTracker.TAG}], Received message: ${event.data}. Error: ${error.message}`); 83 | }); 84 | } 85 | 86 | public release(): void { 87 | super.release(); 88 | this.icc.off('device', this.sendDeviceMessage); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/server/appl-device/mw/QVHStreamProxy.ts: -------------------------------------------------------------------------------- 1 | import WS from 'ws'; 2 | import { Mw } from '../../mw/Mw'; 3 | import { ControlCenterCommand } from '../../../common/ControlCenterCommand'; 4 | import { QvhackRunner } from '../services/QvhackRunner'; 5 | import { WebsocketProxy } from '../../mw/WebsocketProxy'; 6 | import { Multiplexer } from '../../../packages/multiplexer/Multiplexer'; 7 | import { ChannelCode } from '../../../common/ChannelCode'; 8 | import Util from '../../../app/Util'; 9 | 10 | export class QVHStreamProxy extends Mw { 11 | public static readonly TAG = 'QVHStreamProxy'; 12 | 13 | public static processChannel(ws: Multiplexer, code: string, data: ArrayBuffer): Mw | undefined { 14 | if (code !== ChannelCode.QVHS) { 15 | return; 16 | } 17 | if (!data || data.byteLength < 4) { 18 | return; 19 | } 20 | const buffer = Buffer.from(data); 21 | const length = buffer.readInt32LE(0); 22 | const udid = Util.utf8ByteArrayToString(buffer.slice(4, 4 + length)); 23 | return new QVHStreamProxy(ws, udid); 24 | } 25 | 26 | private qvhProcess: QvhackRunner; 27 | private wsProxy?: WebsocketProxy; 28 | protected name: string; 29 | constructor(protected ws: Multiplexer, private readonly udid: string) { 30 | super(ws); 31 | this.name = `[${QVHStreamProxy.TAG}][udid:${this.udid}]`; 32 | this.qvhProcess = QvhackRunner.getInstance(udid); 33 | this.attachEventListeners(); 34 | } 35 | 36 | private onStarted = (): void => { 37 | const remote = this.qvhProcess.getWebSocketAddress(); 38 | this.wsProxy = WebsocketProxy.createProxy(this.ws, remote); 39 | this.ws.addEventListener('close', this.onSocketClose.bind(this)); 40 | }; 41 | 42 | private attachEventListeners(): void { 43 | if (this.qvhProcess.isStarted()) { 44 | this.onStarted(); 45 | } else { 46 | this.qvhProcess.once('started', this.onStarted); 47 | } 48 | } 49 | 50 | protected onSocketMessage(event: WS.MessageEvent): void { 51 | let command: ControlCenterCommand; 52 | try { 53 | command = ControlCenterCommand.fromJSON(event.data.toString()); 54 | } catch (error: any) { 55 | console.error(`${this.name}, Received message: ${event.data}. Error: ${error.message}`); 56 | return; 57 | } 58 | console.log(`${this.name}, Received message: type:"${command.getType()}", data:${command.getData()}.`); 59 | } 60 | 61 | protected onSocketClose(): void { 62 | if (this.wsProxy) { 63 | this.wsProxy.release(); 64 | delete this.wsProxy; 65 | } 66 | this.release(); 67 | } 68 | 69 | public release(): void { 70 | super.release(); 71 | if (this.qvhProcess) { 72 | this.qvhProcess.release(); 73 | } 74 | if (this.wsProxy) { 75 | this.wsProxy.release(); 76 | delete this.wsProxy; 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/server/appl-device/services/QvhackRunner.ts: -------------------------------------------------------------------------------- 1 | import * as portfinder from 'portfinder'; 2 | import { ProcessRunner, ProcessRunnerEvents } from '../../services/ProcessRunner'; 3 | 4 | export class QvhackRunner extends ProcessRunner { 5 | private static instances: Map = new Map(); 6 | public static SHUTDOWN_TIMEOUT = 15000; 7 | public static getInstance(udid: string): QvhackRunner { 8 | let instance = this.instances.get(udid); 9 | if (!instance) { 10 | instance = new QvhackRunner(udid); 11 | this.instances.set(udid, instance); 12 | instance.start(); 13 | } 14 | instance.lock(); 15 | return instance; 16 | } 17 | protected TAG = '[QvhackRunner]'; 18 | protected name: string; 19 | protected cmd = 'ws-qvh'; 20 | protected releaseTimeoutId?: NodeJS.Timeout; 21 | protected address = ''; 22 | protected started = false; 23 | private holders = 0; 24 | 25 | constructor(private readonly udid: string) { 26 | super(); 27 | this.name = `${this.TAG}[udid: ${this.udid}]`; 28 | } 29 | 30 | public getWebSocketAddress(): string { 31 | return this.address; 32 | } 33 | 34 | protected lock(): void { 35 | if (this.releaseTimeoutId) { 36 | clearTimeout(this.releaseTimeoutId); 37 | } 38 | this.holders++; 39 | } 40 | 41 | protected unlock(): void { 42 | this.holders--; 43 | if (this.holders > 0) { 44 | return; 45 | } 46 | this.releaseTimeoutId = setTimeout(() => { 47 | super.release(); 48 | QvhackRunner.instances.delete(this.udid); 49 | }, QvhackRunner.SHUTDOWN_TIMEOUT); 50 | } 51 | 52 | protected async getArgs(): Promise { 53 | const port = await portfinder.getPortPromise(); 54 | const host = `127.0.0.1:${port}`; 55 | this.address = `ws://${host}/ws?stream=${encodeURIComponent(this.udid)}`; 56 | return [host]; 57 | } 58 | 59 | public async start(): Promise { 60 | return this.runProcess() 61 | .then(() => { 62 | // Wait for server to start listen on a port 63 | this.once('stderr', () => { 64 | this.started = true; 65 | this.emit('started', true); 66 | }); 67 | }) 68 | .catch((e) => { 69 | console.error(this.name, e.message); 70 | }); 71 | } 72 | 73 | public isStarted(): boolean { 74 | return this.started; 75 | } 76 | 77 | public release(): void { 78 | this.unlock(); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/server/goog-device/Properties.ts: -------------------------------------------------------------------------------- 1 | import GoogDeviceDescriptor from '../../types/GoogDeviceDescriptor'; 2 | 3 | export const Properties: ReadonlyArray = [ 4 | 'ro.product.cpu.abi', 5 | 'ro.product.manufacturer', 6 | 'ro.product.model', 7 | 'ro.build.version.release', 8 | 'ro.build.version.sdk', 9 | 'wifi.interface', 10 | ]; 11 | -------------------------------------------------------------------------------- /src/server/goog-device/ServerVersion.ts: -------------------------------------------------------------------------------- 1 | export class ServerVersion { 2 | protected parts: string[] = []; 3 | protected suffix: string; 4 | protected readonly compatible: boolean; 5 | 6 | constructor(public readonly versionString: string) { 7 | const temp = versionString.split('-'); 8 | const main = temp.shift(); 9 | this.suffix = temp.join('-'); 10 | if (main) { 11 | this.parts = main.split('.'); 12 | } 13 | this.compatible = this.suffix.startsWith('ws') && this.parts.length >= 2; 14 | } 15 | public equals(a: ServerVersion | string): boolean { 16 | const versionString = typeof a === 'string' ? a : a.versionString; 17 | return this.versionString === versionString; 18 | } 19 | public gt(a: ServerVersion | string): boolean { 20 | if (this.equals(a)) { 21 | return false; 22 | } 23 | if (typeof a === 'string') { 24 | a = new ServerVersion(a); 25 | } 26 | const minLength = Math.min(this.parts.length, a.parts.length); 27 | for (let i = 0; i < minLength; i++) { 28 | if (this.parts[i] > a.parts[i]) { 29 | return true; 30 | } 31 | } 32 | if (this.parts.length > a.parts.length) { 33 | return true; 34 | } 35 | if (this.parts.length < a.parts.length) { 36 | return false; 37 | } 38 | return this.suffix > a.suffix; 39 | } 40 | public isCompatible(): boolean { 41 | return this.compatible; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/server/goog-device/adb/ExtendedClient.ts: -------------------------------------------------------------------------------- 1 | import Client from '@dead50f7/adbkit/lib/adb/client'; 2 | import { ExtendedSync } from './ExtendedSync'; 3 | import { SyncCommand } from './command/host-transport/sync'; 4 | import { Multiplexer } from '../../../packages/multiplexer/Multiplexer'; 5 | 6 | export class ExtendedClient extends Client { 7 | public async pipeSyncService(serial: string): Promise { 8 | const transport = await this.transport(serial); 9 | return new SyncCommand(transport).execute(); 10 | } 11 | 12 | public async pipeReadDir(serial: string, pathString: string, stream: Multiplexer): Promise { 13 | const sync = await this.pipeSyncService(serial); 14 | return sync.pipeReadDir(pathString, stream).then(() => { 15 | sync.end(); 16 | }); 17 | } 18 | 19 | public async pipePull(serial: string, path: string, stream: Multiplexer): Promise { 20 | const sync = await this.pipeSyncService(serial); 21 | return sync.pipePull(path, stream).then(() => { 22 | sync.end(); 23 | }); 24 | } 25 | 26 | public async pipeStat(serial: string, path: string, stream: Multiplexer): Promise { 27 | const sync = await this.pipeSyncService(serial); 28 | return sync.pipeStat(path, stream).then(() => { 29 | sync.end(); 30 | }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/server/goog-device/adb/command/host-transport/sync.ts: -------------------------------------------------------------------------------- 1 | import Protocol from '@dead50f7/adbkit/lib/adb/protocol'; 2 | import Command from '@dead50f7/adbkit/lib/adb/command'; 3 | import { ExtendedSync } from '../../ExtendedSync'; 4 | import Bluebird from 'bluebird'; 5 | 6 | export class SyncCommand extends Command { 7 | execute(): Bluebird { 8 | this._send('sync:'); 9 | return this.parser.readAscii(4).then((reply) => { 10 | switch (reply) { 11 | case Protocol.OKAY: 12 | return new ExtendedSync(this.connection); 13 | case Protocol.FAIL: 14 | return this.parser.readError(); 15 | default: 16 | return this.parser.unexpected(reply, 'OKAY or FAIL'); 17 | } 18 | }); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/server/goog-device/adb/index.ts: -------------------------------------------------------------------------------- 1 | import Adb from '@dead50f7/adbkit/lib/adb'; 2 | import { ExtendedClient } from './ExtendedClient'; 3 | import { ClientOptions } from '@dead50f7/adbkit/lib/ClientOptions'; 4 | 5 | interface Options { 6 | host?: string; 7 | port?: number; 8 | bin?: string; 9 | } 10 | 11 | export class AdbExtended extends Adb { 12 | static createClient(options: Options = {}): ExtendedClient { 13 | const opts: ClientOptions = { 14 | bin: options.bin, 15 | host: options.host || process.env.ADB_HOST || '127.0.0.1', 16 | port: options.port || 0, 17 | }; 18 | if (!opts.port) { 19 | const port = parseInt(process.env.ADB_PORT || '', 10); 20 | if (!isNaN(port)) { 21 | opts.port = port; 22 | } else { 23 | opts.port = 5037; 24 | } 25 | } 26 | return new ExtendedClient(opts); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/server/goog-device/filePush/ReadStream.ts: -------------------------------------------------------------------------------- 1 | import { Readable, ReadableOptions } from 'stream'; 2 | 3 | export class ReadStream extends Readable { 4 | private _bytesRead = 0; 5 | constructor(private readonly _path: string, opts?: ReadableOptions) { 6 | super(opts); 7 | } 8 | public get bytesRead(): number { 9 | return this._bytesRead; 10 | } 11 | public get path(): string | Buffer { 12 | return this._path; 13 | } 14 | public push(chunk: any, encoding?: string): boolean { 15 | if (chunk) { 16 | this._bytesRead += chunk.length; 17 | } 18 | return super.push(chunk, encoding); 19 | } 20 | 21 | public close(): void { 22 | this.destroy(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/server/goog-device/mw/DeviceTracker.ts: -------------------------------------------------------------------------------- 1 | import WS from 'ws'; 2 | import { Mw, RequestParameters } from '../../mw/Mw'; 3 | import { ControlCenterCommand } from '../../../common/ControlCenterCommand'; 4 | import { ControlCenter } from '../services/ControlCenter'; 5 | import { ACTION } from '../../../common/Action'; 6 | import GoogDeviceDescriptor from '../../../types/GoogDeviceDescriptor'; 7 | import { DeviceTrackerEvent } from '../../../types/DeviceTrackerEvent'; 8 | import { DeviceTrackerEventList } from '../../../types/DeviceTrackerEventList'; 9 | import { Multiplexer } from '../../../packages/multiplexer/Multiplexer'; 10 | import { ChannelCode } from '../../../common/ChannelCode'; 11 | 12 | export class DeviceTracker extends Mw { 13 | public static readonly TAG = 'DeviceTracker'; 14 | public static readonly type = 'android'; 15 | private adt: ControlCenter = ControlCenter.getInstance(); 16 | private readonly id: string; 17 | 18 | public static processChannel(ws: Multiplexer, code: string): Mw | undefined { 19 | if (code !== ChannelCode.GTRC) { 20 | return; 21 | } 22 | return new DeviceTracker(ws); 23 | } 24 | 25 | public static processRequest(ws: WS, params: RequestParameters): DeviceTracker | undefined { 26 | if (params.action !== ACTION.GOOG_DEVICE_LIST) { 27 | return; 28 | } 29 | return new DeviceTracker(ws); 30 | } 31 | 32 | constructor(ws: WS | Multiplexer) { 33 | super(ws); 34 | 35 | this.id = this.adt.getId(); 36 | this.adt 37 | .init() 38 | .then(() => { 39 | this.adt.on('device', this.sendDeviceMessage); 40 | this.buildAndSendMessage(this.adt.getDevices()); 41 | }) 42 | .catch((error: Error) => { 43 | console.error(`[${DeviceTracker.TAG}] Error: ${error.message}`); 44 | }); 45 | } 46 | 47 | private sendDeviceMessage = (device: GoogDeviceDescriptor): void => { 48 | const data: DeviceTrackerEvent = { 49 | device, 50 | id: this.id, 51 | name: this.adt.getName(), 52 | }; 53 | this.sendMessage({ 54 | id: -1, 55 | type: 'device', 56 | data, 57 | }); 58 | }; 59 | 60 | private buildAndSendMessage = (list: GoogDeviceDescriptor[]): void => { 61 | const data: DeviceTrackerEventList = { 62 | list, 63 | id: this.id, 64 | name: this.adt.getName(), 65 | }; 66 | this.sendMessage({ 67 | id: -1, 68 | type: 'devicelist', 69 | data, 70 | }); 71 | }; 72 | 73 | protected onSocketMessage(event: WS.MessageEvent): void { 74 | let command: ControlCenterCommand; 75 | try { 76 | command = ControlCenterCommand.fromJSON(event.data.toString()); 77 | } catch (error: any) { 78 | console.error(`[${DeviceTracker.TAG}], Received message: ${event.data}. Error: ${error?.message}`); 79 | return; 80 | } 81 | this.adt.runCommand(command).catch((e) => { 82 | console.error(`[${DeviceTracker.TAG}], Received message: ${event.data}. Error: ${e.message}`); 83 | }); 84 | } 85 | 86 | public release(): void { 87 | super.release(); 88 | this.adt.off('device', this.sendDeviceMessage); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/server/goog-device/mw/RemoteDevtools.ts: -------------------------------------------------------------------------------- 1 | import WS from 'ws'; 2 | import { Mw, RequestParameters } from '../../mw/Mw'; 3 | import { RemoteDevtoolsCommand } from '../../../types/RemoteDevtoolsCommand'; 4 | import { AdbUtils } from '../AdbUtils'; 5 | import { ACTION } from '../../../common/Action'; 6 | 7 | export class RemoteDevtools extends Mw { 8 | public static readonly TAG = 'RemoteDevtools'; 9 | public static processRequest(ws: WS, params: RequestParameters): RemoteDevtools | undefined { 10 | const { action, request, url } = params; 11 | if (action !== ACTION.DEVTOOLS) { 12 | return; 13 | } 14 | const host = request.headers['host']; 15 | const udid = url.searchParams.get('udid'); 16 | if (!udid) { 17 | ws.close(4003, `[${this.TAG}] Invalid value "${udid}" for "udid" parameter`); 18 | return; 19 | } 20 | if (typeof host !== 'string' || !host) { 21 | ws.close(4003, `[${this.TAG}] Invalid value "${host}" in "Host" header`); 22 | return; 23 | } 24 | return new RemoteDevtools(ws, host, udid); 25 | } 26 | constructor(protected ws: WS, private readonly host: string, private readonly udid: string) { 27 | super(ws); 28 | } 29 | protected onSocketMessage(event: WS.MessageEvent): void { 30 | let data; 31 | try { 32 | data = JSON.parse(event.data.toString()); 33 | } catch (error: any) { 34 | console.log(`Received message: ${event.data}`); 35 | return; 36 | } 37 | if (!data || !data.command) { 38 | console.log(`Received message: ${event.data}`); 39 | return; 40 | } 41 | const command = data.command; 42 | switch (command) { 43 | case RemoteDevtoolsCommand.LIST_DEVTOOLS: { 44 | AdbUtils.getRemoteDevtoolsInfo(this.host, this.udid) 45 | .then((list) => { 46 | this.ws.send( 47 | JSON.stringify({ 48 | type: ACTION.DEVTOOLS, 49 | data: list, 50 | }), 51 | ); 52 | }) 53 | .catch((e) => { 54 | const { message } = e; 55 | console.error(`Command: "${command}", error: ${message}`); 56 | this.ws.send(JSON.stringify({ command, error: message })); 57 | }); 58 | break; 59 | } 60 | default: 61 | console.warn(`Unsupported command: "${data.command}"`); 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/server/goog-device/mw/WebsocketProxyOverAdb.ts: -------------------------------------------------------------------------------- 1 | import { WebsocketProxy } from '../../mw/WebsocketProxy'; 2 | import { AdbUtils } from '../AdbUtils'; 3 | import WS from 'ws'; 4 | import { RequestParameters } from '../../mw/Mw'; 5 | import { ACTION } from '../../../common/Action'; 6 | 7 | export class WebsocketProxyOverAdb extends WebsocketProxy { 8 | public static processRequest(ws: WS, params: RequestParameters): WebsocketProxy | undefined { 9 | const { action, url } = params; 10 | let udid: string | null = ''; 11 | let remote: string | null = ''; 12 | let path: string | null = ''; 13 | let isSuitable = false; 14 | if (action === ACTION.PROXY_ADB) { 15 | isSuitable = true; 16 | remote = url.searchParams.get('remote'); 17 | udid = url.searchParams.get('udid'); 18 | path = url.searchParams.get('path'); 19 | } 20 | if (url && url.pathname) { 21 | const temp = url.pathname.split('/'); 22 | // Shortcut for action=proxy, without query string 23 | if (temp.length >= 4 && temp[0] === '' && temp[1] === ACTION.PROXY_ADB) { 24 | isSuitable = true; 25 | temp.splice(0, 2); 26 | udid = decodeURIComponent(temp.shift() || ''); 27 | remote = decodeURIComponent(temp.shift() || ''); 28 | path = temp.join('/') || '/'; 29 | } 30 | } 31 | if (!isSuitable) { 32 | return; 33 | } 34 | if (typeof remote !== 'string' || !remote) { 35 | ws.close(4003, `[${this.TAG}] Invalid value "${remote}" for "remote" parameter`); 36 | return; 37 | } 38 | if (typeof udid !== 'string' || !udid) { 39 | ws.close(4003, `[${this.TAG}] Invalid value "${udid}" for "udid" parameter`); 40 | return; 41 | } 42 | if (path && typeof path !== 'string') { 43 | ws.close(4003, `[${this.TAG}] Invalid value "${path}" for "path" parameter`); 44 | return; 45 | } 46 | return this.createProxyOverAdb(ws, udid, remote, path); 47 | } 48 | 49 | public static createProxyOverAdb(ws: WS, udid: string, remote: string, path?: string | null): WebsocketProxy { 50 | const service = new WebsocketProxy(ws); 51 | AdbUtils.forward(udid, remote) 52 | .then((port) => { 53 | return service.init(`ws://127.0.0.1:${port}${path ? path : ''}`); 54 | }) 55 | .catch((e) => { 56 | const msg = `[${this.TAG}] Failed to start service: ${e.message}`; 57 | console.error(msg); 58 | ws.close(4005, msg); 59 | }); 60 | return service; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/server/mw/HostTracker.ts: -------------------------------------------------------------------------------- 1 | import WS from 'ws'; 2 | import { Mw } from './Mw'; 3 | import { Config } from '../Config'; 4 | import { MessageError, MessageHosts, MessageType } from '../../common/HostTrackerMessage'; 5 | import { HostItem } from '../../types/Configuration'; 6 | import { Multiplexer } from '../../packages/multiplexer/Multiplexer'; 7 | import { ChannelCode } from '../../common/ChannelCode'; 8 | 9 | export interface TrackerClass { 10 | type: string; 11 | } 12 | 13 | export class HostTracker extends Mw { 14 | public static readonly TAG = 'HostTracker'; 15 | private static localTrackers: Set = new Set(); 16 | private static remoteHostItems?: HostItem[]; 17 | 18 | public static processChannel(ws: Multiplexer, code: string): Mw | undefined { 19 | if (code !== ChannelCode.HSTS) { 20 | return; 21 | } 22 | return new HostTracker(ws); 23 | } 24 | 25 | public static registerLocalTracker(tracker: TrackerClass): void { 26 | this.localTrackers.add(tracker); 27 | } 28 | 29 | constructor(ws: Multiplexer) { 30 | super(ws); 31 | 32 | const local: { type: string }[] = Array.from(HostTracker.localTrackers.keys()).map((tracker) => { 33 | return { type: tracker.type }; 34 | }); 35 | if (!HostTracker.remoteHostItems) { 36 | const config = Config.getInstance(); 37 | HostTracker.remoteHostItems = Array.from(config.getHostList()); 38 | } 39 | const message: MessageHosts = { 40 | id: -1, 41 | type: MessageType.HOSTS, 42 | data: { 43 | local, 44 | remote: HostTracker.remoteHostItems, 45 | }, 46 | }; 47 | this.sendMessage(message); 48 | } 49 | 50 | protected onSocketMessage(event: WS.MessageEvent): void { 51 | const message: MessageError = { 52 | id: -1, 53 | type: MessageType.ERROR, 54 | data: `Unsupported message: "${event.data.toString()}"`, 55 | }; 56 | this.sendMessage(message); 57 | } 58 | 59 | public release(): void { 60 | super.release(); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/server/mw/MjpegProxyFactory.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from 'express'; 2 | import MjpegProxy from 'node-mjpeg-proxy'; 3 | import { WdaRunner } from '../appl-device/services/WDARunner'; 4 | import { WdaStatus } from '../../common/WdaStatus'; 5 | 6 | export class MjpegProxyFactory { 7 | private static instances: Map = new Map(); 8 | proxyRequest = async (req: Request, res: Response): Promise => { 9 | const { udid } = req.params; 10 | if (!udid) { 11 | res.destroy(); 12 | return; 13 | } 14 | let proxy = MjpegProxyFactory.instances.get(udid); 15 | if (!proxy) { 16 | const wda = await WdaRunner.getInstance(udid); 17 | if (!wda.isStarted()) { 18 | // FIXME: `wda.start()` should resolve on 'started' 19 | const startPromise = new Promise((resolve) => { 20 | const onStatusChange = ({ status }: { status: WdaStatus }) => { 21 | if (status === WdaStatus.STARTED) { 22 | wda.off('status-change', onStatusChange); 23 | resolve(undefined); 24 | } 25 | }; 26 | wda.on('status-change', onStatusChange); 27 | }); 28 | await wda.start(); 29 | await startPromise; 30 | } 31 | const port = wda.mjpegPort; 32 | const url = `http://127.0.0.1:${port}`; 33 | proxy = new MjpegProxy(url); 34 | proxy.on('streamstop', (): void => { 35 | wda.release(); 36 | MjpegProxyFactory.instances.delete(udid); 37 | }); 38 | proxy.on('error', (data: { msg: Error; url: string }): void => { 39 | console.error('msg: ' + data.msg); 40 | console.error('url: ' + data.url); 41 | }); 42 | MjpegProxyFactory.instances.set(udid, proxy); 43 | } 44 | proxy.proxyRequest(req, res); 45 | }; 46 | } 47 | -------------------------------------------------------------------------------- /src/server/mw/Mw.ts: -------------------------------------------------------------------------------- 1 | import { Message } from '../../types/Message'; 2 | import * as http from 'http'; 3 | import { Multiplexer } from '../../packages/multiplexer/Multiplexer'; 4 | import WS from 'ws'; 5 | 6 | export type RequestParameters = { 7 | request: http.IncomingMessage; 8 | url: URL; 9 | action: string; 10 | }; 11 | 12 | export interface MwFactory { 13 | processRequest(ws: WS, params: RequestParameters): Mw | undefined; 14 | processChannel(ws: Multiplexer, code: string, data?: ArrayBuffer): Mw | undefined; 15 | } 16 | 17 | export abstract class Mw { 18 | protected name = 'Mw'; 19 | 20 | public static processChannel(_ws: Multiplexer, _code: string, _data?: ArrayBuffer): Mw | undefined { 21 | return; 22 | } 23 | 24 | public static processRequest(_ws: WS, _params: RequestParameters): Mw | undefined { 25 | return; 26 | } 27 | 28 | protected constructor(protected readonly ws: WS | Multiplexer) { 29 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 30 | // @ts-ignore 31 | this.ws.addEventListener('message', this.onSocketMessage.bind(this)); 32 | this.ws.addEventListener('close', this.onSocketClose.bind(this)); 33 | } 34 | 35 | protected abstract onSocketMessage(event: WS.MessageEvent): void; 36 | 37 | protected sendMessage = (data: Message): void => { 38 | if (this.ws.readyState !== this.ws.OPEN) { 39 | return; 40 | } 41 | this.ws.send(JSON.stringify(data)); 42 | }; 43 | 44 | protected onSocketClose(): void { 45 | this.release(); 46 | } 47 | 48 | public release(): void { 49 | const { readyState, CLOSED, CLOSING } = this.ws; 50 | if (readyState !== CLOSED && readyState !== CLOSING) { 51 | this.ws.close(); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/server/mw/WebsocketMultiplexer.ts: -------------------------------------------------------------------------------- 1 | import { Mw, MwFactory, RequestParameters } from './Mw'; 2 | import { ACTION } from '../../common/Action'; 3 | import { Multiplexer } from '../../packages/multiplexer/Multiplexer'; 4 | import WS from 'ws'; 5 | import Util from '../../app/Util'; 6 | 7 | export class WebsocketMultiplexer extends Mw { 8 | public static readonly TAG = 'WebsocketMultiplexer'; 9 | private static mwFactories: Set = new Set(); 10 | private multiplexer: Multiplexer; 11 | // private mw: Set = new Set(); 12 | 13 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 14 | public static processRequest(ws: WS, params: RequestParameters): WebsocketMultiplexer | undefined { 15 | const { action } = params; 16 | if (action !== ACTION.MULTIPLEX) { 17 | return; 18 | } 19 | return this.createMultiplexer(ws); 20 | } 21 | 22 | public static createMultiplexer(ws: WS): WebsocketMultiplexer { 23 | const service = new WebsocketMultiplexer(ws); 24 | service.init().catch((e) => { 25 | const msg = `[${this.TAG}] Failed to start service: ${e.message}`; 26 | console.error(msg); 27 | ws.close(4005, msg); 28 | }); 29 | return service; 30 | } 31 | 32 | constructor(ws: WS) { 33 | super(ws); 34 | this.multiplexer = Multiplexer.wrap(ws as unknown as WebSocket); 35 | } 36 | 37 | public async init(): Promise { 38 | this.multiplexer.addEventListener('channel', this.onChannel); 39 | } 40 | 41 | public static registerMw(mwFactory: MwFactory): void { 42 | this.mwFactories.add(mwFactory); 43 | } 44 | 45 | protected onSocketMessage(_event: WS.MessageEvent): void { 46 | // none; 47 | } 48 | 49 | protected onChannel({ channel, data }: { channel: Multiplexer; data: ArrayBuffer }): void { 50 | let processed = false; 51 | for (const mwFactory of WebsocketMultiplexer.mwFactories.values()) { 52 | try { 53 | const code = Util.utf8ByteArrayToString(Buffer.from(data).slice(0, 4)); 54 | const buffer = data.byteLength > 4 ? data.slice(4) : undefined; 55 | const mw = mwFactory.processChannel(channel, code, buffer); 56 | if (mw) { 57 | processed = true; 58 | // this.mw.add(mw); 59 | // const remove = () => { 60 | // this.mw.delete(mw); 61 | // }; 62 | // channel.addEventListener('close', remove); 63 | // channel.addEventListener('error', remove); 64 | } 65 | } finally { 66 | } 67 | } 68 | if (!processed) { 69 | channel.close(4002, `[${WebsocketMultiplexer.TAG}] Unsupported request`); 70 | } 71 | } 72 | 73 | public release(): void { 74 | super.release(); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/server/mw/WebsocketProxy.ts: -------------------------------------------------------------------------------- 1 | import { Mw, RequestParameters } from './Mw'; 2 | import WS from 'ws'; 3 | import { ACTION } from '../../common/Action'; 4 | import { Multiplexer } from '../../packages/multiplexer/Multiplexer'; 5 | 6 | export class WebsocketProxy extends Mw { 7 | public static readonly TAG = 'WebsocketProxy'; 8 | private remoteSocket?: WS; 9 | private released = false; 10 | private storage: WS.MessageEvent[] = []; 11 | 12 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 13 | public static processRequest(ws: WS, params: RequestParameters): WebsocketProxy | undefined { 14 | const { action, url } = params; 15 | if (action !== ACTION.PROXY_WS) { 16 | return; 17 | } 18 | const wsString = url.searchParams.get('ws'); 19 | if (!wsString) { 20 | ws.close(4003, `[${this.TAG}] Invalid value "${ws}" for "ws" parameter`); 21 | return; 22 | } 23 | return this.createProxy(ws, wsString); 24 | } 25 | 26 | public static createProxy(ws: WS | Multiplexer, remoteUrl: string): WebsocketProxy { 27 | const service = new WebsocketProxy(ws); 28 | service.init(remoteUrl).catch((e) => { 29 | const msg = `[${this.TAG}] Failed to start service: ${e.message}`; 30 | console.error(msg); 31 | ws.close(4005, msg); 32 | }); 33 | return service; 34 | } 35 | 36 | constructor(ws: WS | Multiplexer) { 37 | super(ws); 38 | } 39 | 40 | public async init(remoteUrl: string): Promise { 41 | this.name = `[${WebsocketProxy.TAG}{$${remoteUrl}}]`; 42 | const remoteSocket = new WS(remoteUrl); 43 | remoteSocket.onopen = () => { 44 | this.remoteSocket = remoteSocket; 45 | this.flush(); 46 | }; 47 | remoteSocket.onmessage = (event) => { 48 | if (this.ws && this.ws.readyState === this.ws.OPEN) { 49 | if (Array.isArray(event.data)) { 50 | event.data.forEach((data) => this.ws.send(data)); 51 | } else { 52 | this.ws.send(event.data); 53 | } 54 | } 55 | }; 56 | remoteSocket.onclose = (e) => { 57 | if (this.ws.readyState === this.ws.OPEN) { 58 | this.ws.close(e.wasClean ? 1000 : 4010); 59 | } 60 | }; 61 | remoteSocket.onerror = (e) => { 62 | if (this.ws.readyState === this.ws.OPEN) { 63 | this.ws.close(4011, e.message); 64 | } 65 | }; 66 | } 67 | 68 | private flush(): void { 69 | if (this.remoteSocket) { 70 | while (this.storage.length) { 71 | const event = this.storage.shift(); 72 | if (event && event.data) { 73 | this.remoteSocket.send(event.data); 74 | } 75 | } 76 | if (this.released) { 77 | this.remoteSocket.close(); 78 | } 79 | } 80 | this.storage.length = 0; 81 | } 82 | 83 | protected onSocketMessage(event: WS.MessageEvent): void { 84 | if (this.remoteSocket) { 85 | this.remoteSocket.send(event.data); 86 | } else { 87 | this.storage.push(event); 88 | } 89 | } 90 | 91 | public release(): void { 92 | if (this.released) { 93 | return; 94 | } 95 | super.release(); 96 | this.released = true; 97 | this.flush(); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/server/services/BaseControlCenter.ts: -------------------------------------------------------------------------------- 1 | import { ControlCenterCommand } from '../../common/ControlCenterCommand'; 2 | import { TypedEmitter } from '../../common/TypedEmitter'; 3 | 4 | export interface ControlCenterEvents { 5 | device: T; 6 | } 7 | 8 | export abstract class BaseControlCenter extends TypedEmitter> { 9 | abstract getId(): string; 10 | abstract getName(): string; 11 | abstract getDevices(): T[]; 12 | abstract runCommand(command: ControlCenterCommand): Promise; 13 | } 14 | -------------------------------------------------------------------------------- /src/server/services/ProcessRunner.ts: -------------------------------------------------------------------------------- 1 | import { Service } from './Service'; 2 | import { TypedEmitter } from '../../common/TypedEmitter'; 3 | import { ChildProcessByStdio, spawn } from 'child_process'; 4 | import { Readable, Writable } from 'stream'; 5 | 6 | export interface ProcessRunnerEvents { 7 | spawned: boolean; 8 | started: boolean; 9 | stdout: string; 10 | stderr: string; 11 | close: { code: number; signal: string }; 12 | exit: { code: number | null; signal: string | null }; 13 | error: Error; 14 | } 15 | 16 | export abstract class ProcessRunner extends TypedEmitter implements Service { 17 | protected TAG = '[ProcessRunner]'; 18 | protected name: string; 19 | protected cmd = ''; 20 | protected spawned = false; 21 | protected proc?: ChildProcessByStdio; 22 | protected constructor() { 23 | super(); 24 | this.name = `${this.TAG}`; 25 | } 26 | 27 | protected abstract getArgs(): Promise; 28 | 29 | protected async runProcess(): Promise { 30 | if (!this.cmd) { 31 | throw new Error('Empty command'); 32 | } 33 | const args = await this.getArgs(); 34 | this.proc = spawn(this.cmd, args, { stdio: ['pipe', 'pipe', 'pipe'] }); 35 | 36 | this.proc.stdout.on('data', (data) => { 37 | this.emit('stdout', data.toString()); 38 | }); 39 | 40 | this.proc.stderr.on('data', (data) => { 41 | this.emit('stderr', data); 42 | }); 43 | 44 | this.proc.on('spawn', () => { 45 | this.spawned = true; 46 | this.emit('spawned', true); 47 | }); 48 | 49 | this.proc.on('exit', (code, signal) => { 50 | this.emit('exit', { code, signal }); 51 | }); 52 | 53 | this.proc.on('error', (error) => { 54 | console.error(this.name, `failed to spawn process.\n${error.stack}`); 55 | this.emit('error', error); 56 | }); 57 | 58 | this.proc.on('close', (code, signal) => { 59 | this.emit('close', { code, signal }); 60 | }); 61 | } 62 | 63 | public getName(): string { 64 | return this.name; 65 | } 66 | 67 | public release(): void { 68 | if (this.proc) { 69 | this.proc.kill(); 70 | this.proc = undefined; 71 | } 72 | } 73 | 74 | public start(): Promise { 75 | return this.runProcess().catch((e) => { 76 | console.error(this.name, e.message); 77 | // throw e; 78 | }); 79 | } 80 | 81 | public isStarted(): boolean { 82 | return this.spawned; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/server/services/Service.ts: -------------------------------------------------------------------------------- 1 | export interface Service { 2 | getName(): string; 3 | start(): Promise; 4 | release(): void; 5 | } 6 | 7 | export interface ServiceClass { 8 | getInstance(): Service; 9 | hasInstance(): boolean; 10 | } 11 | -------------------------------------------------------------------------------- /src/server/services/WebSocketServer.ts: -------------------------------------------------------------------------------- 1 | import { Server as WSServer } from 'ws'; 2 | import WS from 'ws'; 3 | import { Service } from './Service'; 4 | import { HttpServer, ServerAndPort } from './HttpServer'; 5 | import { MwFactory } from '../mw/Mw'; 6 | 7 | export class WebSocketServer implements Service { 8 | private static instance?: WebSocketServer; 9 | private servers: WSServer[] = []; 10 | private mwFactories: Set = new Set(); 11 | 12 | protected constructor() { 13 | // nothing here 14 | } 15 | 16 | public static getInstance(): WebSocketServer { 17 | if (!this.instance) { 18 | this.instance = new WebSocketServer(); 19 | } 20 | return this.instance; 21 | } 22 | 23 | public static hasInstance(): boolean { 24 | return !!this.instance; 25 | } 26 | 27 | public registerMw(mwFactory: MwFactory): void { 28 | this.mwFactories.add(mwFactory); 29 | } 30 | 31 | public attachToServer(item: ServerAndPort): WSServer { 32 | const { server, port } = item; 33 | const TAG = `WebSocket Server {tcp:${port}}`; 34 | const wss = new WSServer({ server }); 35 | wss.on('connection', async (ws: WS, request) => { 36 | if (!request.url) { 37 | ws.close(4001, `[${TAG}] Invalid url`); 38 | return; 39 | } 40 | const url = new URL(request.url, 'https://example.org/'); 41 | const action = url.searchParams.get('action') || ''; 42 | let processed = false; 43 | for (const mwFactory of this.mwFactories.values()) { 44 | const service = mwFactory.processRequest(ws, { action, request, url }); 45 | if (service) { 46 | processed = true; 47 | } 48 | } 49 | if (!processed) { 50 | ws.close(4002, `[${TAG}] Unsupported request`); 51 | } 52 | return; 53 | }); 54 | wss.on('close', () => { 55 | console.log(`${TAG} stopped`); 56 | }); 57 | this.servers.push(wss); 58 | return wss; 59 | } 60 | 61 | public getServers(): WSServer[] { 62 | return this.servers; 63 | } 64 | 65 | public getName(): string { 66 | return `WebSocket Server Service`; 67 | } 68 | 69 | public async start(): Promise { 70 | const service = HttpServer.getInstance(); 71 | const servers = await service.getServers(); 72 | servers.forEach((item) => { 73 | this.attachToServer(item); 74 | }); 75 | } 76 | 77 | public release(): void { 78 | this.servers.forEach((server) => { 79 | server.close(); 80 | }); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/style/devtools.css: -------------------------------------------------------------------------------- 1 | 2 | body.devtools { 3 | font-family: Ubuntu, Arial, sans-serif; 4 | font-size: 13px; 5 | } 6 | 7 | body.devtools .device { 8 | padding: 20px; 9 | } 10 | 11 | body.devtools .device-header { 12 | -webkit-box-align: baseline; 13 | -webkit-box-orient: horizontal; 14 | display: -webkit-box; 15 | margin: 10px 0 0; 16 | padding: 2px 0; 17 | } 18 | 19 | body.devtools .device-name { 20 | font-size: 150%; 21 | } 22 | 23 | body.devtools .device-serial { 24 | color: var(--url-color); 25 | font-size: 80%; 26 | margin-left: 6px; 27 | } 28 | 29 | body.devtools .browser-header { 30 | align-items: center; 31 | display: flex; 32 | flex-flow: row wrap; 33 | min-height: 33px; 34 | padding-top: 10px; 35 | } 36 | 37 | body.devtools .browser-header > .browser-name { 38 | font-size: 110%; 39 | font-weight: bold; 40 | } 41 | 42 | body.devtools div.list { 43 | margin-top: 5px; 44 | } 45 | 46 | body.devtools div.list > .row { 47 | padding: 6px 0; 48 | position: relative; 49 | } 50 | 51 | body.devtools .properties-box { 52 | display: flex; 53 | } 54 | 55 | body.devtools .properties-box > img { 56 | flex-shrink: 0; 57 | height: 23px; 58 | padding-left: 2px; 59 | padding-right: 5px; 60 | vertical-align: top; 61 | width: 23px; 62 | } 63 | 64 | body.devtools .subrow-box { 65 | display: inline-block; 66 | vertical-align: top; 67 | } 68 | 69 | body.devtools .subrow { 70 | display: flex; 71 | flex-flow: row wrap; 72 | } 73 | 74 | body.devtools .subrow > div { 75 | margin-right: 0.5em; 76 | } 77 | 78 | .body.devtools url { 79 | color: var(--url-color); 80 | max-width: 200px; 81 | text-overflow: ellipsis; 82 | white-space: nowrap; 83 | overflow: hidden; 84 | } 85 | 86 | body.devtools .action { 87 | color: var(--link-color); 88 | cursor: pointer; 89 | margin-right: 15px; 90 | } 91 | 92 | body.devtools .action.disabled { 93 | color: var(--url-color); 94 | cursor: not-allowed; 95 | } 96 | 97 | body.devtools a.action { 98 | text-decoration: none; 99 | } 100 | 101 | body.devtools a.action.copy { 102 | cursor: copy; 103 | } 104 | 105 | body.devtools .browser-header .action { 106 | margin-left: 10px; 107 | } 108 | 109 | body.devtools .open > input { 110 | border: 1px solid #aaa; 111 | height: 17px; 112 | line-height: 17px; 113 | margin-left: 20px; 114 | padding: 0 2px; 115 | } 116 | 117 | body.devtools .tooltip { 118 | z-index: 1; 119 | position: absolute; 120 | padding: 2px; 121 | color: var(--controls-bg-color); 122 | background-color: var(--text-color); 123 | } 124 | -------------------------------------------------------------------------------- /src/style/dialog.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --block-top-padding: 0.5rem; 3 | --block-bottom-padding: 0.5rem; 4 | --button-top-padding: 0.2rem; 5 | --button-bottom-padding: 0.2rem; 6 | --header-height: 3rem; 7 | --footer-height: 1.55rem; 8 | } 9 | 10 | .dialog-background { 11 | width: 100%; 12 | height: 100%; 13 | position: fixed; 14 | top: 0; 15 | left: 0; 16 | background-color: rgba(0, 0, 0, 0.75); 17 | z-index: 3; 18 | } 19 | 20 | .dialog-container { 21 | font-family: monospace; 22 | width: 75%; 23 | max-width: 30rem; 24 | min-width: 20rem; 25 | background-color: var(--main-bg-color); 26 | /*border-radius: 0.3rem;*/ 27 | overflow: hidden; 28 | } 29 | 30 | .dialog-container.ready { 31 | height: 100%; 32 | min-height: 100%; 33 | } 34 | 35 | .dialog-container button, .dialog-container select, .dialog-container input { 36 | font-family: monospace; 37 | } 38 | 39 | .dialog-container button { 40 | font-size: var(--font-size); 41 | } 42 | 43 | .dialog-container select { 44 | text-overflow: ellipsis; 45 | } 46 | 47 | .dialog-block { 48 | } 49 | 50 | .dialog-header { 51 | background-color: var(--header-bg-color); 52 | height: var(--header-height); 53 | overflow: hidden; 54 | display: flex; 55 | align-items: center; 56 | width: auto; 57 | position: initial; 58 | } 59 | 60 | .dialog-header span.dialog-title { 61 | display: inline-block; 62 | padding: 0 0.5rem; 63 | } 64 | 65 | .dialog-body { 66 | padding: var(--block-top-padding) 0.5rem var(--block-bottom-padding); 67 | background-color: var(--control-buttons-bg-color); 68 | overflow: auto; 69 | } 70 | 71 | .dialog-body.hidden { 72 | height: 0; 73 | padding: 0; 74 | } 75 | 76 | .dialog-body.visible { 77 | height: calc( 78 | 100% 79 | - 2 * var(--block-top-padding) 80 | - 2 * var(--block-bottom-padding) 81 | - var(--header-height) 82 | - var(--footer-height) 83 | ); 84 | } 85 | 86 | .dialog-footer { 87 | /*display: flex;*/ 88 | /*flex-direction: row-reverse;*/ 89 | padding: var(--block-top-padding) 0.5rem var(--block-bottom-padding); 90 | background-color: var(--stream-bg-color); 91 | height: var(--footer-height); 92 | overflow: hidden; 93 | } 94 | 95 | .dialog-footer span.subtitle { 96 | font-weight: lighter; 97 | line-height: var(--footer-height); 98 | float: left; 99 | } 100 | 101 | .dialog-footer button { 102 | padding: var(--button-top-padding) 0.5rem var(--button-bottom-padding); 103 | margin: 0 0 0 0.5rem; 104 | border-radius: 0.3rem; 105 | /*background-color: var(--main-bg-color);*/ 106 | color: var(--button-text-color); 107 | border: 1px solid var(--button-border-color); 108 | cursor: pointer; 109 | background-color: rgba(0, 0, 0, 0); 110 | height: var(--footer-height); 111 | float: right; 112 | } 113 | 114 | .dialog-footer button:disabled { 115 | cursor: not-allowed; 116 | color: var(--text-color-light); 117 | } 118 | 119 | .controls .label { 120 | grid-column: labels; 121 | } 122 | 123 | .controls .input { 124 | grid-column: controls; 125 | box-sizing: border-box; 126 | margin: 0; 127 | /*height: 2.75ex;*/ 128 | } 129 | 130 | .controls .button { 131 | grid-column: controls; 132 | } 133 | 134 | .controls { 135 | display: grid; 136 | grid-template-columns: [labels] 35% [controls] 65%; 137 | padding: 1rem; 138 | grid-gap: 0.2rem; 139 | align-items: center; 140 | } 141 | -------------------------------------------------------------------------------- /src/style/morebox.css: -------------------------------------------------------------------------------- 1 | .text-area { 2 | width: 100%; 3 | resize: vertical; 4 | } 5 | 6 | .more-box { 7 | display: none; 8 | position: absolute; 9 | background-color: var(--controls-bg-color); 10 | z-index: 2; 11 | padding: 0 .714rem .714rem .714rem; 12 | } 13 | 14 | .text-with-shadow, .more-box label { 15 | color: var(--text-color); 16 | text-shadow: var(--text-shadow-color) 0 0 .357rem; 17 | } 18 | 19 | .spoiler > input ~ .box { 20 | display: none; 21 | } 22 | 23 | .spoiler > input:checked ~ .box { 24 | display: block; 25 | } 26 | 27 | .spoiler > label::before { 28 | content: '►'; 29 | margin-right: 5px; 30 | } 31 | 32 | .spoiler > input:checked ~ label::before { 33 | content: '▼'; 34 | } 35 | 36 | .spoiler > input:checked ~ div { 37 | display: block; 38 | padding: 10px; 39 | } 40 | 41 | .spoiler > input { 42 | display: none; 43 | } 44 | -------------------------------------------------------------------------------- /src/types/ApplDeviceDescriptor.d.ts: -------------------------------------------------------------------------------- 1 | import { BaseDeviceDescriptor } from './BaseDeviceDescriptor'; 2 | 3 | export default interface ApplDeviceDescriptor extends BaseDeviceDescriptor { 4 | name: string; 5 | model: string; 6 | version: string; 7 | 'last.update.timestamp': number; 8 | } 9 | -------------------------------------------------------------------------------- /src/types/BaseDeviceDescriptor.d.ts: -------------------------------------------------------------------------------- 1 | export interface BaseDeviceDescriptor { 2 | udid: string; 3 | state: string; 4 | } 5 | -------------------------------------------------------------------------------- /src/types/Configuration.d.ts: -------------------------------------------------------------------------------- 1 | import * as https from 'https'; 2 | 3 | export type OperatingSystem = 'android' | 'ios'; 4 | 5 | export interface HostItem { 6 | type: OperatingSystem; 7 | secure: boolean; 8 | hostname: string; 9 | port: number; 10 | pathname?: string; 11 | useProxy?: boolean; 12 | } 13 | 14 | export interface HostsItem { 15 | type: OperatingSystem | OperatingSystem[]; 16 | secure: boolean; 17 | hostname: string; 18 | port: number; 19 | pathname?: string; 20 | useProxy?: boolean; 21 | } 22 | 23 | export type ExtendedServerOption = https.ServerOptions & { 24 | certPath?: string; 25 | keyPath?: string; 26 | }; 27 | 28 | export interface ServerItem { 29 | secure: boolean; 30 | port: number; 31 | options?: ExtendedServerOption; 32 | redirectToSecure?: 33 | | { 34 | port?: number; 35 | host?: string; 36 | } 37 | | boolean; 38 | } 39 | 40 | // The configuration file must contain a single object with this structure 41 | export interface Configuration { 42 | server?: ServerItem[]; 43 | runApplTracker?: boolean; 44 | announceApplTracker?: boolean; 45 | runGoogTracker?: boolean; 46 | announceGoogTracker?: boolean; 47 | remoteHostList?: HostsItem[]; 48 | } 49 | -------------------------------------------------------------------------------- /src/types/DeviceTrackerEvent.ts: -------------------------------------------------------------------------------- 1 | export type DeviceTrackerEvent = { 2 | name: string; 3 | id: string; 4 | device: T; 5 | }; 6 | -------------------------------------------------------------------------------- /src/types/DeviceTrackerEventList.ts: -------------------------------------------------------------------------------- 1 | export type DeviceTrackerEventList = { 2 | name: string; 3 | id: string; 4 | list: T[]; 5 | }; 6 | -------------------------------------------------------------------------------- /src/types/FileStats.ts: -------------------------------------------------------------------------------- 1 | export interface FileStats { 2 | name: string; 3 | isDir: number; 4 | size: number; 5 | dateModified: number; 6 | } 7 | -------------------------------------------------------------------------------- /src/types/GoogDeviceDescriptor.d.ts: -------------------------------------------------------------------------------- 1 | import { NetInterface } from './NetInterface'; 2 | import { BaseDeviceDescriptor } from './BaseDeviceDescriptor'; 3 | 4 | export default interface GoogDeviceDescriptor extends BaseDeviceDescriptor { 5 | 'ro.build.version.release': string; 6 | 'ro.build.version.sdk': string; 7 | 'ro.product.cpu.abi': string; 8 | 'ro.product.manufacturer': string; 9 | 'ro.product.model': string; 10 | 'wifi.interface': string; 11 | interfaces: NetInterface[]; 12 | pid: number; 13 | 'last.update.timestamp': number; 14 | } 15 | -------------------------------------------------------------------------------- /src/types/Message.d.ts: -------------------------------------------------------------------------------- 1 | export interface Message { 2 | id: number; 3 | type: string; 4 | data: any; 5 | } 6 | -------------------------------------------------------------------------------- /src/types/MessageFileListing.d.ts: -------------------------------------------------------------------------------- 1 | import { Message } from './Message'; 2 | 3 | export interface MessageFileListing extends Message { 4 | entry: string; 5 | } 6 | -------------------------------------------------------------------------------- /src/types/MessageRunWdaResponse.ts: -------------------------------------------------------------------------------- 1 | import { Message } from './Message'; 2 | import { WdaStatus } from '../common/WdaStatus'; 3 | 4 | export interface MessageRunWdaResponse extends Message { 5 | type: 'run-wda'; 6 | data: { 7 | udid: string; 8 | status: WdaStatus; 9 | code?: number; 10 | text?: string; 11 | }; 12 | } 13 | -------------------------------------------------------------------------------- /src/types/MessageXtermClient.ts: -------------------------------------------------------------------------------- 1 | import { Message } from './Message'; 2 | import { XtermClientMessage } from './XtermMessage'; 3 | 4 | export interface MessageXtermClient extends Message { 5 | type: 'shell'; 6 | data: XtermClientMessage; 7 | } 8 | -------------------------------------------------------------------------------- /src/types/NetInterface.d.ts: -------------------------------------------------------------------------------- 1 | export interface NetInterface { 2 | name: string; 3 | ipv4: string; 4 | } 5 | -------------------------------------------------------------------------------- /src/types/ParamsBase.ts: -------------------------------------------------------------------------------- 1 | export interface ParamsBase { 2 | action: string; 3 | useProxy?: boolean; 4 | secure?: boolean; 5 | hostname?: string; 6 | port?: number; 7 | pathname?: string; 8 | } 9 | -------------------------------------------------------------------------------- /src/types/ParamsDeviceTracker.ts: -------------------------------------------------------------------------------- 1 | import { ParamsBase } from './ParamsBase'; 2 | 3 | export interface ParamsDeviceTracker extends ParamsBase { 4 | type: 'android' | 'ios'; 5 | } 6 | -------------------------------------------------------------------------------- /src/types/ParamsDevtools.d.ts: -------------------------------------------------------------------------------- 1 | import { ACTION } from '../common/Action'; 2 | import { ParamsBase } from './ParamsBase'; 3 | 4 | export interface ParamsDevtools extends ParamsBase { 5 | action: ACTION.DEVTOOLS; 6 | udid: string; 7 | } 8 | -------------------------------------------------------------------------------- /src/types/ParamsFileListing.d.ts: -------------------------------------------------------------------------------- 1 | import { ACTION } from '../common/Action'; 2 | import { ParamsBase } from './ParamsBase'; 3 | 4 | export interface ParamsFileListing extends ParamsBase { 5 | action: ACTION.FILE_LISTING; 6 | udid: string; 7 | path: string; 8 | } 9 | -------------------------------------------------------------------------------- /src/types/ParamsShell.d.ts: -------------------------------------------------------------------------------- 1 | import { ParamsBase } from './ParamsBase'; 2 | import { ACTION } from '../common/Action'; 3 | 4 | export interface ParamsShell extends ParamsBase { 5 | action: ACTION.SHELL; 6 | udid: string; 7 | } 8 | -------------------------------------------------------------------------------- /src/types/ParamsStream.ts: -------------------------------------------------------------------------------- 1 | import { ParamsBase } from './ParamsBase'; 2 | 3 | export interface ParamsStream extends ParamsBase { 4 | udid: string; 5 | player: string; 6 | } 7 | -------------------------------------------------------------------------------- /src/types/ParamsStreamScrcpy.d.ts: -------------------------------------------------------------------------------- 1 | import { ACTION } from '../common/Action'; 2 | import { ParamsStream } from './ParamsStream'; 3 | import VideoSettings from '../app/VideoSettings'; 4 | 5 | export interface ParamsStreamScrcpy extends ParamsStream { 6 | action: ACTION.STREAM_SCRCPY; 7 | ws: string; 8 | fitToScreen?: boolean; 9 | videoSettings?: VideoSettings; 10 | } 11 | -------------------------------------------------------------------------------- /src/types/ParamsWdaProxy.d.ts: -------------------------------------------------------------------------------- 1 | import { ParamsBase } from './ParamsBase'; 2 | import { ACTION } from '../common/Action'; 3 | 4 | export interface ParamsWdaProxy extends ParamsBase { 5 | action: ACTION.PROXY_WDA; 6 | udid: string; 7 | } 8 | -------------------------------------------------------------------------------- /src/types/RemoteDevtools.d.ts: -------------------------------------------------------------------------------- 1 | export interface VersionMetadata { 2 | 'Android-Package': string; 3 | Browser: string; 4 | 'Protocol-Version': string; 5 | 'User-Agent': string; 6 | 'V8-Version'?: string; 7 | 'WebKit-Version': string; 8 | webSocketDebuggerUrl?: string; 9 | } 10 | 11 | export interface TargetDescription { 12 | attached: boolean; 13 | empty: boolean; 14 | height: number; 15 | screenX: number; 16 | screenY: number; 17 | visible: boolean; 18 | width: number; 19 | } 20 | 21 | export interface RemoteTarget { 22 | description: string; // JSON.stringify(TargetDescription) 23 | devtoolsFrontendUrl: string; 24 | faviconUrl: string; 25 | id: string; 26 | title: string; 27 | type: string; 28 | url: string; 29 | webSocketDebuggerUrl: string; 30 | } 31 | 32 | export type RemoteBrowserInfo = { 33 | socket: string; 34 | version: VersionMetadata; 35 | targets: RemoteTarget[]; 36 | }; 37 | 38 | export type DevtoolsInfo = { 39 | deviceName: string; 40 | deviceSerial: string; 41 | browsers: RemoteBrowserInfo[]; 42 | }; 43 | -------------------------------------------------------------------------------- /src/types/RemoteDevtoolsCommand.ts: -------------------------------------------------------------------------------- 1 | export enum RemoteDevtoolsCommand { 2 | LIST_DEVTOOLS = 'list_devtools', 3 | } 4 | -------------------------------------------------------------------------------- /src/types/ReplyFileListing.d.ts: -------------------------------------------------------------------------------- 1 | import { Message } from './Message'; 2 | import { FileStats } from './FileStats'; 3 | 4 | export interface ReplyFileListing extends Message { 5 | success: boolean; 6 | error?: string; 7 | list?: FileStats[]; 8 | } 9 | -------------------------------------------------------------------------------- /src/types/WdaServer.d.ts: -------------------------------------------------------------------------------- 1 | // This file is used instead of 'appium-xcuitest-driver/lib/server' 2 | 3 | import * as http from 'http'; 4 | import { XCUITestDriver } from 'appium-xcuitest-driver'; 5 | 6 | declare class Server extends http.Server { 7 | driver: XCUITestDriver; 8 | } 9 | 10 | export { Server, XCUITestDriver }; 11 | -------------------------------------------------------------------------------- /src/types/XtermMessage.d.ts: -------------------------------------------------------------------------------- 1 | export enum XtermServiceActions { 2 | start, 3 | stop, 4 | } 5 | 6 | export interface XtermServiceParameters { 7 | cols?: number; 8 | rows?: number; 9 | cwd?: string; 10 | env?: { [key: string]: string }; 11 | udid: string; 12 | } 13 | 14 | export interface XtermClientMessage extends XtermServiceParameters { 15 | type: keyof typeof XtermServiceActions; 16 | pid?: number; 17 | } 18 | -------------------------------------------------------------------------------- /typings/appium-base-driver/index.d.ts: -------------------------------------------------------------------------------- 1 | import { DeviceSettings } from './lib/basedriver/device-settings'; 2 | 3 | export class server {} 4 | 5 | export class BaseDriver {} 6 | 7 | export { DeviceSettings }; 8 | -------------------------------------------------------------------------------- /typings/appium-base-driver/lib/basedriver/device-settings.d.ts: -------------------------------------------------------------------------------- 1 | export class DeviceSettings { 2 | constructor( 3 | defaultSettings: Record, 4 | onSettingsUpdate?: (name: string, newValue: any, oldValue: any) => Promise, 5 | ); 6 | public update(newSettings: Record): Promise; 7 | public getSettings(): Record; 8 | } 9 | -------------------------------------------------------------------------------- /typings/appium-support/build/lib/timing.d.ts: -------------------------------------------------------------------------------- 1 | export class Duration {} 2 | 3 | export class Timer { 4 | get startTime(): number; 5 | public start(): Timer; 6 | getDuration(): Duration; 7 | } 8 | 9 | export default Timer; 10 | -------------------------------------------------------------------------------- /typings/appium-support/index.d.ts: -------------------------------------------------------------------------------- 1 | import * as timing from './build/lib/timing'; 2 | 3 | export { timing }; 4 | 5 | // export default { Timer }; 6 | -------------------------------------------------------------------------------- /typings/appium-xcuitest-driver/build/lib/device-connections-factory.d.ts: -------------------------------------------------------------------------------- 1 | declare class DeviceConnectionsFactory { 2 | listConnections (udid?: string | null, port?: string | null, strict?: boolean): string[]; 3 | requestConnection( 4 | udid: string, 5 | port: number, 6 | options: { usePortForwarding?: boolean; devicePort?: number }, 7 | ): Promise; 8 | releaseConnection(udid: string | null, port: number | null): void; 9 | } 10 | declare const DEVICE_CONNECTIONS_FACTORY: DeviceConnectionsFactory; 11 | 12 | export { DEVICE_CONNECTIONS_FACTORY, DeviceConnectionsFactory }; 13 | export default DEVICE_CONNECTIONS_FACTORY; 14 | -------------------------------------------------------------------------------- /typings/appium-xcuitest-driver/build/lib/driver.d.ts: -------------------------------------------------------------------------------- 1 | import { BaseDriver } from 'appium-base-driver'; 2 | 3 | declare interface ScreenInfo { 4 | statusBarSize: { width: number; height: number }; 5 | scale: number; 6 | } 7 | 8 | declare interface Gesture { 9 | action: string; 10 | options: { 11 | x?: number; 12 | y?: number; 13 | ms?: number; 14 | }; 15 | } 16 | 17 | declare class XCUITestDriver extends BaseDriver { 18 | constructor(opts: Record, shouldValidateCaps: boolean); 19 | public createSession(...args: any): Promise; 20 | public findElement(strategy: string, selector: string): Promise; 21 | public getSize(element: any): Promise<{ width: number; height: number } | undefined>; 22 | public getScreenInfo(): Promise; 23 | public performTouch(gestures: Gesture[]): Promise; 24 | public mobilePressButton(args: { name: string }): Promise; 25 | public stop(): Promise; 26 | public deleteSession(): Promise; 27 | public updateSettings(opts: any): Promise; 28 | public keys(value: string): Promise; 29 | public wda: any; 30 | } 31 | 32 | export default XCUITestDriver; 33 | export { XCUITestDriver }; 34 | -------------------------------------------------------------------------------- /typings/appium-xcuitest-driver/build/lib/server.d.ts: -------------------------------------------------------------------------------- 1 | import { Server as HttpServer } from 'http'; 2 | 3 | import { XCUITestDriver } from './driver'; 4 | 5 | export class Server extends HttpServer { 6 | public driver: XCUITestDriver; 7 | } 8 | 9 | export function startServer(port: string | number, address?: string): Server; 10 | -------------------------------------------------------------------------------- /typings/appium-xcuitest-driver/index.d.ts: -------------------------------------------------------------------------------- 1 | import { XCUITestDriver } from './build/lib/driver'; 2 | import { startServer } from './build/lib/server'; 3 | 4 | export { XCUITestDriver, startServer }; 5 | export default XCUITestDriver; 6 | -------------------------------------------------------------------------------- /typings/build-config.d.ts: -------------------------------------------------------------------------------- 1 | declare var __PATHNAME__: string; 2 | -------------------------------------------------------------------------------- /typings/custom_png.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.png' { 2 | const content: any; 3 | export default content; 4 | } 5 | -------------------------------------------------------------------------------- /typings/custom_svg.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.svg' { 2 | const content: any; 3 | export default content; 4 | } 5 | -------------------------------------------------------------------------------- /typings/node-mjpeg-proxy/index.d.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from 'express'; 2 | import { EventEmitter } from 'events'; 3 | 4 | declare class MjpegProxy extends EventEmitter { 5 | constructor(mjpegUrl: string); 6 | proxyRequest(req: Request, res: Response): void; 7 | } 8 | 9 | 10 | export = MjpegProxy; 11 | -------------------------------------------------------------------------------- /typings/tinyh264.d.ts: -------------------------------------------------------------------------------- 1 | export const init: () => void; 2 | -------------------------------------------------------------------------------- /typings/worker-loader.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'worker-loader!*' { 2 | // You need to change `Worker`, if you specified a different value for the `workerType` option 3 | class WebpackWorker extends Worker { 4 | constructor(); 5 | } 6 | 7 | // Uncomment this if you set the `esModule` option to `false` 8 | // export = WebpackWorker; 9 | export default WebpackWorker; 10 | } 11 | -------------------------------------------------------------------------------- /vendor/Broadway/AUTHORS: -------------------------------------------------------------------------------- 1 | The following authors have all licensed their contributions to the project 2 | under the licensing terms detailed in LICENSE. 3 | 4 | Michael Bebenita 5 | Alon Zakai 6 | Andreas Gal 7 | Mathieu 'p01' Henri 8 | Matthias 'soliton4' Behrens 9 | Sam Leitch @oneam - provided the webgl canvas 10 | -------------------------------------------------------------------------------- /vendor/Broadway/Decoder.d.ts: -------------------------------------------------------------------------------- 1 | declare class Avc { 2 | public onPictureDecoded: (buffer: Uint8Array, width: number, height: number) => void; 3 | public decode(data: Uint8Array): void; 4 | } 5 | 6 | export = Avc; 7 | -------------------------------------------------------------------------------- /vendor/Broadway/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011, Project Authors (see AUTHORS file) 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | 6 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | * 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. 8 | * 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. 9 | 10 | 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. 11 | -------------------------------------------------------------------------------- /vendor/Broadway/avc.wasm.asset: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NetrisTV/ws-scrcpy/9136dd930eaad27a874c0807476e338b3a1ed5f9/vendor/Broadway/avc.wasm.asset -------------------------------------------------------------------------------- /vendor/Genymobile/scrcpy/scrcpy-server.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NetrisTV/ws-scrcpy/9136dd930eaad27a874c0807476e338b3a1ed5f9/vendor/Genymobile/scrcpy/scrcpy-server.jar -------------------------------------------------------------------------------- /vendor/h264-live-player/AUTHORS: -------------------------------------------------------------------------------- 1 | The following authors have all licensed their contributions to the project 2 | under the licensing terms detailed in LICENSE (MIT style) 3 | 4 | # h264-live-player migration to TypeScript 5 | * Sergey Volkov 6 | 7 | # h264-live-player 8 | * Francois Leurent @131 <131.js@cloudyks.org> 9 | 10 | # Broadway emscriptend h264 (broadway/Decoder.js) 11 | * Michael Bebenita 12 | * Alon Zakai 13 | * Andreas Gal 14 | * Mathieu 'p01' Henri 15 | * Matthias 'soliton4' Behrens 16 | 17 | # WebGL canvas helpers 18 | * Sam Leitch @oneam 19 | 20 | # AVC player inspiration 21 | * Benjamin Xiao @urbenlegend 22 | 23 | -------------------------------------------------------------------------------- /vendor/h264-live-player/Canvas.ts: -------------------------------------------------------------------------------- 1 | import Size from './utils/Size'; 2 | 3 | export default abstract class Canvas { 4 | protected constructor(readonly canvas: HTMLCanvasElement, readonly size: Size) {} 5 | public abstract decode(buffer: Uint8Array, width: number, height: number): void; 6 | } 7 | -------------------------------------------------------------------------------- /vendor/h264-live-player/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019, Project Authors (see AUTHORS file) 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | 6 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | * 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. 8 | * 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. 9 | 10 | 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. 11 | -------------------------------------------------------------------------------- /vendor/h264-live-player/Program.ts: -------------------------------------------------------------------------------- 1 | import Shader from './Shader'; 2 | import assert from './utils/assert'; 3 | 4 | export default class Program { 5 | public readonly program: WebGLProgram | null; 6 | 7 | constructor(readonly gl: WebGLRenderingContext) { 8 | this.program = this.gl.createProgram(); 9 | } 10 | 11 | public attach(shader: Shader): void { 12 | if (!this.program) { 13 | throw Error(`Program type is ${typeof this.program}`); 14 | } 15 | if (!shader.shader) { 16 | throw Error(`Shader type is ${typeof shader.shader}`); 17 | } 18 | this.gl.attachShader(this.program, shader.shader); 19 | } 20 | 21 | public link(): void { 22 | if (!this.program) { 23 | throw Error(`Program type is ${typeof this.program}`); 24 | } 25 | this.gl.linkProgram(this.program); 26 | // If creating the shader program failed, alert. 27 | assert(this.gl.getProgramParameter(this.program, this.gl.LINK_STATUS), 28 | 'Unable to initialize the shader program.'); 29 | } 30 | 31 | public use(): void { 32 | this.gl.useProgram(this.program); 33 | } 34 | 35 | public getAttributeLocation(name: string): number { 36 | if (!this.program) { 37 | throw Error(`Program type is ${typeof this.program}`); 38 | } 39 | return this.gl.getAttribLocation(this.program, name); 40 | } 41 | 42 | public setMatrixUniform(name: string, array: Float32List): void { 43 | if (!this.program) { 44 | throw Error(`Program type is ${typeof this.program}`); 45 | } 46 | const uniform = this.gl.getUniformLocation(this.program, name); 47 | this.gl.uniformMatrix4fv(uniform, false, array); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /vendor/h264-live-player/README.md: -------------------------------------------------------------------------------- 1 | Based on client code from [131/h264-live-player](https://github.com/131/h264-live-player/tree/6966af8/vendor) 2 | 3 | See [License](LICENSE) 4 | -------------------------------------------------------------------------------- /vendor/h264-live-player/Script.ts: -------------------------------------------------------------------------------- 1 | export default class Script { 2 | constructor(public type: string, public source: string) { 3 | } 4 | 5 | public static createFromElementId(id: string): Script { 6 | const script = document.getElementById(id); 7 | 8 | // Didn't find an element with the specified ID, abort. 9 | if (!script) { 10 | throw Error('Could not find shader with ID: ' + id); 11 | } 12 | 13 | // Walk through the source element's children, building the shader source string. 14 | let source = ''; 15 | let currentChild = script.firstChild; 16 | while (currentChild) { 17 | if (currentChild.nodeType === 3) { 18 | source += currentChild.textContent; 19 | } 20 | currentChild = currentChild.nextSibling; 21 | } 22 | 23 | return new Script((script as HTMLScriptElement).type, source); 24 | } 25 | 26 | public static createFromSource(type: string, source: string): Script { 27 | return new Script(type, source); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /vendor/h264-live-player/Shader.ts: -------------------------------------------------------------------------------- 1 | import Script from './Script'; 2 | import error from './utils/error'; 3 | 4 | export default class Shader { 5 | public readonly shader?: WebGLShader | null; 6 | 7 | constructor(readonly gl: WebGLRenderingContext, readonly script: Script) { 8 | // Now figure out what type of shader script we have, based on its MIME type. 9 | if (script.type === 'x-shader/x-fragment') { 10 | this.shader = gl.createShader(gl.FRAGMENT_SHADER); 11 | } else if (script.type === 'x-shader/x-vertex') { 12 | this.shader = gl.createShader(gl.VERTEX_SHADER); 13 | } else { 14 | error(`Unknown shader type: ${script.type}`); 15 | return; 16 | } 17 | 18 | if (!this.shader) { 19 | error(`Shader is ${typeof this.shader}`); 20 | return; 21 | } 22 | 23 | // Send the source to the shader object. 24 | gl.shaderSource(this.shader, script.source); 25 | 26 | // Compile the shader program. 27 | gl.compileShader(this.shader); 28 | 29 | // See if it compiled successfully. 30 | if (!gl.getShaderParameter(this.shader, gl.COMPILE_STATUS)) { 31 | error('An error occurred compiling the shaders: ' + gl.getShaderInfoLog(this.shader)); 32 | return; 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /vendor/h264-live-player/Texture.ts: -------------------------------------------------------------------------------- 1 | import Size from './utils/Size'; 2 | import assert from './utils/assert'; 3 | import Program from './Program'; 4 | 5 | export default class Texture { 6 | public readonly texture: WebGLTexture | null; 7 | public readonly format: GLenum; 8 | private textureIDs: number[]; 9 | 10 | static create (gl: WebGLRenderingContext, format: number): Texture { 11 | return new Texture(gl, undefined, format); 12 | } 13 | 14 | constructor(readonly gl: WebGLRenderingContext, readonly size?: Size, format?: GLenum) { 15 | this.texture = gl.createTexture(); 16 | gl.bindTexture(gl.TEXTURE_2D, this.texture); 17 | this.format = format ? format : gl.LUMINANCE; 18 | if (size) { 19 | gl.texImage2D(gl.TEXTURE_2D, 0, this.format, size.w, size.h, 0, this.format, gl.UNSIGNED_BYTE, null); 20 | } 21 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); 22 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); 23 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); 24 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); 25 | this.textureIDs = [gl.TEXTURE0, gl.TEXTURE1, gl.TEXTURE2]; 26 | } 27 | 28 | public fill(textureData: Uint8Array, useTexSubImage2D?: boolean, w?: number, h?: number): void { 29 | if (typeof w === 'undefined' || typeof h === 'undefined') { 30 | if (!this.size) { 31 | return; 32 | } 33 | w = this.size.w; 34 | h = this.size.h; 35 | } 36 | const gl = this.gl; 37 | assert(textureData.length >= w * h, 38 | 'Texture size mismatch, data:' + textureData.length + ', texture: ' + w * h); 39 | gl.bindTexture(gl.TEXTURE_2D, this.texture); 40 | if (useTexSubImage2D) { 41 | gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, w, h, this.format, gl.UNSIGNED_BYTE, textureData); 42 | } else { 43 | // texImage2D seems to be faster, thus keeping it as the default 44 | gl.texImage2D(gl.TEXTURE_2D, 0, this.format, w, h, 0, this.format, gl.UNSIGNED_BYTE, textureData); 45 | } 46 | } 47 | 48 | public image2dBuffer (buffer: Uint8Array, width: number, height: number) { 49 | this.fill(buffer, false, width, height); 50 | } 51 | 52 | public bind(n: number, program: Program, name: string): void { 53 | const gl = this.gl; 54 | if (!program.program) { 55 | return; 56 | } 57 | gl.activeTexture(this.textureIDs[n]); 58 | gl.bindTexture(gl.TEXTURE_2D, this.texture); 59 | gl.uniform1i(gl.getUniformLocation(program.program, name), n); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /vendor/h264-live-player/YUVCanvas.ts: -------------------------------------------------------------------------------- 1 | import Size from './utils/Size'; 2 | import Canvas from './Canvas'; 3 | 4 | export default class YUVCanvas extends Canvas { 5 | private canvasCtx: CanvasRenderingContext2D; 6 | private canvasBuffer: ImageData; 7 | 8 | constructor(readonly canvas: HTMLCanvasElement, readonly size: Size) { 9 | super(canvas, size); 10 | this.canvasCtx = this.canvas.getContext('2d') as CanvasRenderingContext2D; 11 | this.canvasBuffer = this.canvasCtx.createImageData(size.w, size.h); 12 | } 13 | 14 | public decode(buffer: Uint8Array, width: number, height: number): void { 15 | if (!buffer) { 16 | return; 17 | } 18 | 19 | const lumaSize = width * height; 20 | const chromaSize = lumaSize >> 2; 21 | 22 | const ybuf = buffer.subarray(0, lumaSize); 23 | const ubuf = buffer.subarray(lumaSize, lumaSize + chromaSize); 24 | const vbuf = buffer.subarray(lumaSize + chromaSize, lumaSize + 2 * chromaSize); 25 | 26 | for (let y = 0; y < height; y++) { 27 | for (let x = 0; x < width; x++) { 28 | const yIndex = x + y * width; 29 | const uIndex = ~~(y / 2) * ~~(width / 2) + ~~(x / 2); 30 | const vIndex = ~~(y / 2) * ~~(width / 2) + ~~(x / 2); 31 | const R = 1.164 * (ybuf[yIndex] - 16) + 1.596 * (vbuf[vIndex] - 128); 32 | const G = 1.164 * (ybuf[yIndex] - 16) - 0.813 * (vbuf[vIndex] - 128) - 0.391 * (ubuf[uIndex] - 128); 33 | const B = 1.164 * (ybuf[yIndex] - 16) + 2.018 * (ubuf[uIndex] - 128); 34 | 35 | const rgbIndex = yIndex * 4; 36 | this.canvasBuffer.data[rgbIndex + 0] = R; 37 | this.canvasBuffer.data[rgbIndex + 1] = G; 38 | this.canvasBuffer.data[rgbIndex + 2] = B; 39 | this.canvasBuffer.data[rgbIndex + 3] = 0xff; 40 | } 41 | } 42 | 43 | this.canvasCtx.putImageData(this.canvasBuffer, 0, 0); 44 | 45 | // const date = new Date(); 46 | // console.log('WSAvcPlayer: Decode time: ' + (date.getTime() - this.rcvtime) + ' ms'); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /vendor/h264-live-player/utils/Size.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Represents a 2-dimensional size value. 3 | */ 4 | 5 | export default class Size { 6 | constructor(public w: number, public h: number) {} 7 | toString() { 8 | return '(' + this.w + ', ' + this.h + ')'; 9 | } 10 | getHalfSize() { 11 | return new Size(this.w >>> 1, this.h >>> 1); 12 | } 13 | length() { 14 | return this.w * this.h; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /vendor/h264-live-player/utils/assert.ts: -------------------------------------------------------------------------------- 1 | import error from './error'; 2 | 3 | export default function assert(condition: boolean, message: string): void { 4 | if (!condition) { 5 | error(message); 6 | throw new Error(message); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /vendor/h264-live-player/utils/error.ts: -------------------------------------------------------------------------------- 1 | export default function error(message: string): void { 2 | console.error(message); 3 | console.trace(); 4 | } 5 | -------------------------------------------------------------------------------- /vendor/h264-live-player/utils/glUtils.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | import { Matrix, Vector } from 'sylvester.js'; 3 | 4 | const $M = Matrix.create; 5 | 6 | // augment Sylvester some 7 | Matrix.Translation = function(v: Matrix): Matrix { 8 | if (v.elements.length === 2) { 9 | const r = Matrix.I(3); 10 | r.elements[2][0] = v.elements[0]; 11 | r.elements[2][1] = v.elements[1]; 12 | return r; 13 | } 14 | 15 | if (v.elements.length === 3) { 16 | const r = Matrix.I(4); 17 | r.elements[0][3] = v.elements[0]; 18 | r.elements[1][3] = v.elements[1]; 19 | r.elements[2][3] = v.elements[2]; 20 | return r; 21 | } 22 | 23 | throw Error('Invalid length for Translation'); 24 | }; 25 | 26 | Matrix.prototype.flatten = function(): number[] { 27 | const result = []; 28 | /* tslint:disable: no-invalid-this prefer-for-of */ 29 | if (this.elements.length === 0) { 30 | return []; 31 | } 32 | 33 | for (let j = 0; j < this.elements[0].length; j++) { 34 | for (let i = 0; i < this.elements.length; i++) { 35 | result.push(this.elements[i][j]); 36 | } 37 | } 38 | /* tslint:enable */ 39 | return result; 40 | }; 41 | 42 | Matrix.prototype.ensure4x4 = function(): Matrix | null { 43 | /* tslint:disable: no-invalid-this */ 44 | if (this.elements.length === 4 && this.elements[0].length === 4) { 45 | return this; 46 | } 47 | 48 | if (this.elements.length > 4 || this.elements[0].length > 4) { 49 | return null; 50 | } 51 | 52 | for (let i = 0; i < this.elements.length; i++) { 53 | for (let j = this.elements[i].length; j < 4; j++) { 54 | if (i === j) { 55 | this.elements[i].push(1); 56 | } else { 57 | this.elements[i].push(0); 58 | } 59 | } 60 | } 61 | 62 | for (let i = this.elements.length; i < 4; i++) { 63 | if (i === 0) { 64 | this.elements.push([1, 0, 0, 0]); 65 | } else if (i === 1) { 66 | this.elements.push([0, 1, 0, 0]); 67 | } else if (i === 2) { 68 | this.elements.push([0, 0, 1, 0]); 69 | } else if (i === 3) { 70 | this.elements.push([0, 0, 0, 1]); 71 | } 72 | } 73 | 74 | return this; 75 | /* tslint:enable */ 76 | }; 77 | 78 | Vector.prototype.flatten = function(): number[] { 79 | /* tslint:disable: no-invalid-this */ 80 | return this.elements; 81 | /* tslint:enable */ 82 | }; 83 | 84 | // 85 | // gluPerspective 86 | // 87 | export function makePerspective(fovy: number, aspect: number, znear: number, zfar: number): Matrix { 88 | const ymax = znear * Math.tan(fovy * Math.PI / 360.0); 89 | const ymin = -ymax; 90 | const xmin = ymin * aspect; 91 | const xmax = ymax * aspect; 92 | 93 | return makeFrustum(xmin, xmax, ymin, ymax, znear, zfar); 94 | } 95 | 96 | // 97 | // glFrustum 98 | // 99 | function makeFrustum(left: number, right: number, 100 | bottom: number, top: number, 101 | znear: number, zfar: number): Matrix { 102 | const X = 2 * znear / (right - left); 103 | const Y = 2 * znear / (top - bottom); 104 | const A = (right + left) / (right - left); 105 | const B = (top + bottom) / (top - bottom); 106 | const C = -(zfar + znear) / (zfar - znear); 107 | const D = -2 * zfar * znear / (zfar - znear); 108 | 109 | return $M([[X, 0, A, 0], 110 | [0, Y, B, 0], 111 | [0, 0, C, D], 112 | [0, 0, -1, 0]]); 113 | } 114 | -------------------------------------------------------------------------------- /vendor/tinyh264/Canvas.ts: -------------------------------------------------------------------------------- 1 | export default abstract class Canvas { 2 | constructor(protected readonly canvas: HTMLCanvasElement) {} 3 | public abstract decode(buffer: Uint8Array, width: number, height: number): void; 4 | } 5 | -------------------------------------------------------------------------------- /vendor/tinyh264/H264NALDecoder.worker.ts: -------------------------------------------------------------------------------- 1 | import { init } from 'tinyh264'; 2 | 3 | init(); 4 | -------------------------------------------------------------------------------- /vendor/tinyh264/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019 Erik De Rijcke 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any 4 | purpose with or without fee is hereby granted, provided that the above 5 | copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -------------------------------------------------------------------------------- /vendor/tinyh264/README.md: -------------------------------------------------------------------------------- 1 | Based on demo code from [udevbe/tinyh264](https://github.com/udevbe/tinyh264/tree/caf7142/demo) 2 | 3 | See [License](LICENSE) 4 | -------------------------------------------------------------------------------- /vendor/tinyh264/ShaderCompiler.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Represents a WebGL shader object and provides a mechanism to load shaders from HTML 3 | * script tags. 4 | */ 5 | 6 | export default class ShaderCompiler { 7 | /** 8 | * @param {WebGLRenderingContext}gl 9 | * @param {{type: string, source: string}}script 10 | * @return {WebGLShader} 11 | */ 12 | static compile(gl: WebGLRenderingContext, script: { type: string; source: string }): WebGLShader | null { 13 | let shader: WebGLShader | null; 14 | // Now figure out what type of shader script we have, based on its MIME type. 15 | if (script.type === 'x-shader/x-fragment') { 16 | shader = gl.createShader(gl.FRAGMENT_SHADER); 17 | } else if (script.type === 'x-shader/x-vertex') { 18 | shader = gl.createShader(gl.VERTEX_SHADER); 19 | } else { 20 | throw new Error('Unknown shader type: ' + script.type); 21 | } 22 | if (!shader) { 23 | throw new Error('Failed to create shader'); 24 | } 25 | 26 | // Send the source to the shader object. 27 | gl.shaderSource(shader, script.source); 28 | 29 | // Compile the shader program. 30 | gl.compileShader(shader); 31 | 32 | // See if it compiled successfully. 33 | if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { 34 | throw new Error('An error occurred compiling the shaders: ' + gl.getShaderInfoLog(shader)); 35 | } 36 | 37 | return shader; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /vendor/tinyh264/ShaderProgram.ts: -------------------------------------------------------------------------------- 1 | export default class ShaderProgram { 2 | public program: WebGLProgram | null; 3 | /** 4 | * @param {WebGLRenderingContext}gl 5 | */ 6 | constructor(private gl: WebGLRenderingContext) { 7 | this.program = this.gl.createProgram(); 8 | } 9 | 10 | /** 11 | * @param {WebGLShader}shader 12 | */ 13 | attach(shader: WebGLShader): void { 14 | if (!this.program) { 15 | throw Error(`Program type is ${typeof this.program}`); 16 | } 17 | this.gl.attachShader(this.program, shader); 18 | } 19 | 20 | link(): void { 21 | if (!this.program) { 22 | throw Error(`Program type is ${typeof this.program}`); 23 | } 24 | this.gl.linkProgram(this.program); 25 | // If creating the shader program failed, alert. 26 | if (!this.gl.getProgramParameter(this.program, this.gl.LINK_STATUS)) { 27 | console.error('Unable to initialize the shader program.'); 28 | } 29 | } 30 | 31 | use(): void { 32 | this.gl.useProgram(this.program); 33 | } 34 | 35 | /** 36 | * @param {string}name 37 | * @return {number} 38 | */ 39 | getAttributeLocation(name: string): number { 40 | if (!this.program) { 41 | throw Error(`Program type is ${typeof this.program}`); 42 | } 43 | return this.gl.getAttribLocation(this.program, name); 44 | } 45 | 46 | /** 47 | * @param {string}name 48 | * @return {WebGLUniformLocation | null} 49 | */ 50 | getUniformLocation(name: string): WebGLUniformLocation | null { 51 | if (!this.program) { 52 | throw Error(`Program type is ${typeof this.program}`); 53 | } 54 | return this.gl.getUniformLocation(this.program, name); 55 | } 56 | 57 | /** 58 | * @param {WebGLUniformLocation}uniformLocation 59 | * @param {Array}array 60 | */ 61 | setUniformM4(uniformLocation: WebGLUniformLocation, array: number[]): void { 62 | this.gl.uniformMatrix4fv(uniformLocation, false, array); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /vendor/tinyh264/ShaderSources.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @type {{type: string, source: string}} 3 | */ 4 | export const vertexQuad = { 5 | type: 'x-shader/x-vertex', 6 | source: ` 7 | precision mediump float; 8 | 9 | uniform mat4 u_projection; 10 | attribute vec2 a_position; 11 | attribute vec2 a_texCoord; 12 | varying vec2 v_texCoord; 13 | void main(){ 14 | v_texCoord = a_texCoord; 15 | gl_Position = u_projection * vec4(a_position, 0.0, 1.0); 16 | } 17 | `, 18 | }; 19 | 20 | /** 21 | * @type {{type: string, source: string}} 22 | */ 23 | export const fragmentYUV = { 24 | type: 'x-shader/x-fragment', 25 | source: ` 26 | precision lowp float; 27 | 28 | varying vec2 v_texCoord; 29 | 30 | uniform sampler2D yTexture; 31 | uniform sampler2D uTexture; 32 | uniform sampler2D vTexture; 33 | 34 | const mat4 conversion = mat4( 35 | 1.0, 0.0, 1.402, -0.701, 36 | 1.0, -0.344, -0.714, 0.529, 37 | 1.0, 1.772, 0.0, -0.886, 38 | 0.0, 0.0, 0.0, 0.0 39 | ); 40 | 41 | void main(void) { 42 | float yChannel = texture2D(yTexture, v_texCoord).x; 43 | float uChannel = texture2D(uTexture, v_texCoord).x; 44 | float vChannel = texture2D(vTexture, v_texCoord).x; 45 | vec4 channels = vec4(yChannel, uChannel, vChannel, 1.0); 46 | vec3 rgb = (channels * conversion).xyz; 47 | gl_FragColor = vec4(rgb, 1.0); 48 | } 49 | `, 50 | }; 51 | -------------------------------------------------------------------------------- /vendor/tinyh264/YUVCanvas.ts: -------------------------------------------------------------------------------- 1 | import Canvas from './Canvas'; 2 | 3 | export default class YUVCanvas extends Canvas { 4 | private canvasCtx: CanvasRenderingContext2D; 5 | private canvasBuffer: ImageData | null = null; 6 | 7 | constructor(canvas: HTMLCanvasElement) { 8 | super(canvas); 9 | this.canvasCtx = this.canvas.getContext('2d') as CanvasRenderingContext2D; 10 | } 11 | public decode(buffer: Uint8Array, width: number, height: number): void { 12 | if (!buffer) { 13 | return; 14 | } 15 | if (!this.canvasBuffer) { 16 | this.canvasBuffer = this.canvasCtx.createImageData(width, height); 17 | } 18 | 19 | const lumaSize = width * height; 20 | const chromaSize = lumaSize >> 2; 21 | 22 | const ybuf = buffer.subarray(0, lumaSize); 23 | const ubuf = buffer.subarray(lumaSize, lumaSize + chromaSize); 24 | const vbuf = buffer.subarray(lumaSize + chromaSize, lumaSize + 2 * chromaSize); 25 | 26 | for (let y = 0; y < height; y++) { 27 | for (let x = 0; x < width; x++) { 28 | const yIndex = x + y * width; 29 | const uIndex = ~~(y / 2) * ~~(width / 2) + ~~(x / 2); 30 | const vIndex = ~~(y / 2) * ~~(width / 2) + ~~(x / 2); 31 | const R = 1.164 * (ybuf[yIndex] - 16) + 1.596 * (vbuf[vIndex] - 128); 32 | const G = 1.164 * (ybuf[yIndex] - 16) - 0.813 * (vbuf[vIndex] - 128) - 0.391 * (ubuf[uIndex] - 128); 33 | const B = 1.164 * (ybuf[yIndex] - 16) + 2.018 * (ubuf[uIndex] - 128); 34 | 35 | const rgbIndex = yIndex * 4; 36 | this.canvasBuffer.data[rgbIndex + 0] = R; 37 | this.canvasBuffer.data[rgbIndex + 1] = G; 38 | this.canvasBuffer.data[rgbIndex + 2] = B; 39 | this.canvasBuffer.data[rgbIndex + 3] = 0xff; 40 | } 41 | } 42 | 43 | this.canvasCtx.putImageData(this.canvasBuffer, 0, 0); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /vendor/tinyh264/YUVWebGLCanvas.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * based on tinyh264 demo: https://github.com/udevbe/tinyh264/tree/master/demo 3 | */ 4 | 5 | import YUVSurfaceShader from './YUVSurfaceShader'; 6 | import Texture from '../h264-live-player/Texture'; 7 | import Canvas from './Canvas'; 8 | 9 | export default class YUVWebGLCanvas extends Canvas { 10 | private yTexture: Texture; 11 | private uTexture: Texture; 12 | private vTexture: Texture; 13 | private yuvSurfaceShader: YUVSurfaceShader; 14 | 15 | constructor(canvas: HTMLCanvasElement) { 16 | super(canvas); 17 | const gl = canvas.getContext('experimental-webgl', { 18 | preserveDrawingBuffer: true, 19 | }) as WebGLRenderingContext | null; 20 | if (!gl) { 21 | throw new Error('Unable to initialize WebGL. Your browser may not support it.'); 22 | } 23 | this.yuvSurfaceShader = YUVSurfaceShader.create(gl); 24 | this.yTexture = Texture.create(gl, gl.LUMINANCE); 25 | this.uTexture = Texture.create(gl, gl.LUMINANCE); 26 | this.vTexture = Texture.create(gl, gl.LUMINANCE); 27 | } 28 | 29 | decode(buffer: Uint8Array, width: number, height: number): void { 30 | this.canvas.width = width; 31 | this.canvas.height = height; 32 | 33 | // the width & height returned are actually padded, so we have to use the frame size to get the real image dimension 34 | // when uploading to texture 35 | const stride = width; // stride 36 | // height is padded with filler rows 37 | 38 | // if we knew the size of the video before encoding, we could cut out the black filler pixels. We don't, so just set 39 | // it to the size after encoding 40 | const sourceWidth = width; 41 | const sourceHeight = height; 42 | const maxXTexCoord = sourceWidth / stride; 43 | const maxYTexCoord = sourceHeight / height; 44 | 45 | const lumaSize = stride * height; 46 | const chromaSize = lumaSize >> 2; 47 | 48 | const yBuffer = buffer.subarray(0, lumaSize); 49 | const uBuffer = buffer.subarray(lumaSize, lumaSize + chromaSize); 50 | const vBuffer = buffer.subarray(lumaSize + chromaSize, lumaSize + 2 * chromaSize); 51 | 52 | const chromaHeight = height >> 1; 53 | const chromaStride = stride >> 1; 54 | 55 | // we upload the entire image, including stride padding & filler rows. The actual visible image will be mapped 56 | // from texture coordinates as to crop out stride padding & filler rows using maxXTexCoord and maxYTexCoord. 57 | 58 | this.yTexture.image2dBuffer(yBuffer, stride, height); 59 | this.uTexture.image2dBuffer(uBuffer, chromaStride, chromaHeight); 60 | this.vTexture.image2dBuffer(vBuffer, chromaStride, chromaHeight); 61 | 62 | this.yuvSurfaceShader.setTexture(this.yTexture, this.uTexture, this.vTexture); 63 | this.yuvSurfaceShader.updateShaderData({ w: width, h: height }, { maxXTexCoord, maxYTexCoord }); 64 | this.yuvSurfaceShader.draw(); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /webpack/build.config.utils.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import * as path from 'path'; 3 | 4 | type BuildConfig = Record; 5 | 6 | const DEFAULT_CONFIG_PATH = path.resolve(path.dirname(__filename), 'default.build.config.json'); 7 | const configCache: Map = new Map(); 8 | const mergedCache: Map = new Map(); 9 | 10 | export function getConfig(filename: string): BuildConfig { 11 | let cached = configCache.get(filename); 12 | if (!cached) { 13 | const filtered: BuildConfig = {}; 14 | const absolutePath = path.isAbsolute(filename) ? filename : path.resolve(process.cwd(), filename); 15 | const rawConfig = JSON.parse(fs.readFileSync(absolutePath).toString()); 16 | Object.keys(rawConfig).forEach((key) => { 17 | const value = rawConfig[key]; 18 | if (typeof value === 'boolean' || typeof value === 'string') { 19 | filtered[key] = value; 20 | } 21 | }); 22 | cached = filtered; 23 | configCache.set(filename, cached); 24 | } 25 | return cached; 26 | } 27 | 28 | export function mergeWithDefaultConfig(custom?: string): BuildConfig { 29 | if (!custom) { 30 | return getConfig(DEFAULT_CONFIG_PATH); 31 | } 32 | let cached = mergedCache.get(custom); 33 | if (!cached) { 34 | const defaultConfig = getConfig(DEFAULT_CONFIG_PATH); 35 | const customConfig = getConfig(custom); 36 | cached = Object.assign({}, defaultConfig, customConfig); 37 | mergedCache.set(custom, cached); 38 | } 39 | return cached; 40 | } 41 | -------------------------------------------------------------------------------- /webpack/default.build.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "SCRCPY_LISTENS_ON_ALL_INTERFACES": true, 3 | "USE_WEBCODECS": true, 4 | "USE_BROADWAY": true, 5 | "USE_H264_CONVERTER": true, 6 | "USE_TINY_H264": true, 7 | "USE_WDA_MJPEG_SERVER": false, 8 | "USE_QVH_SERVER": true, 9 | "INCLUDE_DEV_TOOLS": true, 10 | "INCLUDE_ADB_SHELL": true, 11 | "INCLUDE_FILE_LISTING": true, 12 | "INCLUDE_APPL": false, 13 | "INCLUDE_GOOG": true, 14 | "PATHNAME": "/" 15 | } 16 | -------------------------------------------------------------------------------- /webpack/ws-scrcpy.dev.ts: -------------------------------------------------------------------------------- 1 | import { frontend, backend } from './ws-scrcpy.common'; 2 | import webpack from 'webpack'; 3 | 4 | const devOpts: webpack.Configuration = { 5 | devtool: 'inline-source-map', 6 | mode: 'development', 7 | }; 8 | 9 | const front = () => { 10 | return Object.assign({}, frontend(), devOpts); 11 | }; 12 | const back = () => { 13 | return Object.assign({}, backend(), devOpts); 14 | }; 15 | 16 | module.exports = [front, back]; 17 | -------------------------------------------------------------------------------- /webpack/ws-scrcpy.prod.ts: -------------------------------------------------------------------------------- 1 | import { backend, frontend } from './ws-scrcpy.common'; 2 | import webpack from 'webpack'; 3 | 4 | const prodOpts: webpack.Configuration = { 5 | mode: 'production', 6 | }; 7 | 8 | const front = () => { 9 | return Object.assign({}, frontend(), prodOpts); 10 | }; 11 | const back = () => { 12 | return Object.assign({}, backend(), prodOpts); 13 | }; 14 | 15 | module.exports = [front, back]; 16 | --------------------------------------------------------------------------------