├── .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 | 
19 | 
20 | 
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 | [](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 | 
19 | 
20 | 
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 | [](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 |
9 |
10 |
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 |
681 |
682 |
690 |
691 |
694 |
695 |
696 |
714 |
715 |
733 |
734 |
754 |
755 |
756 |
757 |
758 |
763 |
--------------------------------------------------------------------------------
/src/components/KvmPlayer/QRScanner.vue:
--------------------------------------------------------------------------------
1 |
116 |
117 |
118 |
127 |
128 |
129 |
136 |
--------------------------------------------------------------------------------
/src/components/KvmPlayer/SettingsPrompt.vue:
--------------------------------------------------------------------------------
1 |
51 |
52 |
53 |
54 |
219 |
220 |
221 |
222 |
275 |
--------------------------------------------------------------------------------
/src/components/KvmPlayer/TauriActions.vue:
--------------------------------------------------------------------------------
1 |
18 |
19 |
20 |
24 |
32 |
33 |
--------------------------------------------------------------------------------
/src/components/KvmPlayer/UI/DragButton.vue:
--------------------------------------------------------------------------------
1 |
15 |
16 |
17 |
33 |
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 |
380 |
613 |
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