├── .eslintrc.cjs ├── .github └── workflows │ ├── build.yml │ └── tauri.yml.bak ├── .gitignore ├── .prettierrc.json ├── .vscode └── extensions.json ├── LICENSE ├── README-en.md ├── README.md ├── docs ├── screenshot-2.jpg ├── screenshot-3.jpg └── screenshot.png ├── index.html ├── package.json ├── public ├── favicon-192.png ├── favicon-512.png ├── favicon.ico └── favicon.png ├── src-tauri ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── app-icon.png ├── build.rs ├── icons │ ├── 128x128.png │ ├── 128x128@2x.png │ ├── 32x32.png │ ├── Square107x107Logo.png │ ├── Square142x142Logo.png │ ├── Square150x150Logo.png │ ├── Square284x284Logo.png │ ├── Square30x30Logo.png │ ├── Square310x310Logo.png │ ├── Square44x44Logo.png │ ├── Square71x71Logo.png │ ├── Square89x89Logo.png │ ├── StoreLogo.png │ ├── icon.icns │ ├── icon.ico │ └── icon.png ├── src │ └── main.rs └── tauri.conf.json ├── src ├── App.vue ├── assets │ ├── common.scss │ └── main.scss ├── components │ ├── KvmPlayer │ │ ├── KvmInput.vue │ │ ├── QRScanner.vue │ │ ├── SettingsPrompt.vue │ │ ├── TauriActions.vue │ │ ├── UI │ │ │ └── DragButton.vue │ │ ├── hooks │ │ │ └── use-action-bar.ts │ │ ├── index.vue │ │ └── utils │ │ │ ├── ch9329.ts │ │ │ ├── cursor-hider.ts │ │ │ ├── index.ts │ │ │ ├── keys-enum.ts │ │ │ ├── qrcode-decoder.ts │ │ │ ├── serial-state.ts │ │ │ └── video-recorder.ts │ ├── NotificationList │ │ ├── NotificationList.vue │ │ ├── NotifyItem.vue │ │ └── notification-list.ts │ └── PromptInput │ │ ├── PromptInput.vue │ │ └── prompt-input.ts ├── global.d.ts ├── main.ts ├── router │ └── index.ts ├── stores │ └── settings.ts └── utils │ ├── event-bus.ts │ ├── index.ts │ └── router-utils.ts ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.node.json ├── vite.config.js └── yarn.lock /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | require('@rushstack/eslint-patch/modern-module-resolution') 3 | 4 | module.exports = { 5 | root: true, 6 | extends: [ 7 | 'plugin:vue/vue3-essential', 8 | 'eslint:recommended', 9 | '@vue/eslint-config-typescript', 10 | '@vue/eslint-config-prettier', 11 | ], 12 | parserOptions: { 13 | ecmaVersion: 'latest', 14 | }, 15 | } 16 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Auto Deploy 5 | 6 | #on: 7 | # push: 8 | # branches: [ "master" ] 9 | # pull_request: 10 | # branches: [ "master" ] 11 | on: 12 | push: 13 | tags: 14 | - 'v*' 15 | 16 | jobs: 17 | build: 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: actions/checkout@v3 21 | - name: Setup node 22 | uses: actions/setup-node@v3 23 | with: 24 | node-version: '20' 25 | cache: 'yarn' 26 | - name: Build 27 | run: yarn install && yarn build 28 | - name: Deploy 29 | uses: JamesIves/github-pages-deploy-action@4.1.1 30 | with: 31 | branch: gh-pages 32 | folder: dist 33 | -------------------------------------------------------------------------------- /.github/workflows/tauri.yml.bak: -------------------------------------------------------------------------------- 1 | name: Tauri Auto Build 2 | #on: 3 | # push: 4 | # branches: [ "tauri-auto-build" ] 5 | # pull_request: 6 | # branches: [ "tauri-auto-build" ] 7 | 8 | on: 9 | push: 10 | tags: 11 | - 'v*' 12 | 13 | jobs: 14 | release: 15 | permissions: 16 | contents: write 17 | strategy: 18 | fail-fast: false 19 | matrix: 20 | platform: [windows-latest] #[macos-latest, ubuntu-20.04, windows-latest] 21 | runs-on: ${{ matrix.platform }} 22 | 23 | steps: 24 | - name: Checkout repository 25 | uses: actions/checkout@v4 26 | 27 | - name: Install dependencies (ubuntu only) 28 | if: matrix.platform == 'ubuntu-20.04' 29 | # You can remove libayatana-appindicator3-dev if you don't use the system tray feature. 30 | run: | 31 | sudo apt-get update 32 | sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libayatana-appindicator3-dev librsvg2-dev 33 | 34 | - name: Rust setup 35 | uses: dtolnay/rust-toolchain@stable 36 | 37 | - name: Rust cache 38 | uses: swatinem/rust-cache@v2 39 | with: 40 | workspaces: './src-tauri -> target' 41 | 42 | - name: Sync node version and setup cache 43 | uses: actions/setup-node@v4 44 | with: 45 | node-version: 'lts/*' 46 | cache: 'yarn' # Set this to npm, yarn or pnpm. 47 | 48 | - name: Install frontend dependencies 49 | # If you don't have `beforeBuildCommand` configured you may want to build your frontend here too. 50 | run: yarn install # Change this to npm, yarn or pnpm. 51 | 52 | - name: Build the app 53 | uses: tauri-apps/tauri-action@v0 54 | 55 | env: 56 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 57 | with: 58 | tagName: ${{ github.ref_name }} # This only works if your workflow triggers on new tags. 59 | releaseName: 'v__VERSION__' # tauri-action replaces \_\_VERSION\_\_ with the app version. 60 | releaseBody: 'See the assets to download and install this version.' 61 | releaseDraft: true 62 | prerelease: false 63 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | .DS_Store 12 | dist 13 | dist-ssr 14 | coverage 15 | *.local 16 | 17 | /cypress/videos/ 18 | /cypress/screenshots/ 19 | 20 | # Editor directories and files 21 | .vscode/* 22 | !.vscode/extensions.json 23 | .idea 24 | *.suo 25 | *.ntvs* 26 | *.njsproj 27 | *.sln 28 | *.sw? 29 | 30 | *.tsbuildinfo 31 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/prettierrc", 3 | "trailingComma": "all", 4 | "printWidth": 100, 5 | "tabWidth": 2, 6 | "tabs": false, 7 | "semi": false, 8 | "singleQuote": true, 9 | "quoteProps": "as-needed", 10 | "bracketSpacing": false, 11 | "bracketSameLine": false, 12 | "arrowParens": "always", 13 | "endOfLine": "auto" 14 | } 15 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"] 3 | } 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Canwdev 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README-en.md: -------------------------------------------------------------------------------- 1 | ## Web MediaDevices Player 2 | 3 | A web application for playing system [video/audio] input devices using the [Media Capture and Streams API](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia) technology. 4 | 5 | - Web version: https://canwdev.github.io/web-mediadevices-player/ 6 | - Tauri packaged client: [Releases](https://github.com/canwdev/web-mediadevices-player/releases) 7 | - [Chinese Readme](./README.md) 8 | 9 | Main purposes: 10 | - View HDMI to USB capture card 11 | - Play webcam videos, desktop screen recording 12 | - Capture screenshots and record in webm format 13 | - v1.1.5 New features 14 | - [CH9329](https://one-kvm.mofeng.run/ch9329_hid/) KVM keyboard and mouse control, ref: [webusbkvm](https://github.com/kkocdko/kblog/blob/master/source/toys/webusbkvm/README.md) 15 | - Supports relative mouse, absolute mouse, hotkeys, and ASCII text sending. 16 | - Video screen QR code scanning 17 | 18 | ![screenshot](docs/screenshot-2.jpg) 19 | ![screenshot](docs/screenshot-3.jpg) 20 | ![screenshot](docs/screenshot.png) 21 | 22 | Tips: 23 | - The first time you use it, it will request camera and microphone permissions. You can reject microphone permissions if not needed. After requesting, it will wait a few seconds to load the devices. 24 | - This page must run in https or localhost environments. Other environments (such as: filesystem) do not have access to devices. 25 | - There may be issues with dragging the progress bar of the recorded webm video. Manually transcoding it to mp4 can solve the problem. 26 | 27 | ## Development 28 | 29 | > Contributions are welcome 30 | 31 | ```sh 32 | # Install dependencies 33 | yarn install 34 | 35 | # Development mode 36 | yarn dev 37 | 38 | # Build the Web version 39 | yarn build 40 | 41 | # Build Tauri App 42 | yarn build:tauri 43 | ``` 44 | 45 | --- 46 | 47 | ## Star History 48 | 49 | - Thanks for your stars https://www.ruanyifeng.com/blog/2024/06/weekly-issue-303.html 50 | 51 | [![Star History Chart](https://api.star-history.com/svg?repos=canwdev/web-mediadevices-player&type=Date)](https://star-history.com/#canwdev/web-mediadevices-player&Date) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Web MediaDevices Player 2 | 3 | 用于播放【视频/音频】输入设备的网页应用,使用了 [Media Capture and Streams API](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia) 技术。 4 | 5 | - 网页版:https://canwdev.github.io/web-mediadevices-player/ 6 | - Tauri 打包的客户端:[Releases](https://github.com/canwdev/web-mediadevices-player/releases) 7 | - [English Readme](./README-en.md) 8 | 9 | 主要用途: 10 | - HDMI to USB 采集卡查看 11 | - Webcam 视频播放,桌面录屏 12 | - 画面截图,录制为 webm 格式 13 | - v1.1.5 新增功能 14 | - [CH9329](https://one-kvm.mofeng.run/ch9329_hid/) KVM 键鼠控制,参考: [webusbkvm](https://github.com/kkocdko/kblog/blob/master/source/toys/webusbkvm/README.md) 15 | - 支持相对鼠标、绝对鼠标、快捷键、ASCII文本发送 16 | - 视频画面二维码扫描 17 | 18 | ![screenshot](docs/screenshot-2.jpg) 19 | ![screenshot](docs/screenshot-3.jpg) 20 | ![screenshot](docs/screenshot.png) 21 | 22 | 提示: 23 | - 首次使用会请求摄像头和麦克风权限,如果不需要麦克风权限可以拒绝,请求过后会等待几秒钟加载设备。 24 | - 此页面必须运行在 https 或 localhost 环境,其他环境(如:filesystem)无访问设备的权限。 25 | - 录制的 webm 视频拖动进度条可能存在问题,手动转码成 mp4 即可解决。 26 | 27 | --- 28 | 29 | ## 开发 30 | 31 | > 欢迎提交PR 32 | 33 | ```sh 34 | # 安装依赖 35 | yarn install 36 | 37 | # 开发模式 38 | yarn dev 39 | 40 | # 构建 Web 版 41 | yarn build 42 | 43 | # 构建 Tauri App 44 | yarn build:tauri 45 | ``` 46 | 47 | --- 48 | 49 | ## Star History 50 | 51 | - Thanks for your stars https://www.ruanyifeng.com/blog/2024/06/weekly-issue-303.html 52 | 53 | [![Star History Chart](https://api.star-history.com/svg?repos=canwdev/web-mediadevices-player&type=Date)](https://star-history.com/#canwdev/web-mediadevices-player&Date) -------------------------------------------------------------------------------- /docs/screenshot-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/canwdev/web-mediadevices-player/49197f77d499df65d7118515ae5ca9561a5bc278/docs/screenshot-2.jpg -------------------------------------------------------------------------------- /docs/screenshot-3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/canwdev/web-mediadevices-player/49197f77d499df65d7118515ae5ca9561a5bc278/docs/screenshot-3.jpg -------------------------------------------------------------------------------- /docs/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/canwdev/web-mediadevices-player/49197f77d499df65d7118515ae5ca9561a5bc278/docs/screenshot.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Web MediaDevices Player 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web-mediadevices-player", 3 | "version": "1.1.7", 4 | "private": true, 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "build:strict": "run-p type-check \"build-only {@}\" --", 10 | "preview": "vite preview", 11 | "type-check": "vue-tsc --build --force", 12 | "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore", 13 | "format": "prettier --write src/", 14 | "tauri": "tauri", 15 | "build:tauri": "tauri build" 16 | }, 17 | "dependencies": { 18 | "@mdi/font": "^7.4.47", 19 | "@tauri-apps/api": "^1.5.3", 20 | "@vueuse/core": "^10.10.0", 21 | "jsqr": "^1.4.0", 22 | "moment": "^2.30.1", 23 | "pinia": "^2.1.7", 24 | "pinia-plugin-persistedstate": "^3.2.1", 25 | "sass": "^1.77.4", 26 | "vue": "^3.4.21", 27 | "vue-router": "^4.3.0", 28 | "web-serial-polyfill": "^1.0.15" 29 | }, 30 | "devDependencies": { 31 | "@rushstack/eslint-patch": "^1.8.0", 32 | "@tauri-apps/cli": "^1.5.10", 33 | "@tsconfig/node20": "^20.1.4", 34 | "@types/node": "^20.12.5", 35 | "@vitejs/plugin-vue": "^5.0.4", 36 | "@vitejs/plugin-vue-jsx": "^3.1.0", 37 | "@vue/eslint-config-prettier": "^9.0.0", 38 | "@vue/eslint-config-typescript": "^13.0.0", 39 | "@vue/tsconfig": "^0.5.1", 40 | "eslint": "^8.57.0", 41 | "eslint-plugin-vue": "^9.23.0", 42 | "npm-run-all2": "^6.1.2", 43 | "prettier": "^3.2.5", 44 | "typescript": "~5.4.0", 45 | "vite": "^5.2.8", 46 | "vite-plugin-pwa": "^0.20.0", 47 | "vue-tsc": "^2.0.11" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /public/favicon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/canwdev/web-mediadevices-player/49197f77d499df65d7118515ae5ca9561a5bc278/public/favicon-192.png -------------------------------------------------------------------------------- /public/favicon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/canwdev/web-mediadevices-player/49197f77d499df65d7118515ae5ca9561a5bc278/public/favicon-512.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/canwdev/web-mediadevices-player/49197f77d499df65d7118515ae5ca9561a5bc278/public/favicon.ico -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/canwdev/web-mediadevices-player/49197f77d499df65d7118515ae5ca9561a5bc278/public/favicon.png -------------------------------------------------------------------------------- /src-tauri/.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | -------------------------------------------------------------------------------- /src-tauri/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "app" 3 | version = "0.1.0" 4 | description = "A Tauri App" 5 | authors = ["you"] 6 | license = "" 7 | repository = "" 8 | default-run = "app" 9 | edition = "2021" 10 | rust-version = "1.60" 11 | 12 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 13 | 14 | [build-dependencies] 15 | tauri-build = { version = "1.5.1", features = [] } 16 | 17 | [dependencies] 18 | serde_json = "1.0" 19 | serde = { version = "1.0", features = ["derive"] } 20 | tauri = { version = "1.6.0", features = [ "window-set-fullscreen", "window-set-always-on-top", "shell-open"] } 21 | tauri-plugin-window-state = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" } 22 | 23 | [features] 24 | # this feature is used for production builds or when `devPath` points to the filesystem and the built-in dev server is disabled. 25 | # If you use cargo directly instead of tauri's cli you can use this feature flag to switch between tauri's `dev` and `build` modes. 26 | # DO NOT REMOVE!! 27 | custom-protocol = [ "tauri/custom-protocol" ] 28 | 29 | # 生产打包启用控制台 30 | [profile.release.package.wry] 31 | debug = true 32 | debug-assertions = true 33 | -------------------------------------------------------------------------------- /src-tauri/app-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/canwdev/web-mediadevices-player/49197f77d499df65d7118515ae5ca9561a5bc278/src-tauri/app-icon.png -------------------------------------------------------------------------------- /src-tauri/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | tauri_build::build() 3 | } 4 | -------------------------------------------------------------------------------- /src-tauri/icons/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/canwdev/web-mediadevices-player/49197f77d499df65d7118515ae5ca9561a5bc278/src-tauri/icons/128x128.png -------------------------------------------------------------------------------- /src-tauri/icons/128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/canwdev/web-mediadevices-player/49197f77d499df65d7118515ae5ca9561a5bc278/src-tauri/icons/128x128@2x.png -------------------------------------------------------------------------------- /src-tauri/icons/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/canwdev/web-mediadevices-player/49197f77d499df65d7118515ae5ca9561a5bc278/src-tauri/icons/32x32.png -------------------------------------------------------------------------------- /src-tauri/icons/Square107x107Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/canwdev/web-mediadevices-player/49197f77d499df65d7118515ae5ca9561a5bc278/src-tauri/icons/Square107x107Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square142x142Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/canwdev/web-mediadevices-player/49197f77d499df65d7118515ae5ca9561a5bc278/src-tauri/icons/Square142x142Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square150x150Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/canwdev/web-mediadevices-player/49197f77d499df65d7118515ae5ca9561a5bc278/src-tauri/icons/Square150x150Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square284x284Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/canwdev/web-mediadevices-player/49197f77d499df65d7118515ae5ca9561a5bc278/src-tauri/icons/Square284x284Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square30x30Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/canwdev/web-mediadevices-player/49197f77d499df65d7118515ae5ca9561a5bc278/src-tauri/icons/Square30x30Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square310x310Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/canwdev/web-mediadevices-player/49197f77d499df65d7118515ae5ca9561a5bc278/src-tauri/icons/Square310x310Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square44x44Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/canwdev/web-mediadevices-player/49197f77d499df65d7118515ae5ca9561a5bc278/src-tauri/icons/Square44x44Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square71x71Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/canwdev/web-mediadevices-player/49197f77d499df65d7118515ae5ca9561a5bc278/src-tauri/icons/Square71x71Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square89x89Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/canwdev/web-mediadevices-player/49197f77d499df65d7118515ae5ca9561a5bc278/src-tauri/icons/Square89x89Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/StoreLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/canwdev/web-mediadevices-player/49197f77d499df65d7118515ae5ca9561a5bc278/src-tauri/icons/StoreLogo.png -------------------------------------------------------------------------------- /src-tauri/icons/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/canwdev/web-mediadevices-player/49197f77d499df65d7118515ae5ca9561a5bc278/src-tauri/icons/icon.icns -------------------------------------------------------------------------------- /src-tauri/icons/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/canwdev/web-mediadevices-player/49197f77d499df65d7118515ae5ca9561a5bc278/src-tauri/icons/icon.ico -------------------------------------------------------------------------------- /src-tauri/icons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/canwdev/web-mediadevices-player/49197f77d499df65d7118515ae5ca9561a5bc278/src-tauri/icons/icon.png -------------------------------------------------------------------------------- /src-tauri/src/main.rs: -------------------------------------------------------------------------------- 1 | // Prevents additional console window on Windows in release, DO NOT REMOVE!! 2 | #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] 3 | 4 | fn main() { 5 | tauri::Builder::default() 6 | .plugin(tauri_plugin_window_state::Builder::default().build()) 7 | .run(tauri::generate_context!()) 8 | .expect("error while running tauri application"); 9 | } 10 | -------------------------------------------------------------------------------- /src-tauri/tauri.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../node_modules/@tauri-apps/cli/schema.json", 3 | "build": { 4 | "beforeBuildCommand": "npm run build", 5 | "beforeDevCommand": "npm run dev", 6 | "devPath": "http://localhost:8087", 7 | "distDir": "../dist" 8 | }, 9 | "package": { 10 | "productName": "web-mediadevices-player", 11 | "version": "1.1.7" 12 | }, 13 | "tauri": { 14 | "allowlist": { 15 | "window": { 16 | "setAlwaysOnTop": true, 17 | "setFullscreen": true 18 | }, 19 | "shell": { 20 | "open": true 21 | } 22 | }, 23 | "bundle": { 24 | "active": true, 25 | "category": "DeveloperTool", 26 | "copyright": "", 27 | "deb": { 28 | "depends": [] 29 | }, 30 | "externalBin": [], 31 | "icon": [ 32 | "icons/32x32.png", 33 | "icons/128x128.png", 34 | "icons/128x128@2x.png", 35 | "icons/icon.icns", 36 | "icons/icon.ico" 37 | ], 38 | "identifier": "com.canwdev.web-mediadevices-player", 39 | "longDescription": "", 40 | "macOS": { 41 | "entitlements": null, 42 | "exceptionDomain": "", 43 | "frameworks": [], 44 | "providerShortName": null, 45 | "signingIdentity": null 46 | }, 47 | "resources": [], 48 | "shortDescription": "", 49 | "targets": "all", 50 | "windows": { 51 | "certificateThumbprint": null, 52 | "digestAlgorithm": "sha256", 53 | "timestampUrl": "" 54 | } 55 | }, 56 | "security": { 57 | "csp": null 58 | }, 59 | "updater": { 60 | "active": false 61 | }, 62 | "windows": [ 63 | { 64 | "fullscreen": false, 65 | "height": 600, 66 | "resizable": true, 67 | "title": "Web Mediadevices Player", 68 | "width": 800 69 | } 70 | ] 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 11 | -------------------------------------------------------------------------------- /src/assets/common.scss: -------------------------------------------------------------------------------- 1 | .font-italic { 2 | font-style: italic; 3 | } 4 | 5 | .font-code, .font-code * { 6 | font-family: 'Victor Mono Regular', 'Cascadia Code', '等距更纱黑体 SC', 'Monaco', 'Fira Code Medium', Consolas, 'Courier New', monospace; 7 | } 8 | 9 | .font-emoji, .font-emoji * { 10 | font-family: "Segoe UI Emoji", "SF Pro SC", "SF Pro Text", "SF Pro Icons", "PingFang SC", "Helvetica Neue", "Helvetica", "Arial", sans-serif; 11 | } 12 | 13 | .btn-no-style { 14 | background: none; 15 | border: none; 16 | outline: none; 17 | cursor: pointer; 18 | color: inherit; 19 | padding: 0; 20 | text-decoration: none; 21 | 22 | &:active { 23 | opacity: 0.8; 24 | } 25 | 26 | &:disabled { 27 | cursor: not-allowed; 28 | opacity: 0.5; 29 | } 30 | } 31 | 32 | .scrollbar-mini * { 33 | 34 | &::-webkit-scrollbar { 35 | width: 8px; 36 | height: 8px; 37 | overflow: overlay; 38 | background: transparent; 39 | } 40 | 41 | &::-webkit-scrollbar-thumb { 42 | border-radius: 8px; 43 | border: 2px solid transparent; 44 | background-clip: content-box; 45 | background-color: rgba(192, 192, 192, 0.63); 46 | 47 | &:hover { 48 | background-color: #bebebe; 49 | } 50 | } 51 | 52 | &::-webkit-scrollbar-track { 53 | border-radius: 4px; 54 | background: transparent; 55 | } 56 | 57 | &::-webkit-scrollbar-corner { 58 | background-color: transparent; 59 | } 60 | } 61 | 62 | .cursor-pointer { 63 | cursor: pointer; 64 | } 65 | .cursor-help { 66 | cursor: help; 67 | } 68 | 69 | .flex-row-center-gap { 70 | display: flex; 71 | flex-wrap: wrap; 72 | gap: 8px; 73 | align-items: center; 74 | } 75 | 76 | 77 | /* Vue transitions */ 78 | .fade-enter-active, 79 | .fade-leave-active { 80 | transition: opacity 0.3s ease; 81 | } 82 | 83 | .fade-enter-from, 84 | .fade-leave-to { 85 | opacity: 0; 86 | } 87 | 88 | 89 | .fade-scale-enter-active, 90 | .fade-scale-leave-active { 91 | transform: scale(1); 92 | transition: opacity 0.3s, transform 0.3s; 93 | } 94 | 95 | .fade-scale-enter-from, 96 | .fade-scale-leave-to { 97 | opacity: 0; 98 | transform: scale(0.92); 99 | transition: opacity 0.3s, transform 0.3s; 100 | } 101 | 102 | 103 | .fade-up-enter-active, 104 | .fade-up-leave-active { 105 | transition: opacity 0.3s ease, transform 0.3s ease; 106 | } 107 | 108 | .fade-up-enter-from, 109 | .fade-up-leave-to { 110 | transform: translateY(20px); 111 | opacity: 0; 112 | } 113 | 114 | .fade-down-enter-active, 115 | .fade-down-leave-active { 116 | transition: opacity 0.3s ease, transform 0.3s ease; 117 | } 118 | 119 | .fade-down-enter-from, 120 | .fade-down-leave-to { 121 | transform: translateY(-20px); 122 | opacity: 0; 123 | } 124 | 125 | .fade-left-enter-active, 126 | .fade-left-leave-active { 127 | transition: opacity 0.3s ease, transform 0.3s ease; 128 | } 129 | 130 | .fade-left-enter-from, 131 | .fade-left-leave-to { 132 | transform: translateX(20px); 133 | opacity: 0; 134 | } 135 | 136 | .fade-right-enter-active, 137 | .fade-right-leave-active { 138 | transition: opacity 0.3s ease, transform 0.3s ease; 139 | } 140 | 141 | .fade-right-enter-from, 142 | .fade-right-leave-to { 143 | transform: translateX(-20px); 144 | opacity: 0; 145 | } 146 | 147 | 148 | /* 1. declare transition */ 149 | .fadeMove-move, 150 | .fadeMove-enter-active, 151 | .fadeMove-leave-active { 152 | transition: all 0.5s cubic-bezier(0.55, 0, 0.1, 1); 153 | } 154 | 155 | /* 2. declare enter from and leave to state */ 156 | .fadeMove-enter-from, 157 | .fadeMove-leave-to { 158 | opacity: 0; 159 | transform: scaleY(0.01); 160 | } 161 | 162 | /* 3. ensure leaving items are taken out of layout flow so that moving 163 | animations can be calculated correctly. */ 164 | .fadeMove-leave-active { 165 | position: absolute; 166 | } 167 | 168 | 169 | // animation: linear blink-animation 3s infinite; 170 | @keyframes blink-animation { 171 | 0% { 172 | opacity: 0.5; 173 | } 174 | 50% { 175 | opacity: 1; 176 | } 177 | 100% { 178 | opacity: 0.5; 179 | } 180 | } -------------------------------------------------------------------------------- /src/assets/main.scss: -------------------------------------------------------------------------------- 1 | @import './common.scss'; 2 | 3 | body, 4 | html, 5 | #app { 6 | width: 100%; 7 | height: 100%; 8 | } 9 | 10 | #app { 11 | position: relative; 12 | overflow: hidden; 13 | user-select: none; 14 | } 15 | 16 | * { 17 | box-sizing: border-box; 18 | } 19 | 20 | body { 21 | margin: 0; 22 | background-color: #000000; 23 | color: white; 24 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji"; 25 | } 26 | 27 | .themed-input { 28 | background: rgba(0, 0, 0, 0.6); 29 | color: white; 30 | border: 1px solid rgba(255, 255, 255, 0.4); 31 | border-radius: 4px; 32 | padding: 2px 4px; 33 | box-sizing: border-box; 34 | } 35 | 36 | .themed-button { 37 | background: rgba(0, 0, 0, 0.6); 38 | color: white; 39 | border: 1px solid rgba(255, 255, 255, 0.4); 40 | border-radius: 4px; 41 | padding: 2px 6px; 42 | box-sizing: border-box; 43 | transition: all 0.3s; 44 | height: 26px; 45 | font-size: 12px; 46 | cursor: pointer; 47 | 48 | &:not(&:disabled) { 49 | &:hover { 50 | background: rgba(70, 70, 70, 0.4); 51 | transition: none; 52 | } 53 | } 54 | 55 | &:disabled { 56 | opacity: 0.7; 57 | cursor: not-allowed; 58 | } 59 | 60 | &.blue { 61 | background-color: #2196f3 !important; 62 | } 63 | &.green { 64 | background-color: #4caf50 !important; 65 | } 66 | &.red { 67 | background-color: #F44336 !important; 68 | } 69 | &.yellow { 70 | background-color: #ffeb3b !important; 71 | color: black !important; 72 | } 73 | } 74 | 75 | .panel-blur-bg { 76 | //backdrop-filter: blur(10px); 77 | background-color: rgba(0, 0, 0, 0.8); 78 | border: 1px solid rgba(255, 255, 255, 0.3); 79 | box-shadow: 0 0 10px rgba(0, 0, 0, 0.2); 80 | color: white; 81 | 82 | 83 | &._light { 84 | background-color: rgba(250, 250, 250, 0.82); 85 | border: 1px solid rgba(255, 255, 255, 0.4); 86 | box-shadow: 0 0 10px rgba(0, 0, 0, 0.2); 87 | color: black; 88 | } 89 | } -------------------------------------------------------------------------------- /src/components/KvmPlayer/KvmInput.vue: -------------------------------------------------------------------------------- 1 | 679 | 680 | 757 | 758 | 763 | -------------------------------------------------------------------------------- /src/components/KvmPlayer/QRScanner.vue: -------------------------------------------------------------------------------- 1 | 116 | 117 | 128 | 129 | 136 | -------------------------------------------------------------------------------- /src/components/KvmPlayer/SettingsPrompt.vue: -------------------------------------------------------------------------------- 1 | 51 | 52 | 221 | 222 | 275 | -------------------------------------------------------------------------------- /src/components/KvmPlayer/TauriActions.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 33 | -------------------------------------------------------------------------------- /src/components/KvmPlayer/UI/DragButton.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 34 | 35 | 128 | -------------------------------------------------------------------------------- /src/components/KvmPlayer/hooks/use-action-bar.ts: -------------------------------------------------------------------------------- 1 | import {onBeforeUnmount, onMounted, ref, shallowRef, watch} from 'vue' 2 | import {useSettingsStore} from '@/stores/settings' 3 | import {CursorHider} from '@/components/KvmPlayer/utils/cursor-hider' 4 | import {useStorage} from '@vueuse/core' 5 | 6 | export const useActionBar = () => { 7 | const mouseHider = shallowRef() 8 | const actionBarRef = shallowRef() 9 | const settingsStore = useSettingsStore() 10 | 11 | const isShowFloatBar = useStorage('wmd__is_show_float_bar', true) 12 | 13 | watch( 14 | () => settingsStore.enableKvmInput, 15 | (val) => { 16 | if (mouseHider.value) { 17 | if (!val) { 18 | mouseHider.value.start() 19 | } else { 20 | mouseHider.value.stop() 21 | } 22 | } 23 | }, 24 | ) 25 | 26 | // 是否在非KVM模式下展示控制条 27 | const isShowFloatBarInNonKvmMode = ref(false) 28 | 29 | onMounted(() => { 30 | mouseHider.value = new CursorHider( 31 | '#app', 32 | ({el, isShow}) => { 33 | if (!isShow) { 34 | el.style.cursor = 'none' 35 | isShowFloatBarInNonKvmMode.value = false 36 | } else { 37 | el.style.cursor = '' 38 | isShowFloatBarInNonKvmMode.value = true 39 | } 40 | }, 41 | 3000, 42 | ) 43 | if (settingsStore.enableKvmInput) { 44 | mouseHider.value.stop() 45 | } 46 | }) 47 | 48 | onBeforeUnmount(() => { 49 | if (mouseHider.value) { 50 | mouseHider.value.stop() 51 | } 52 | }) 53 | return { 54 | actionBarRef, 55 | mouseHider, 56 | isShowFloatBar, 57 | isShowFloatBarInNonKvmMode, 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/components/KvmPlayer/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 378 | 379 | 614 | 615 | 945 | -------------------------------------------------------------------------------- /src/components/KvmPlayer/utils/ch9329.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * CH9329芯片串口通信协议 3 | * https://www.wch.cn/products/ch9329.html 4 | * https://www.wch.cn/downloads/CH9329EVT_ZIP.html 5 | */ 6 | export enum CmdType { 7 | // 获取芯片版本等信息 8 | CMD_GET_INFO = 0x01, 9 | // 发送USB键盘普通数据 10 | CMD_SEND_KB_GENERAL_DATA = 0x02, 11 | // 发送USB键盘媒体数据 12 | CMD_SEND_KB_MEDIA_DATA = 0x03, 13 | // 发送USB绝对鼠标数据 14 | CMD_SEND_MS_ABS_DATA = 0x04, 15 | // 发送USB相对鼠标数据 16 | CMD_SEND_MS_REL_DATA = 0x05, 17 | // 发送USB自定义HID设备数据 18 | CMD_SEND_MY_HID_DATA = 0x06, 19 | // 读取USB自定义HID设备数据 20 | CMD_READ_MY_HID_DATA = 0x87, 21 | // 获取参数配置 22 | CMD_GET_PARA_CFG = 0x08, 23 | // 设置参数配置 24 | CMD_SET_PARA_CFG = 0x09, 25 | // 获取字符串描述信息 26 | CMD_GET_USB_STRING = 0x0a, 27 | // 设置字符串描述符配置 28 | CMD_SET_USB_STRING = 0x0b, 29 | // 恢复出厂默认配置 30 | CMD_SET_DEFAULT_CFG = 0x0c, 31 | // 复位芯片 32 | CMD_RESET = 0x0f, 33 | } 34 | 35 | export const genPacket = (cmd: CmdType, ...data: any[]) => { 36 | // console.log(data) 37 | for (const v of data) if (v < 0 || v > 0xff) throw v 38 | const ret = [ 39 | // 帧头:占 2 个字节,固定为 0x57、0xAB 40 | 0x57, 41 | 0xab, 42 | // 地址码:占 1 个字节,默认为 0x00 43 | 0x00, 44 | // 命令码 45 | cmd, 46 | // 后续数据长度 47 | data.length, 48 | // 后续数据 49 | ...data, 50 | ] 51 | 52 | // 累加和:占 1 个字节,计算方式为: SUM = HEAD+ADDR+CMD+LEN+DATA。 53 | const sum = new Uint8Array([0]) 54 | for (const v of ret) sum[0] += v 55 | ret.push(sum[0]) 56 | return ret 57 | } 58 | 59 | // clamp to int8 60 | export const i8clamp = (v: number) => Math.max(-0x7f, Math.min(v, 0x7f)) 61 | 62 | // 分解一个十六进制数为低字节和高字节,低字节在前,高字节在后 63 | export const decomposeHexToBytes = (hexNumber: number) => { 64 | // 确保输入是一个有效的十六进制数 65 | // if (typeof hexNumber !== 'number' || hexNumber < 0 || hexNumber > 0xFFFF) { 66 | // throw new Error('请输入一个有效的16进制数(0到0xFFFF之间)'); 67 | // } 68 | 69 | // 获取低字节(最后8位) 70 | const lowByte = hexNumber & 0xff 71 | 72 | // 获取高字节(前8位) 73 | const highByte = (hexNumber >> 8) & 0xff 74 | 75 | return [lowByte, highByte] 76 | } 77 | 78 | export enum MediaKey { 79 | EJECT = 'Eject', 80 | CD_STOP = 'CD Stop', 81 | PREV_TRACK = 'Prev. Track', 82 | NEXT_TRACK = 'Next Track', 83 | PLAY_PAUSE = 'Play/Pause', 84 | MUTE = 'Mute', 85 | VOLUME_PLUS = 'Volume-', 86 | VOLUME_MINUS = 'Volume+', 87 | REFRESH = 'Refresh', 88 | STOP = 'Stop', 89 | FORWARD = 'Forward', 90 | BACK = 'Back', 91 | HOME = 'Home', 92 | FAVORITES = 'Favorites', 93 | SEARCH = 'Search', 94 | E_MAIL = 'E-Mail', 95 | REWIND = 'Rewind', 96 | RECORD = 'Record', 97 | MINIMIZE = 'Minimize', 98 | MY_COMPUTER = 'My Computer', 99 | SCREEN_SAVE = 'Screen Save', 100 | CALCULATOR = 'Calculator', 101 | EXPLORER = 'Explorer', 102 | MEDIA = 'Media', 103 | } 104 | 105 | export const mediaKeyMatrix = [ 106 | [ 107 | MediaKey.EJECT, 108 | MediaKey.CD_STOP, 109 | MediaKey.PREV_TRACK, 110 | MediaKey.NEXT_TRACK, 111 | MediaKey.PLAY_PAUSE, 112 | MediaKey.MUTE, 113 | MediaKey.VOLUME_PLUS, 114 | MediaKey.VOLUME_MINUS, 115 | ], 116 | [ 117 | MediaKey.REFRESH, 118 | MediaKey.STOP, 119 | MediaKey.FORWARD, 120 | MediaKey.BACK, 121 | MediaKey.HOME, 122 | MediaKey.FAVORITES, 123 | MediaKey.SEARCH, 124 | MediaKey.E_MAIL, 125 | ], 126 | [ 127 | MediaKey.REWIND, 128 | MediaKey.RECORD, 129 | MediaKey.MINIMIZE, 130 | MediaKey.MY_COMPUTER, 131 | MediaKey.SCREEN_SAVE, 132 | MediaKey.CALCULATOR, 133 | MediaKey.EXPLORER, 134 | MediaKey.MEDIA, 135 | ], 136 | ] 137 | 138 | // arr[1] -> 0b10000000, arr[2] -> 0b01000000, arr[6] -> 0b00000001 139 | export const indexToBinary = (index: number) => { 140 | return 1 << (7 - index) // 7 - index 确保将 1 移到适当的位上 141 | } 142 | -------------------------------------------------------------------------------- /src/components/KvmPlayer/utils/cursor-hider.ts: -------------------------------------------------------------------------------- 1 | type showHideFnType = (arg0: {el: HTMLElement; isShow: boolean}) => void 2 | 3 | // 鼠标自动隐藏工具 4 | export class CursorHider { 5 | private timeoutID: any 6 | private targetEl: HTMLElement 7 | private showHideFn: showHideFnType 8 | private time: number 9 | 10 | constructor(cursorSelector: string, showHideFn: showHideFnType, time = 1000) { 11 | this.timeoutID = null 12 | this.targetEl = document.querySelector(cursorSelector)! 13 | this.showHideFn = showHideFn 14 | this.time = time 15 | this.showCursor = this.showCursor.bind(this) 16 | this.handlePointerLockChange = this.handlePointerLockChange.bind(this) 17 | 18 | this.start() 19 | } 20 | 21 | hideCursor() { 22 | if (typeof this.showHideFn === 'function') { 23 | this.showHideFn({el: this.targetEl, isShow: false}) 24 | } else { 25 | this.targetEl.style.cursor = 'none' 26 | } 27 | } 28 | 29 | showCursor() { 30 | if (document.pointerLockElement) { 31 | // ignore lock 32 | return 33 | } 34 | 35 | if (typeof this.showHideFn === 'function') { 36 | this.showHideFn({el: this.targetEl, isShow: true}) 37 | } else { 38 | this.targetEl.style.cursor = '' 39 | } 40 | this.runTimer() 41 | } 42 | 43 | runTimer() { 44 | clearTimeout(this.timeoutID) 45 | this.timeoutID = setTimeout(() => { 46 | this.hideCursor() 47 | }, this.time) 48 | } 49 | 50 | handlePointerLockChange() { 51 | if (!document.pointerLockElement) { 52 | // console.log('Exit pointer lock') 53 | } else { 54 | this.hideCursor() 55 | } 56 | } 57 | 58 | start() { 59 | document.addEventListener('pointerlockchange', this.handlePointerLockChange) 60 | document.addEventListener('mousemove', this.showCursor) 61 | this.runTimer() 62 | } 63 | 64 | stop() { 65 | document.removeEventListener('pointerlockchange', this.handlePointerLockChange) 66 | document.removeEventListener('mousemove', this.showCursor) 67 | clearTimeout(this.timeoutID) 68 | this.showHideFn({el: this.targetEl, isShow: true}) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/components/KvmPlayer/utils/index.ts: -------------------------------------------------------------------------------- 1 | export function snapVideoImage(video: HTMLVideoElement) { 2 | const canvas = document.createElement('canvas') 3 | canvas.width = video.videoWidth 4 | canvas.height = video.videoHeight 5 | canvas.getContext('2d')!.drawImage(video, 0, 0, video.videoWidth, video.videoHeight) 6 | 7 | // 将 canvas 转换为 png 格式并保存 8 | return canvas.toDataURL('image/png') 9 | } 10 | 11 | export const downloadUrl = (url: string, filename?) => { 12 | // 创建一个虚拟的 标签 13 | const a = document.createElement('a') 14 | // 设置 href 为文件的 URL 15 | a.href = url 16 | // 设置 download 属性,以指定下载时的文件名 17 | a.download = filename 18 | // 将 标签添加到 DOM 中 19 | document.body.appendChild(a) 20 | // 模拟点击 标签以触发下载 21 | a.click() 22 | // 点击后移除 标签 23 | document.body.removeChild(a) 24 | } 25 | 26 | /** 27 | * 复制字符串到剪贴板操作(兼容新旧接口) 28 | * @param text 要复制的文本 29 | */ 30 | export const copyToClipboard = (text): Promise => { 31 | return new Promise((resolve, reject) => { 32 | // 如果支持 Clipboard API,就使用它 33 | if (navigator.clipboard && navigator.clipboard.writeText) { 34 | navigator.clipboard 35 | .writeText(text) 36 | .then(() => { 37 | resolve() 38 | }) 39 | .catch((error) => { 40 | reject(error) 41 | }) 42 | } else { 43 | // 使用 document.execCommand 兼容旧 API 44 | const textarea = document.createElement('textarea') 45 | textarea.value = text 46 | textarea.style.display = 'none' 47 | document.body.appendChild(textarea) 48 | textarea.select() 49 | 50 | try { 51 | const success = document.execCommand('copy') 52 | if (!success) { 53 | throw new Error('Unable to perform copy operation') 54 | } else { 55 | resolve() 56 | } 57 | } catch (error) { 58 | reject(error) 59 | } finally { 60 | document.body.removeChild(textarea) 61 | } 62 | } 63 | }) 64 | } 65 | 66 | export const copy = async (val, isShowVal = true) => { 67 | if (!val) { 68 | return 69 | } 70 | if (typeof val === 'object') { 71 | if (isShowVal) { 72 | console.info('object', val) 73 | } 74 | val = JSON.stringify(val, null, 2) 75 | } 76 | if (isShowVal) { 77 | console.info('copy:', val) 78 | } 79 | await copyToClipboard(val) 80 | let showVal = '' 81 | if (isShowVal) { 82 | if (val.length > 350) { 83 | showVal = val.slice(0, 350) + '...' 84 | } else { 85 | showVal = val 86 | } 87 | } 88 | if (showVal) { 89 | showVal = ': ' + showVal 90 | } 91 | window.$notification({ 92 | type: 'success', 93 | message: `Copied${showVal}`, 94 | timeout: 5000, 95 | }) 96 | } 97 | 98 | export const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)) 99 | -------------------------------------------------------------------------------- /src/components/KvmPlayer/utils/keys-enum.ts: -------------------------------------------------------------------------------- 1 | export const ASCII_KEYS = new Map( 2 | [ 3 | // https://gist.github.com/MightyPork/6da26e382a7ad91b5496ee55fdc73db2 4 | ['a', 0x04, 0], 5 | ['A', 0x04, 1], 6 | ['b', 0x05, 0], 7 | ['B', 0x05, 1], 8 | ['c', 0x06, 0], 9 | ['C', 0x06, 1], 10 | ['d', 0x07, 0], 11 | ['D', 0x07, 1], 12 | ['e', 0x08, 0], 13 | ['E', 0x08, 1], 14 | ['f', 0x09, 0], 15 | ['F', 0x09, 1], 16 | ['g', 0x0a, 0], 17 | ['G', 0x0a, 1], 18 | ['h', 0x0b, 0], 19 | ['H', 0x0b, 1], 20 | ['i', 0x0c, 0], 21 | ['I', 0x0c, 1], 22 | ['j', 0x0d, 0], 23 | ['J', 0x0d, 1], 24 | ['k', 0x0e, 0], 25 | ['K', 0x0e, 1], 26 | ['l', 0x0f, 0], 27 | ['L', 0x0f, 1], 28 | ['m', 0x10, 0], 29 | ['M', 0x10, 1], 30 | ['n', 0x11, 0], 31 | ['N', 0x11, 1], 32 | ['o', 0x12, 0], 33 | ['O', 0x12, 1], 34 | ['p', 0x13, 0], 35 | ['P', 0x13, 1], 36 | ['q', 0x14, 0], 37 | ['Q', 0x14, 1], 38 | ['r', 0x15, 0], 39 | ['R', 0x15, 1], 40 | ['s', 0x16, 0], 41 | ['S', 0x16, 1], 42 | ['t', 0x17, 0], 43 | ['T', 0x17, 1], 44 | ['u', 0x18, 0], 45 | ['U', 0x18, 1], 46 | ['v', 0x19, 0], 47 | ['V', 0x19, 1], 48 | ['w', 0x1a, 0], 49 | ['W', 0x1a, 1], 50 | ['x', 0x1b, 0], 51 | ['X', 0x1b, 1], 52 | ['y', 0x1c, 0], 53 | ['Y', 0x1c, 1], 54 | ['z', 0x1d, 0], 55 | ['Z', 0x1d, 1], 56 | ['1', 0x1e, 0], 57 | ['!', 0x1e, 1], 58 | ['2', 0x1f, 0], 59 | ['@', 0x1f, 1], 60 | ['3', 0x20, 0], 61 | ['#', 0x20, 1], 62 | ['4', 0x21, 0], 63 | ['$', 0x21, 1], 64 | ['5', 0x22, 0], 65 | ['%', 0x22, 1], 66 | ['6', 0x23, 0], 67 | ['^', 0x23, 1], 68 | ['7', 0x24, 0], 69 | ['&', 0x24, 1], 70 | ['8', 0x25, 0], 71 | ['*', 0x25, 1], 72 | ['9', 0x26, 0], 73 | ['(', 0x26, 1], 74 | ['0', 0x27, 0], 75 | [')', 0x27, 1], 76 | ['Enter', 0x28, 0], 77 | ['\n', 0x28, 0], 78 | ['Escape', 0x29, 0], 79 | ['Backspace', 0x2a, 0], 80 | ['\b', 0x2a, 0], 81 | ['Tab', 0x2b, 0], 82 | ['\t', 0x2b, 0], 83 | ['Space', 0x2c, 0], 84 | [' ', 0x2c, 0], 85 | ['-', 0x2d, 0], 86 | ['_', 0x2d, 1], 87 | ['=', 0x2e, 0], 88 | ['+', 0x2e, 1], 89 | ['[', 0x2f, 0], 90 | ['{', 0x2f, 1], 91 | [']', 0x30, 0], 92 | ['}', 0x30, 1], 93 | ['\\', 0x31, 0], 94 | ['|', 0x31, 1], 95 | [';', 0x33, 0], 96 | [':', 0x33, 1], 97 | ["'", 0x34, 0], 98 | ['"', 0x34, 1], 99 | ['`', 0x35, 0], 100 | ['~', 0x35, 1], 101 | [',', 0x36, 0], 102 | ['<', 0x36, 1], 103 | ['.', 0x37, 0], 104 | ['>', 0x37, 1], 105 | ['/', 0x38, 0], 106 | ['?', 0x38, 1], 107 | ['CapsLock', 0x39, 0], 108 | ['ScrollLock', 0x47, 0], 109 | ['Pause', 0x48, 0], 110 | ['Insert', 0x49, 0], 111 | ['Home', 0x4a, 0], 112 | ['PageUp', 0x4b, 0], 113 | ['Delete', 0x4c, 0], 114 | ['End', 0x4d, 0], 115 | ['PageDown', 0x4e, 0], 116 | ['ArrowRight', 0x4f, 0], 117 | ['ArrowLeft', 0x50, 0], 118 | ['ArrowDown', 0x51, 0], 119 | ['ArrowUp', 0x52, 0], 120 | ['NumLock', 0x53, 0], 121 | ['F1', 0x3a, 0], 122 | ['F2', 0x3b, 0], 123 | ['F3', 0x3c, 0], 124 | ['F4', 0x3d, 0], 125 | ['F5', 0x3e, 0], 126 | ['F6', 0x3f, 0], 127 | ['F7', 0x40, 0], 128 | ['F8', 0x41, 0], 129 | ['F9', 0x42, 0], 130 | ['F10', 0x43, 0], 131 | ['F11', 0x44, 0], 132 | ['F12', 0x45, 0], 133 | ['Control', 0xe0, 0], 134 | ['Shift', 0xe1, 0], 135 | ['Alt', 0xe2, 0], 136 | ['Meta', 0xe3, 0], 137 | ].map(([key, hidCode, shift]) => [key, [hidCode, shift]]), 138 | ) 139 | -------------------------------------------------------------------------------- /src/components/KvmPlayer/utils/qrcode-decoder.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * https://github.com/yugasun/qrcode-decoder/blob/master/src/index.ts 3 | */ 4 | 5 | import jsQR, {QRCode, Options} from 'jsqr' 6 | 7 | export type CodeResult = QRCode | null 8 | 9 | const videoSize = { 10 | width: {min: 360, ideal: 720, max: 1080}, 11 | height: {min: 360, ideal: 720, max: 1080}, 12 | } 13 | 14 | export class QrcodeDecoder { 15 | timerCapture: null | NodeJS.Timeout 16 | canvasElem: null | HTMLCanvasElement 17 | gCtx: null | CanvasRenderingContext2D 18 | stream: null | MediaStream 19 | videoElem: null | HTMLVideoElement 20 | getUserMediaHandler: null 21 | videoConstraints: MediaStreamConstraints 22 | defaultOption: Options 23 | 24 | constructor() { 25 | this.timerCapture = null 26 | this.canvasElem = null 27 | this.gCtx = null 28 | this.stream = null 29 | this.videoElem = null 30 | this.getUserMediaHandler = null 31 | this.videoConstraints = { 32 | // default use rear camera 33 | video: {...videoSize, facingMode: {exact: 'environment'}}, 34 | audio: false, 35 | } 36 | 37 | this.defaultOption = {inversionAttempts: 'attemptBoth'} 38 | } 39 | 40 | /** 41 | * Verifies if canvas element is supported. 42 | */ 43 | isCanvasSupported() { 44 | const elem = document.createElement('canvas') 45 | return !!(elem.getContext && elem.getContext('2d')) 46 | } 47 | 48 | _createImageData(target: CanvasImageSource, width: number, height: number) { 49 | if (!this.canvasElem) { 50 | this._prepareCanvas(width, height) 51 | } 52 | 53 | this.gCtx!.clearRect(0, 0, width, height) 54 | this.gCtx!.drawImage(target, 0, 0, width, height) 55 | 56 | const imageData = this.gCtx!.getImageData(0, 0, this.canvasElem!.width, this.canvasElem!.height) 57 | 58 | return imageData 59 | } 60 | 61 | /** 62 | * Prepares the canvas element (which will 63 | * receive the image from the camera and provide 64 | * what the algorithm needs for checking for a 65 | * QRCode and then decoding it.) 66 | * 67 | * 68 | * @param {DOMElement} canvasElem the canvas 69 | * element 70 | * @param {number} width The width that 71 | * the canvas element 72 | * should have 73 | * @param {number} height The height that 74 | * the canvas element 75 | * should have 76 | * @return {DOMElement} the canvas 77 | * after the resize if width and height 78 | * provided. 79 | */ 80 | _prepareCanvas(width: number, height: number) { 81 | if (!this.canvasElem) { 82 | this.canvasElem = document.createElement('canvas') 83 | this.canvasElem.style.width = `${width}px` 84 | this.canvasElem.style.height = `${height}px` 85 | this.canvasElem.width = width 86 | this.canvasElem.height = height 87 | } 88 | 89 | this.gCtx = this.canvasElem.getContext('2d') 90 | } 91 | 92 | /** 93 | * Based on the video dimensions and the canvas 94 | * that was previously generated captures the 95 | * video/image source and then paints into the 96 | * canvas so that the decoder is able to work as 97 | * it expects. 98 | * @param {DOMElement} videoElem