├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ └── main.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── FUNDING.yml ├── LICENSE ├── README.md ├── SECURITY.md ├── Taskfile.yml ├── assets ├── How_to_use_LibreRemotePlay.mp4 ├── How_to_use_LibreRemotePlay.webm ├── example.png ├── gamepad.png ├── gamepad.svg ├── libreremoteplaybanner.jpg └── libreremoteplaybanner.kra ├── docs ├── BACKEND.md ├── FRONTEND.md ├── GamepadConversionTable.xlsx ├── LINUX.md ├── README.md ├── SEO.xlsx ├── app_explanation_diagram.excalidraw ├── app_explanation_diagram.svg ├── network_explanation_diagram.excalidraw └── network_explanation_diagram.svg ├── frontend ├── .eslintignore ├── .eslintrc.cjs ├── .gitignore ├── .npmrc ├── .prettierignore ├── .prettierrc ├── .vscode │ └── settings.json ├── README.md ├── cypress.config.ts ├── cypress │ ├── e2e │ │ └── config │ │ │ ├── basic_conn.cy.ts │ │ │ └── tutorial.cy.ts │ ├── fixtures │ │ └── example.json │ ├── support │ │ ├── commands.ts │ │ └── e2e.ts │ └── tsconfig.json ├── package-lock.json ├── package.json ├── package.json.md5 ├── pnpm-lock.yaml ├── postcss.config.js ├── src │ ├── app.css │ ├── app.d.ts │ ├── app.html │ ├── lib │ │ ├── assets │ │ │ └── gamepad.svg │ │ ├── audio │ │ │ └── audio_player.ts │ │ ├── css │ │ │ └── media-video.css │ │ ├── detection │ │ │ ├── IsLinux.svelte │ │ │ ├── IsWindows.svelte │ │ │ ├── detect_os.ts │ │ │ └── onwebsite.ts │ │ ├── easy_connect │ │ │ └── easy_connect.svelte.ts │ │ ├── gamepad │ │ │ └── gamepad_hook.ts │ │ ├── i18n │ │ │ ├── LanguageSelector.svelte │ │ │ ├── en.json │ │ │ ├── es.json │ │ │ ├── fr.json │ │ │ ├── gl.json │ │ │ ├── i18n.ts │ │ │ └── ru.json │ │ ├── keyboard │ │ │ └── keyboard_hook.ts │ │ ├── layout │ │ │ ├── BackwardButton.svelte │ │ │ ├── PageTransition.svelte │ │ │ ├── icons │ │ │ │ ├── DeleteIcon.svelte │ │ │ │ ├── GamepadIcon.svelte │ │ │ │ ├── KeyboardIcon.svelte │ │ │ │ ├── PencilIcon.svelte │ │ │ │ └── TrashIcon.svelte │ │ │ └── useSortable.svelte.ts │ │ ├── loading │ │ │ ├── Loading.svelte │ │ │ └── loading_hook.ts │ │ ├── logger │ │ │ └── logger.ts │ │ ├── toast │ │ │ ├── Toast.svelte │ │ │ └── toast_hook.ts │ │ ├── tutorial │ │ │ └── driver.ts │ │ ├── wailsjs │ │ │ ├── go │ │ │ │ ├── bindings │ │ │ │ │ ├── App.d.ts │ │ │ │ │ └── App.js │ │ │ │ └── models.ts │ │ │ └── runtime │ │ │ │ ├── package.json │ │ │ │ ├── runtime.d.ts │ │ │ │ └── runtime.js │ │ ├── webrtc │ │ │ ├── ICEServerManager.svelte │ │ │ ├── client_webrtc_hook.ts │ │ │ ├── host_webrtc_hook.ts │ │ │ ├── ice.ts │ │ │ ├── stream │ │ │ │ ├── CodecList.svelte │ │ │ │ ├── client_stream_hook.ts │ │ │ │ ├── host_stream_hook.ts │ │ │ │ ├── stream_config.svelte.ts │ │ │ │ └── stream_signal_hook.svelte.ts │ │ │ ├── stun_servers.ts │ │ │ └── turn_servers.ts │ │ └── websocket │ │ │ └── ws.ts │ ├── routes │ │ ├── +error.svelte │ │ ├── +layout.svelte │ │ ├── +layout.ts │ │ ├── +page.svelte │ │ └── mode │ │ │ ├── +layout.svelte │ │ │ ├── client │ │ │ ├── +layout.svelte │ │ │ ├── +page.svelte │ │ │ └── connection │ │ │ │ └── +page.svelte │ │ │ ├── config │ │ │ ├── +page.svelte │ │ │ ├── StunServers.svelte │ │ │ ├── TurnServers.svelte │ │ │ ├── ViGEmDownload.svelte │ │ │ └── advanced │ │ │ │ ├── +layout.svelte │ │ │ │ ├── stun │ │ │ │ └── +page.svelte │ │ │ │ └── turn │ │ │ │ └── +page.svelte │ │ │ └── host │ │ │ ├── +page.svelte │ │ │ └── connection │ │ │ └── +page.svelte │ └── service-worker.js ├── static │ ├── AppImages.zip │ ├── AppImages │ │ ├── android │ │ │ ├── android-launchericon-144-144.png │ │ │ ├── android-launchericon-192-192.png │ │ │ ├── android-launchericon-48-48.png │ │ │ ├── android-launchericon-512-512.png │ │ │ ├── android-launchericon-72-72.png │ │ │ └── android-launchericon-96-96.png │ │ ├── icons.json │ │ ├── ios │ │ │ ├── 100.png │ │ │ ├── 1024.png │ │ │ ├── 114.png │ │ │ ├── 120.png │ │ │ ├── 128.png │ │ │ ├── 144.png │ │ │ ├── 152.png │ │ │ ├── 16.png │ │ │ ├── 167.png │ │ │ ├── 180.png │ │ │ ├── 192.png │ │ │ ├── 20.png │ │ │ ├── 256.png │ │ │ ├── 29.png │ │ │ ├── 32.png │ │ │ ├── 40.png │ │ │ ├── 50.png │ │ │ ├── 512.png │ │ │ ├── 57.png │ │ │ ├── 58.png │ │ │ ├── 60.png │ │ │ ├── 64.png │ │ │ ├── 72.png │ │ │ ├── 76.png │ │ │ ├── 80.png │ │ │ └── 87.png │ │ └── windows11 │ │ │ ├── LargeTile.scale-100.png │ │ │ ├── LargeTile.scale-125.png │ │ │ ├── LargeTile.scale-150.png │ │ │ ├── LargeTile.scale-200.png │ │ │ ├── LargeTile.scale-400.png │ │ │ ├── SmallTile.scale-100.png │ │ │ ├── SmallTile.scale-125.png │ │ │ ├── SmallTile.scale-150.png │ │ │ ├── SmallTile.scale-200.png │ │ │ ├── SmallTile.scale-400.png │ │ │ ├── SplashScreen.scale-100.png │ │ │ ├── SplashScreen.scale-125.png │ │ │ ├── SplashScreen.scale-150.png │ │ │ ├── SplashScreen.scale-200.png │ │ │ ├── SplashScreen.scale-400.png │ │ │ ├── Square150x150Logo.scale-100.png │ │ │ ├── Square150x150Logo.scale-125.png │ │ │ ├── Square150x150Logo.scale-150.png │ │ │ ├── Square150x150Logo.scale-200.png │ │ │ ├── Square150x150Logo.scale-400.png │ │ │ ├── Square44x44Logo.altform-lightunplated_targetsize-16.png │ │ │ ├── Square44x44Logo.altform-lightunplated_targetsize-20.png │ │ │ ├── Square44x44Logo.altform-lightunplated_targetsize-24.png │ │ │ ├── Square44x44Logo.altform-lightunplated_targetsize-256.png │ │ │ ├── Square44x44Logo.altform-lightunplated_targetsize-30.png │ │ │ ├── Square44x44Logo.altform-lightunplated_targetsize-32.png │ │ │ ├── Square44x44Logo.altform-lightunplated_targetsize-36.png │ │ │ ├── Square44x44Logo.altform-lightunplated_targetsize-40.png │ │ │ ├── Square44x44Logo.altform-lightunplated_targetsize-44.png │ │ │ ├── Square44x44Logo.altform-lightunplated_targetsize-48.png │ │ │ ├── Square44x44Logo.altform-lightunplated_targetsize-60.png │ │ │ ├── Square44x44Logo.altform-lightunplated_targetsize-64.png │ │ │ ├── Square44x44Logo.altform-lightunplated_targetsize-72.png │ │ │ ├── Square44x44Logo.altform-lightunplated_targetsize-80.png │ │ │ ├── Square44x44Logo.altform-lightunplated_targetsize-96.png │ │ │ ├── Square44x44Logo.altform-unplated_targetsize-16.png │ │ │ ├── Square44x44Logo.altform-unplated_targetsize-20.png │ │ │ ├── Square44x44Logo.altform-unplated_targetsize-24.png │ │ │ ├── Square44x44Logo.altform-unplated_targetsize-256.png │ │ │ ├── Square44x44Logo.altform-unplated_targetsize-30.png │ │ │ ├── Square44x44Logo.altform-unplated_targetsize-32.png │ │ │ ├── Square44x44Logo.altform-unplated_targetsize-36.png │ │ │ ├── Square44x44Logo.altform-unplated_targetsize-40.png │ │ │ ├── Square44x44Logo.altform-unplated_targetsize-44.png │ │ │ ├── Square44x44Logo.altform-unplated_targetsize-48.png │ │ │ ├── Square44x44Logo.altform-unplated_targetsize-60.png │ │ │ ├── Square44x44Logo.altform-unplated_targetsize-64.png │ │ │ ├── Square44x44Logo.altform-unplated_targetsize-72.png │ │ │ ├── Square44x44Logo.altform-unplated_targetsize-80.png │ │ │ ├── Square44x44Logo.altform-unplated_targetsize-96.png │ │ │ ├── Square44x44Logo.scale-100.png │ │ │ ├── Square44x44Logo.scale-125.png │ │ │ ├── Square44x44Logo.scale-150.png │ │ │ ├── Square44x44Logo.scale-200.png │ │ │ ├── Square44x44Logo.scale-400.png │ │ │ ├── Square44x44Logo.targetsize-16.png │ │ │ ├── Square44x44Logo.targetsize-20.png │ │ │ ├── Square44x44Logo.targetsize-24.png │ │ │ ├── Square44x44Logo.targetsize-256.png │ │ │ ├── Square44x44Logo.targetsize-30.png │ │ │ ├── Square44x44Logo.targetsize-32.png │ │ │ ├── Square44x44Logo.targetsize-36.png │ │ │ ├── Square44x44Logo.targetsize-40.png │ │ │ ├── Square44x44Logo.targetsize-44.png │ │ │ ├── Square44x44Logo.targetsize-48.png │ │ │ ├── Square44x44Logo.targetsize-60.png │ │ │ ├── Square44x44Logo.targetsize-64.png │ │ │ ├── Square44x44Logo.targetsize-72.png │ │ │ ├── Square44x44Logo.targetsize-80.png │ │ │ ├── Square44x44Logo.targetsize-96.png │ │ │ ├── StoreLogo.scale-100.png │ │ │ ├── StoreLogo.scale-125.png │ │ │ ├── StoreLogo.scale-150.png │ │ │ ├── StoreLogo.scale-200.png │ │ │ ├── StoreLogo.scale-400.png │ │ │ ├── Wide310x150Logo.scale-100.png │ │ │ ├── Wide310x150Logo.scale-125.png │ │ │ ├── Wide310x150Logo.scale-150.png │ │ │ ├── Wide310x150Logo.scale-200.png │ │ │ └── Wide310x150Logo.scale-400.png │ ├── favicon.png │ ├── gamepad.svg │ ├── manifest.json │ ├── sounds │ │ ├── open_modal.mp3 │ │ ├── open_modal.wav │ │ ├── page_transition.mp3 │ │ └── page_transition.wav │ └── wasm │ │ ├── signal.go │ │ ├── signal.wasm │ │ └── wasm_exec.js ├── svelte.config.js ├── tailwind.config.js ├── tsconfig.json └── vite.config.ts ├── go.mod ├── go.sum ├── go.work ├── go.work.sum ├── main.go ├── src ├── bin │ ├── ViGEmBus_1.22.0_x64_x86_arm64.exe │ ├── ViGEmClient_x64.dll │ ├── ViGEmClient_x86.dll │ └── bin_windows.go ├── bindings │ ├── app.go │ ├── app_darwin.go │ ├── app_linux.go │ └── app_windows.go ├── devices │ ├── device.go │ ├── gamepad │ │ ├── common.go │ │ ├── handler_unix.go │ │ ├── handler_windows.go │ │ ├── vigem_emulate_windows.go │ │ ├── vigemwraper_windows.go │ │ └── xinput_windows.go │ └── keyboard │ │ ├── handler.go │ │ └── map_js_keys_go.go ├── logger │ └── logger.go ├── net │ ├── http │ │ └── http_linux.go │ ├── webrtc │ │ ├── host.go │ │ ├── signal.go │ │ └── streaming_signal │ │ │ ├── handler_darwin.go │ │ │ ├── handler_linux.go │ │ │ └── handler_windows.go │ └── websocket │ │ └── websocket_linux.go └── oninit │ ├── oninit_darwin.go │ ├── oninit_linux.go │ └── oninit_windows.go └── wails.json /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Version [e.g. 22] 29 | 30 | **Browser (please complete the following information):** 31 | - Device: [e.g. iPhone6] 32 | - OS: [e.g. iOS8.1] 33 | - Browser [e.g. stock browser, safari] 34 | - Version [e.g. 22] 35 | 36 | **Additional context** 37 | Add any other context about the problem here. 38 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Wails build 2 | 3 | on: 4 | push: 5 | tags: 6 | # Match any new tag 7 | - '*' 8 | 9 | env: 10 | # Necessary for most environments as build failure can occur due to OOM issues 11 | NODE_OPTIONS: "--max-old-space-size=4096" 12 | 13 | jobs: 14 | build: 15 | strategy: 16 | # Failure in one platform build won't impact the others 17 | fail-fast: false 18 | matrix: 19 | build: 20 | - name: 'RemoteControllerLinux' 21 | platform: 'linux/amd64' 22 | os: 'ubuntu-latest' 23 | - name: 'RemoteControllerWindows' 24 | platform: 'windows/amd64' 25 | os: 'windows-latest' 26 | - name: 'RemoteControllerDarwin' 27 | platform: 'darwin/universal' 28 | os: 'macos-latest' 29 | 30 | runs-on: ${{ matrix.build.os }} 31 | steps: 32 | - name: Checkout 33 | uses: actions/checkout@v2 34 | with: 35 | submodules: recursive 36 | 37 | - name: Build wails 38 | uses: dAppServer/wails-build-action@v2.2 39 | id: build 40 | with: 41 | build-name: ${{ matrix.build.name }} 42 | build-platform: ${{ matrix.build.platform }} 43 | package: false 44 | go-version: '1.22' 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | /*.exe 3 | /*.dll 4 | .vigemsetup 5 | LibreRemotePlay.log -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Structure of the project 4 | 5 | - src/ (Golang / Wails) 6 | - frontend/ ( Typescript / Sveltekit - All the UI for the Desktop APP & the Web version) 7 | - docs/ (All the development documentation) 8 | - frontend/src/lib/i18n (All the translations of the project) 9 | 10 | ## Requisites 11 | - If you want to contribute first you need to check the issues, them if you like any of the open issues work on it and merge it to the project (obviously you can open a new issue to enhance the features or correct any bug you found to work on it later) 12 | - Try to do self-explanatory code (if cannot be you can comment to enhance the comprehension) 13 | 14 | ## Resources of interest 15 | 16 | - [LibreRemotePlay Docs](./docs/README.md) 17 | - [How to run the project](./README.md#run-dev) 18 | - [How to build the project](./README.md#build) 19 | 20 | ## How to 21 | 22 | 1. Fork this repository 23 | 2. Clone it 24 | 3. Work on the issue 25 | 4. When you have finished make a pull request to merge it with the main branch 26 | 5. Wait for merge (maybe it will not be merged at first because of bad code) 27 | 6. Done 28 | 29 | ## Translations 🔠 30 | 31 | ### How to 32 | 33 | 1. Fork this repository 34 | 2. Clone it 35 | 3. Work on your translations (located in frontend/src/lib/i18n): 36 | - Create a JSON file of the language and register the language in the i18n.ts file (all of this if the language is not added already) 37 | - Add the entries (you can do manually or using [i18n Ally extension](https://marketplace.visualstudio.com/items?itemName=Lokalise.i18n-ally)) 38 | 5. When you have finished make a pull request to merge it with the main branch 39 | 6. Wait for merge 40 | 7. Done 41 | 42 | 43 | ## Thank you for reading this and also for your interest on contributing 44 | -------------------------------------------------------------------------------- /FUNDING.yml: -------------------------------------------------------------------------------- 1 | ko_fi: piterdev 2 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Due to a full rework of the APP only 2.x and newer are considered to be secure 6 | 7 | | Version | Supported | 8 | | ------- | ------------------ | 9 | | 3.x | :white_check_mark: | 10 | | 2.x | :white_check_mark: | 11 | | 1.x | :x: | 12 | 13 | ## Reporting a Vulnerability 14 | 15 | If you want to report a vulnerability mail me at piterzdev@gmail or open an issue or create a new discussion 16 | -------------------------------------------------------------------------------- /Taskfile.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | tasks: 4 | build-front: 5 | dir: frontend 6 | cmds: 7 | - pnpm install 8 | - pnpm run build 9 | desc: Build the frontend 10 | build: 11 | deps: [build-front] 12 | cmds: 13 | - wails build -s -platform=windows/amd64,windows/arm64,linux/amd64,linux/arm64 14 | desc: Build the application for all platforms 15 | build-win: 16 | deps: [build-front] 17 | cmds: 18 | - wails build -s -platform=windows/amd64,windows/arm64 19 | desc: Build the application for Windows 20 | build-debug-win: 21 | deps: [build-front] 22 | cmds: 23 | - wails build -debug -s -platform=windows/amd64,windows/arm64 24 | desc: Build the debug application for Windows 25 | build-linux: 26 | deps: [build-front] 27 | cmds: 28 | - wails build -s -platform=linux/amd64,linux/arm64 29 | desc: Build the application for Linux 30 | build-debug-linux: 31 | deps: [build-front] 32 | cmds: 33 | - wails build -debug -s -platform=linux/amd64,linux/arm64 34 | desc: Build the debug application for Linux 35 | build-wasm-front-linux: 36 | dir: frontend/static/wasm 37 | cmds: 38 | - GOOS=js GOARCH=wasm go build -o signal.wasm 39 | desc: Build the frontend wasm for Linux 40 | build-wasm-front-win: 41 | dir: frontend/static/wasm 42 | cmds: 43 | - powershell.exe -Command { $env:GOOS="js";$env:GOARCH="wasm"; go build -o signal.wasm } 44 | desc: Build the frontend wasm for Windows 45 | dev-all: 46 | deps: [build-front] 47 | cmds: 48 | - wails dev 49 | desc: Run the application in development mode 50 | dev-front: 51 | dir: frontend 52 | deps: [build-front] 53 | cmds: 54 | - pnpm run dev 55 | desc: Run the frontend in development mode 56 | test: 57 | desc: Run E2E tests 58 | dir: frontend 59 | deps: [dev-all] 60 | cmds: 61 | - pnpm run test -------------------------------------------------------------------------------- /assets/How_to_use_LibreRemotePlay.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/assets/How_to_use_LibreRemotePlay.mp4 -------------------------------------------------------------------------------- /assets/How_to_use_LibreRemotePlay.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/assets/How_to_use_LibreRemotePlay.webm -------------------------------------------------------------------------------- /assets/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/assets/example.png -------------------------------------------------------------------------------- /assets/gamepad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/assets/gamepad.png -------------------------------------------------------------------------------- /assets/gamepad.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Created with Fabric.js 3.6.6 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /assets/libreremoteplaybanner.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/assets/libreremoteplaybanner.jpg -------------------------------------------------------------------------------- /assets/libreremoteplaybanner.kra: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/assets/libreremoteplaybanner.kra -------------------------------------------------------------------------------- /docs/BACKEND.md: -------------------------------------------------------------------------------- 1 | # 🚧 Working On ... 2 | -------------------------------------------------------------------------------- /docs/FRONTEND.md: -------------------------------------------------------------------------------- 1 | # 🚧 Working On ... -------------------------------------------------------------------------------- /docs/GamepadConversionTable.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/docs/GamepadConversionTable.xlsx -------------------------------------------------------------------------------- /docs/LINUX.md: -------------------------------------------------------------------------------- 1 | # Linux 2 | 3 | ## Execute in Linux 4 | 5 | When you run LibreRemotePlay you will need to be sure to: 6 | 7 | - Have a compatible default web browser, any chromium based browser up to date should work. (There are known issues with Firefox) 8 | 9 | - Your user has read/write permissions for /dev/input/event/* and uinput devices 10 | - Example in Debian: 11 | ```sh 12 | sudo usermod -aG input $USER 13 | ``` 14 | - Uinput module enabled 15 | - Check if it is loaded: 16 | - Example in Debian: 17 | ```sh 18 | lsmod | grep uinput 19 | ``` 20 | - Load the module: 21 | - Example in Debian: 22 | ```sh 23 | sudo modprobe uinput 24 | ``` 25 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Docs 📘 2 | 3 | ## Network related 4 | 5 | ![Network exaplanation](./network_explanation_diagram.svg) 6 | 7 | ## APP Architecture 8 | 9 | ![APP Architecture](./app_explanation_diagram.svg) 10 | 11 | ## General 12 | 13 | LibreRemotePlay uses web technologies like WebRTC and MediaDevices (displayMedia). 14 | 15 | WebRTC is totally supported by all main desktop/mobile browsers and is also available in different languages (Go included) 16 | 17 | The purpose of WebRTC is to make a P2P connection between Host and Client devices to send Gamepad Input using data channels and also captured Video/Audio with media channels. 18 | 19 | DisplayMedia is for capturing video/audio from desktop/aplications and them stream it through WebRTC media channel. 20 | 21 | ### Wails 22 | 23 | This project is using Wails so it might be important to know how wails works. 24 | 25 | Wails is like Electron but for Go, and instead of embed a Chromium Browser you will use the existent browser of you OS (webview2, gtkwebview, ...). 26 | 27 | You will have two parts: 28 | - "Browser": 29 | This is what runs HTML, CSS, JS, TS, WASM (provides the UI) 30 | - "Desktop APP": 31 | This is what runs Go code 32 | 33 | #### Bindings 34 | 35 | Wails can generate bindings in JS for Go generated functions.
36 | 37 | In the project all bindings are located in [/src/desktop/](../src/desktop/) 38 | 39 | Real example: 40 | 41 | This function located in [/src/desktop/app.go](../src/desktop/app.go) 42 | 43 | ```go 44 | func (a *App) GetCurrentOS() string { 45 | return strings.ToUpper(runtime.GOOS) 46 | } 47 | ``` 48 | will appear as 49 | 50 | ```js 51 | export function GetCurrentOS() { 52 | return window['go']['desktop']['App']['GetCurrentOS'](); 53 | } 54 | ``` 55 | 56 | in the path [/frontend/src/lib/wailsjs/go/desktop/App.js](../frontend/src/lib/wailsjs/go/desktop/App.js) 57 | 58 | #### Events 59 | 60 | Wails have listeners and event dispatchers to send data between JS <-> GO in a bidirectional flow (this are contained in the wails runtime pkg) 61 | 62 | ## Roles 63 | 64 | First make sure to read the Wails section above. 65 | 66 | We are going to start with ¿ How the two roles communicate ?. 67 | 68 | To not enter in WebRTC matery we are going to say that each peer needs to need some information from the other after the connection begins so we need to pass that information.

69 | To do that we share codes, this codes are simply the data needed by WebRTC but encoded and compressed to be the most portable it can, to not require the use of a signaling server. This way you will not have to self-host any serice. 70 | 71 | Note: all the "codes" encoding & compression is done in Go or Wasm (generated from the Go code). 72 | 73 | ### Client 74 | 75 | Client code is only located on the "Browser" (JS/TS). 76 | 77 | The logic is very simple. The client creates a WebRTC connection with the host and through the Gamepad API of the "Browser" gets the gamepad data, later we copy the structure and send using a WebRTC Datachannel. 78 | If there is an available Screen + Audio stream we can connect to it creating a new WebRTC connection and using the previous as signaling server. The render of the stream is all done using Web APIs. 79 | 80 | ### Host 81 | 82 | Host is the most complex role cause part of the logic of webrtc code is on the "Browser" and other in "Desktop APP". 83 | 84 | This division of logic is because we need: 85 | - Go: A WebRTC connection that goes directly to the ViGEm driver (which is loaded as a DLL in Go) to insert the gamepad data or a similar situation with keyboard. 86 | 87 | - JS/TS: We need to send the Screen + Audio stream to the client. To do that we need the WebRTC stream in the "Browser", one way of achieve this could have been doing a rtp stream proxy and use the Go WebRTC connection but that would have add latency and use of more resources. Because of that it is created a new WebRTC connection only for the stream on the "Browser", this connection is auto created and uses as signaling service the normal WebRTC connection used for Gamepad data. 88 | 89 | 90 | ## Frontend (Browser) 91 | 92 | Source code location : [/frontend/](../frontend) 93 | 94 | WebRTC code: [/frontend/src/lib/webrtc/](../frontend/src/lib/webrtc) 95 | 96 | ### [Frontend Docs](./FRONTEND.md) 97 | 98 | ### Stack: 99 | 100 | - 💻 Sveltekit (UI Framework) 101 | - ✨ Tailwind (CSS Framework) 102 | - 💅 DaisyUI (Tailwind Framework) 103 | - 🔢 WASM (for Go compatibility) 104 | - 🔠 Svelte i18n (Translations) 105 | - 📦 Wails (Golang bindings for desktop) 106 | 107 | ## Backend (Dekstop APP Logic) 108 | 109 | Source code location: [/main.go](../main.go) + [/src/](../src) 110 | 111 | WebRTC code: [/src/net/](../src/net) + [/src/streaming_signal/](../src/streaming_signal/) 112 | 113 | ### [Backend Docs](./BACKEND.md) 114 | 115 | ### Stack: 116 | 117 | - 💻 Go 118 | - 📦 Wails (Desktop APP) 119 | - 🌐 Pion/Webrtc 120 | - 🎮 ViGEm (binary & dll for gamepad virtualization) 121 | -------------------------------------------------------------------------------- /docs/SEO.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/docs/SEO.xlsx -------------------------------------------------------------------------------- /frontend/.eslintignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | 10 | # Ignore files for PNPM, NPM and YARN 11 | pnpm-lock.yaml 12 | package-lock.json 13 | yarn.lock 14 | -------------------------------------------------------------------------------- /frontend/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: [ 4 | 'eslint:recommended', 5 | 'plugin:@typescript-eslint/recommended', 6 | 'plugin:svelte/recommended', 7 | 'prettier' 8 | ], 9 | parser: '@typescript-eslint/parser', 10 | plugins: ['@typescript-eslint'], 11 | parserOptions: { 12 | sourceType: 'module', 13 | ecmaVersion: 2020, 14 | extraFileExtensions: ['.svelte'] 15 | }, 16 | env: { 17 | browser: true, 18 | es2017: true, 19 | node: true 20 | }, 21 | overrides: [ 22 | { 23 | files: ['*.svelte'], 24 | parser: 'svelte-eslint-parser', 25 | parserOptions: { 26 | parser: '@typescript-eslint/parser' 27 | } 28 | } 29 | ] 30 | }; 31 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | vite.config.js.timestamp-* 10 | vite.config.ts.timestamp-* -------------------------------------------------------------------------------- /frontend/.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=false 2 | lockfile=true 3 | prefer-frozen-lockfile=true -------------------------------------------------------------------------------- /frontend/.prettierignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | 10 | # Ignore files for PNPM, NPM and YARN 11 | pnpm-lock.yaml 12 | package-lock.json 13 | yarn.lock 14 | -------------------------------------------------------------------------------- /frontend/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": true, 3 | "singleQuote": true, 4 | "trailingComma": "none", 5 | "printWidth": 100, 6 | "plugins": ["prettier-plugin-svelte"], 7 | "pluginSearchDirs": ["."], 8 | "overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }] 9 | } 10 | -------------------------------------------------------------------------------- /frontend/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "i18n-ally.localesPaths": ["src/lib/i18n"], 3 | "i18n-ally.keystyle": "nested", 4 | "i18n-ally.sourceLanguage": "en" 5 | } -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | # create-svelte 2 | 3 | Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/master/packages/create-svelte). 4 | 5 | ## Creating a project 6 | 7 | If you're seeing this, you've probably already done this step. Congrats! 8 | 9 | ```bash 10 | # create a new project in the current directory 11 | npm create svelte@latest 12 | 13 | # create a new project in my-app 14 | npm create svelte@latest my-app 15 | ``` 16 | 17 | ## Developing 18 | 19 | Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server: 20 | 21 | ```bash 22 | npm run dev 23 | 24 | # or start the server and open the app in a new browser tab 25 | npm run dev -- --open 26 | ``` 27 | 28 | ## Building 29 | 30 | To create a production version of your app: 31 | 32 | ```bash 33 | npm run build 34 | ``` 35 | 36 | You can preview the production build with `npm run preview`. 37 | 38 | > To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment. 39 | -------------------------------------------------------------------------------- /frontend/cypress.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | import { defineConfig } from "cypress"; 3 | 4 | export default defineConfig({ 5 | e2e: { 6 | experimentalStudio: true, 7 | setupNodeEvents(on, config) { 8 | 9 | // implement node event listeners here 10 | }, 11 | }, 12 | }); 13 | -------------------------------------------------------------------------------- /frontend/cypress/e2e/config/basic_conn.cy.ts: -------------------------------------------------------------------------------- 1 | import * as MockRTC from 'mockrtc'; 2 | import * as fs from 'fs'; 3 | 4 | const adminServer = MockRTC.getAdminServer(); 5 | adminServer.start().then(() => console.log('WebRTC Admin server started')); 6 | 7 | const mockRTC = MockRTC.getRemote({ recordMessages: true, debug: true }); 8 | 9 | const signalWasmBuffer = fs.readFileSync('../../../static/wasm/signal.wasm'); 10 | 11 | describe('Basic connection', async () => { 12 | const wasmModule = await WebAssembly.instantiate(signalWasmBuffer); 13 | 14 | const signalEncode = wasmModule.instance.exports.signalEncode as (signal: T) => string; 15 | const signalDecode = wasmModule.instance.exports.signalDecode as (signal: string) => T; 16 | 17 | beforeEach(() => mockRTC.start()); 18 | afterEach(() => mockRTC.stop()); 19 | 20 | it('load', async () => { 21 | cy.visit('http://localhost:34115/').wait(1000).log('hello'); 22 | 23 | const mockPeer = await mockRTC.buildPeer().thenEcho(); 24 | 25 | const { offer: mockOffer, setAnswer } = await mockPeer.createOffer(); 26 | 27 | const mockOfferEncoded = signalEncode(mockOffer) 28 | 29 | // const answerEncoded = "" 30 | 31 | // const answer: RTCSessionDescriptionInit = signalDecode(answerEncoded) 32 | 33 | // await setAnswer(answer) 34 | 35 | // Start WebRTC connection from the CypressUI 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /frontend/cypress/e2e/config/tutorial.cy.ts: -------------------------------------------------------------------------------- 1 | describe('Tutorial flow', () => { 2 | it('Load main page ', () => { 3 | cy.visit('http://localhost:34115/'); 4 | }); 5 | 6 | it('tutorial', () => { 7 | 8 | cy.visit('http://localhost:34115/'); 9 | cy.get('button.btn').click(); 10 | cy.get('button.driver-popover-next-btn').click(); 11 | cy.location('pathname').should('equal', '/mode/config'); 12 | cy.wait(1000); 13 | cy.get('.grid > :nth-child(5) > .btn').click(); 14 | cy.get(':nth-child(3) > .btn').click(); 15 | cy.get(':nth-child(4) > .btn').click(); 16 | cy.get('.grid > :nth-child(1) > .btn').click(); 17 | cy.get('.grid > :nth-child(2) > .btn').click(); 18 | cy.get('button.driver-popover-next-btn').click(); 19 | cy.wait(1000); 20 | cy.get('button.driver-popover-next-btn').click(); 21 | cy.location('pathname').should('equal', '/mode/config/advanced/stun'); 22 | cy.wait(1000); 23 | cy.get('button.driver-popover-next-btn').click(); 24 | cy.wait(1000); 25 | cy.get('button.driver-popover-next-btn').click(); 26 | cy.wait(1000); 27 | cy.location('pathname').should('equal', '/mode/config'); 28 | cy.get('button.driver-popover-next-btn').click(); 29 | cy.wait(1000); 30 | cy.get('button.driver-popover-next-btn').click(); 31 | cy.wait(1000); 32 | cy.get('button.driver-popover-next-btn').click(); 33 | cy.location('pathname').should('equal', '/'); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /frontend/cypress/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Using fixtures to represent data", 3 | "email": "hello@cypress.io", 4 | "body": "Fixtures are a great way to mock data for responses to routes" 5 | } 6 | -------------------------------------------------------------------------------- /frontend/cypress/support/commands.ts: -------------------------------------------------------------------------------- 1 | /// 2 | // *********************************************** 3 | // This example commands.ts shows you how to 4 | // create various custom commands and overwrite 5 | // existing commands. 6 | // 7 | // For more comprehensive examples of custom 8 | // commands please read more here: 9 | // https://on.cypress.io/custom-commands 10 | // *********************************************** 11 | // 12 | // 13 | // -- This is a parent command -- 14 | // Cypress.Commands.add('login', (email, password) => { ... }) 15 | // 16 | // 17 | // -- This is a child command -- 18 | // Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... }) 19 | // 20 | // 21 | // -- This is a dual command -- 22 | // Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) 23 | // 24 | // 25 | // -- This will overwrite an existing command -- 26 | // Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) 27 | // 28 | // declare global { 29 | // namespace Cypress { 30 | // interface Chainable { 31 | // login(email: string, password: string): Chainable 32 | // drag(subject: string, options?: Partial): Chainable 33 | // dismiss(subject: string, options?: Partial): Chainable 34 | // visit(originalFn: CommandOriginalFn, url: string, options: Partial): Chainable 35 | // } 36 | // } 37 | // } -------------------------------------------------------------------------------- /frontend/cypress/support/e2e.ts: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/e2e.ts is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands' 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') -------------------------------------------------------------------------------- /frontend/cypress/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "include": [ 4 | "../node_modules/cypress", 5 | "**/*.ts" 6 | ], 7 | "compilerOptions": { 8 | "noEmit": false, 9 | "sourceMap": false, 10 | "types": [ 11 | "cypress" 12 | ] 13 | }, 14 | } -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "dev": "vite dev --host", 7 | "build": "vite build", 8 | "preview": "vite preview", 9 | "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", 10 | "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", 11 | "lint": "prettier --plugin-search-dir . --check . && eslint .", 12 | "format": "prettier --plugin-search-dir . --write .", 13 | "cy:open": "cypress open --e2e", 14 | "test": "cypress run --e2e", 15 | "cy:parallel": "cypress-parallel -s cy:open -t 2 -m false" 16 | }, 17 | "devDependencies": { 18 | "@rollup/plugin-json": "^6.1.0", 19 | "@sveltejs/adapter-static": "^3.0.5", 20 | "@sveltejs/adapter-vercel": "^5.6.3", 21 | "@sveltejs/kit": "^2.7.2", 22 | "@sveltejs/vite-plugin-svelte": "^4.0.0", 23 | "@types/sortablejs": "^1.15.8", 24 | "@typescript-eslint/eslint-plugin": "^6.21.0", 25 | "@typescript-eslint/parser": "^6.21.0", 26 | "autoprefixer": "^10.4.20", 27 | "cypress": "^13.15.0", 28 | "daisyui": "^3.9.4", 29 | "eslint": "^8.57.1", 30 | "eslint-config-prettier": "^8.10.0", 31 | "eslint-plugin-svelte": "^2.46.0", 32 | "mockrtc": "^0.3.2", 33 | "postcss": "^8.4.47", 34 | "prettier": "^3.3.3", 35 | "prettier-plugin-svelte": "^3.2.7", 36 | "svelte": "^5.1.0", 37 | "svelte-check": "^4.0.5", 38 | "tailwindcss": "^3.4.14", 39 | "tslib": "^2.8.0", 40 | "typescript": "^5.6.3", 41 | "vite": "^5.4.17" 42 | }, 43 | "type": "module", 44 | "dependencies": { 45 | "@formkit/auto-animate": "0.8.2", 46 | "@libreremoteplay/signals": "^1.0.0", 47 | "bowser": "^2.11.0", 48 | "driver.js": "^1.3.1", 49 | "player.style": "^0.1.5", 50 | "sortablejs": "^1.15.6", 51 | "svelte-i18n": "^4.0.1" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /frontend/package.json.md5: -------------------------------------------------------------------------------- 1 | d4a02d189c3b0b4cc63a1d79c1ade10e -------------------------------------------------------------------------------- /frontend/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /frontend/src/app.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; -------------------------------------------------------------------------------- /frontend/src/app.d.ts: -------------------------------------------------------------------------------- 1 | // See https://kit.svelte.dev/docs/types#app 2 | // for information about these interfaces 3 | declare global { 4 | namespace App { 5 | // interface Error {} 6 | // interface Locals {} 7 | // interface PageData {} 8 | // interface Platform {} 9 | } 10 | } 11 | 12 | export {}; 13 | -------------------------------------------------------------------------------- /frontend/src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 11 | 12 | 13 | %sveltekit.head% 14 | 15 | 34 | 35 | 36 |
%sveltekit.body%
37 | 38 | 39 | -------------------------------------------------------------------------------- /frontend/src/lib/assets/gamepad.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/lib/audio/audio_player.ts: -------------------------------------------------------------------------------- 1 | import { writable, get } from "svelte/store"; 2 | 3 | const defaultVolume = 0.1; 4 | const audioVolumeStore = writable(defaultVolume); 5 | 6 | function playAudio(name: string) { 7 | 8 | try { 9 | const basePath = '/sounds/'; 10 | 11 | const audio = new Audio(basePath + name + ".mp3"); 12 | 13 | audio.volume = get(audioVolumeStore); 14 | 15 | navigator.userActivation.isActive && audio.play(); 16 | } catch (error) { 17 | console.error("Error playing audio:", error); 18 | } 19 | 20 | } 21 | 22 | 23 | function volumeChange(volume: number) { 24 | audioVolumeStore.set(volume); 25 | } 26 | 27 | export {playAudio, volumeChange}; -------------------------------------------------------------------------------- /frontend/src/lib/css/media-video.css: -------------------------------------------------------------------------------- 1 | media-live-button { 2 | display: none !important; 3 | } -------------------------------------------------------------------------------- /frontend/src/lib/detection/IsLinux.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | {#await isLinux() then bool} 8 | {#if bool} 9 | {#if !not} 10 | {@render children?.()} 11 | {/if} 12 | {:else if not} 13 | {@render children?.()} 14 | {/if} 15 | {/await} 16 | -------------------------------------------------------------------------------- /frontend/src/lib/detection/IsWindows.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | {#await isWindows() then bool} 8 | {#if bool} 9 | {#if !not} 10 | {@render children?.()} 11 | {/if} 12 | {:else if not} 13 | {@render children?.()} 14 | {/if} 15 | {/await} 16 | -------------------------------------------------------------------------------- /frontend/src/lib/detection/detect_os.ts: -------------------------------------------------------------------------------- 1 | import onwebsite from '$lib/detection/onwebsite'; 2 | import { GetCurrentOS } from '$lib/wailsjs/go/bindings/App'; 3 | 4 | type OS = 'WINDOWS' | 'LINUX' | 'MACOS'; 5 | 6 | export async function isWindows() { 7 | try { 8 | if (onwebsite) return false; 9 | else return ((await GetCurrentOS()) as OS) === 'WINDOWS'; 10 | } catch { 11 | return false 12 | } 13 | } 14 | 15 | export async function isLinux() { 16 | try { 17 | if (onwebsite) return false; 18 | else return ((await GetCurrentOS()) as OS) === 'LINUX'; 19 | } catch { 20 | return false 21 | } 22 | } 23 | 24 | export async function isMacOS() { 25 | try { 26 | if (onwebsite) return false; 27 | else return ((await GetCurrentOS()) as OS) === 'MACOS'; 28 | } catch { 29 | return false 30 | } 31 | } -------------------------------------------------------------------------------- /frontend/src/lib/detection/onwebsite.ts: -------------------------------------------------------------------------------- 1 | // This file is used to determine if the app is running on the website(vercel client only) or not. 2 | const onwebsite = import.meta.env?.VITE_ON_WEBSITE === 'true'; 3 | 4 | // This will be true if using the linux client when browser opens 5 | const IS_RUNNING_EXTERNAL = window && window?.location?.port === "8080"; 6 | 7 | export {IS_RUNNING_EXTERNAL} 8 | 9 | export default onwebsite; -------------------------------------------------------------------------------- /frontend/src/lib/easy_connect/easy_connect.svelte.ts: -------------------------------------------------------------------------------- 1 | import { ConnectToHostWeb, CreateClientWeb } from "$lib/webrtc/client_webrtc_hook" 2 | import {SendClientCode, server} from "@libreremoteplay/signals" 3 | import { _ } from 'svelte-i18n' 4 | 5 | export const easyConnectServerIpDomain = $state({value: "localhost:80"}) 6 | export const easyConnectID = $state({value: 0}) 7 | 8 | export async function handleEasyConnectClient() { 9 | 10 | const easyConnectServerUrl = (() => { 11 | if (easyConnectServerIpDomain.value.length < 1) { 12 | return ""; 13 | } 14 | return `ws://${easyConnectServerIpDomain.value}/ws`; 15 | })() 16 | 17 | console.log("Easy Connect Server URL:", easyConnectServerUrl); 18 | 19 | if (!URL.canParse(easyConnectServerUrl)) { 20 | throw new Error("Easy Connect Server URL is not set"); 21 | } 22 | 23 | const serverInstance = server(easyConnectServerUrl) 24 | 25 | const clientCode = await CreateClientWeb() 26 | 27 | console.log("Client Code:", clientCode); 28 | 29 | if (!clientCode) { 30 | throw new Error("Failed to create client code"); 31 | } 32 | 33 | const hostCode = await SendClientCode(serverInstance, {data: clientCode}, easyConnectID.value) 34 | 35 | console.log("Host Code:", hostCode); 36 | 37 | if (hostCode.data.length < 1) { 38 | throw new Error("Failed to send client code to host"); 39 | } 40 | 41 | await ConnectToHostWeb(hostCode.data) 42 | 43 | } -------------------------------------------------------------------------------- /frontend/src/lib/gamepad/gamepad_hook.ts: -------------------------------------------------------------------------------- 1 | export type ClonedGamepad = { 2 | axes: number[]; 3 | buttons: GamepadButton[]; 4 | connected: boolean; 5 | id: string; 6 | index: number; 7 | }; 8 | 9 | type GamepadButton = { 10 | pressed: boolean; 11 | value: number; 12 | }; 13 | 14 | 15 | export function cloneGamepad(gamepad: Gamepad): ClonedGamepad { 16 | 17 | return { 18 | axes: [...gamepad.axes], 19 | buttons: gamepad.buttons.map((button) => { 20 | return { 21 | pressed: button.pressed, 22 | value: button.value 23 | }; 24 | }), 25 | connected: gamepad.connected, 26 | id: gamepad.id, 27 | index: gamepad.index 28 | }; 29 | } 30 | 31 | export function handleGamepad(controllerChannel: RTCDataChannel) { 32 | const sendGamepadData = () => { 33 | const gamepadData = navigator.getGamepads(); 34 | 35 | gamepadData.forEach((gamepad) => { 36 | if (!gamepad) return; 37 | 38 | const serializedData = JSON.stringify(cloneGamepad(gamepad)); 39 | controllerChannel.send(serializedData); 40 | }); 41 | }; 42 | 43 | const gamepadLoop = () => { 44 | sendGamepadData(); 45 | 46 | // Continue the loop 47 | requestAnimationFrame(gamepadLoop); 48 | }; 49 | 50 | // Start the gamepad loop 51 | gamepadLoop(); 52 | } 53 | -------------------------------------------------------------------------------- /frontend/src/lib/i18n/LanguageSelector.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 |
19 | 22 | 23 |
    24 | {#each $locales as locale} 25 |
  • 26 | 29 |
  • 30 | {/each} 31 |
32 |
33 | -------------------------------------------------------------------------------- /frontend/src/lib/i18n/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "main_title_choose": "Choose", 3 | "main_title_your": "your", 4 | "main_title_role": "role", 5 | "host_card_title": "Host", 6 | "host_card_description": "Share & stream your game to clients", 7 | "host_card_cta": "Start Host", 8 | "client_card_title": "Client", 9 | "client_card_description": "Plug your gamepad and play through streaming", 10 | "client_card_cta": "Create Client", 11 | "stun-servers-title": "STUN Servers", 12 | "server-list-link": "Server List", 13 | "add": "Add", 14 | "get-your-host-code": "Get your client code", 15 | "connect-to-client": "Connect to Client", 16 | "paste-here-code": "Paste here code", 17 | "start-streaming": "Start Streaming", 18 | "error-creating-client": "Error creating client", 19 | "client-code-copied-to-clipboard": "Client code copied to clipboard", 20 | "error-connecting-to-host": "Error connecting to host", 21 | "connection-lost": "Connection lost", 22 | "connection-failed": "Connection failed", 23 | "connection-closed": "Connection closed", 24 | "connected": "Connected", 25 | "unknown-connection-state": "Unknown connection state", 26 | "error-creating-host": "Error creating host", 27 | "error-starting-streaming": "Error starting streaming", 28 | "streaming-stopped": "Streaming stopped", 29 | "error-stopping-streaming": "Error stopping streaming", 30 | "you-are-now-disconnected": "You are now disconnected", 31 | "code-is-empty": "Code is empty", 32 | "host-code-copied-to-clipboard": "Host code copied to clipboard", 33 | "waiting-for-client-to-connect": "Waiting for client to connect", 34 | "make-sure-to-pass-the-code-to-the-client": "¡Make sure to pass the code to the client!", 35 | "first-step": "First Step", 36 | "share-the-code-with-your-host": "Share the code with your host", 37 | "if-your-code-is-missing-from-your-clipboard-you-must-restart-the-process": "If your code is missing from your clipboard, you must restart the process.", 38 | "connect-to-host": "Connect to Host", 39 | "second-step": "Second Step", 40 | "get-the-code-from-your-host": "Get the code from your host", 41 | "connect-to-stream": "Connect to stream", 42 | "language-selector-title": "Language Selector", 43 | "turn-servers-title": "TURN Servers", 44 | "selfhost-turn-link": "TURN Selfhost", 45 | "config_title": "Config", 46 | "config": "Modify", 47 | "advance": "Advanced", 48 | "delete": "Delete", 49 | "create_group": "Create a new group", 50 | "no_servers": "There are no servers", 51 | "new_server": "New server", 52 | "username": "Username", 53 | "password": "Password", 54 | "no_groups_warning": "Be careful you have no server, this may cause you to not be able to connect properly to other computers.", 55 | "restore_default_servers": "Restore default values", 56 | "server-group-update": "Server group updated successfully", 57 | "default-loading-title": "Waiting to resolve connection!", 58 | "default-loading-message": "Make sure to stay focused on this window", 59 | "are-you-sure-you-want-to-leave": "Are you sure you want to leave?", 60 | "error-copying-host-code-to-clipboard": "Error copying host code to clipboard", 61 | "error-copying-client-code-to-clipboard": "Error copying client code to clipboard", 62 | "connection-established-successfully": "Connection established successfully", 63 | "tutorial_btn": "Tutorial", 64 | "tutorial_language_title": "Choose a language", 65 | "tutorial_language_description": "There are different languages available", 66 | "tutorial_config_title": "Go to config", 67 | "tutorial_config_description": "You can normally do it by clicking on the button", 68 | "tutorial_stun_title": "See the STUN servers", 69 | "tutorial_stun_description": "STUN servers are important to stablish connections", 70 | "tutorial_group_server_title": "Edit your STUN config", 71 | "tutorial_group_server_description": "You can add more or edit/remove the existing servers", 72 | "tutorial_go_back_title": "Go back", 73 | "tutorial_go_back_description": "Let's go back to the previous menu", 74 | "tutorial_turn_title": "Here are the TURN servers", 75 | "tutorial_turn_description": "The TURN servers are important if you want to stablish connections behind stricts firewalls or using mobile internet", 76 | "tutorial_done_text": "Done", 77 | "tutorial_next_text": "Next", 78 | "tutorial_prev_text": "Previous", 79 | "tutorial_play_title": "You are ready to play", 80 | "tutorial_play_description": "Now select your prefered mode to play (in web there is only one) and follow the instructions inside", 81 | "share-the-code-with-your-client": "Share the code with your client", 82 | "go-browser": "You default browser should have been opened automatically (There are known issues in Firefox: We recommend using Chromium based Browsers)", 83 | "relay-title": "Go to the browser tab", 84 | "warning-go-browser": "Don't close the APP or go back in the menu", 85 | "resolutions": "Resolutions", 86 | "framerate": "Framerate", 87 | "ideal-framerate": "Ideal Framerate", 88 | "max-framerate": "Max Framerate" 89 | } 90 | -------------------------------------------------------------------------------- /frontend/src/lib/i18n/fr.json: -------------------------------------------------------------------------------- 1 | { 2 | "main_title_choose": "Choisissez", 3 | "main_title_your": "votre", 4 | "main_title_role": "rôle", 5 | "host_card_title": "Hôte", 6 | "host_card_description": "Partagez et diffusez votre jeu à des clients", 7 | "host_card_cta": "Commencer en tant qu'Hôte", 8 | "client_card_title": "Client", 9 | "client_card_description": "Branchez votre manette de jeu et jouez via le streaming", 10 | "client_card_cta": "Créer un Client", 11 | "stun-servers-title": "Serveurs STUN", 12 | "server-list-link": "Liste des Serveurs", 13 | "add": "Ajouter", 14 | "get-your-host-code": "Obtenez votre code client", 15 | "connect-to-client": "Se connecter au Client", 16 | "paste-here-code": "Collez le code ici", 17 | "start-streaming": "Commencer le Streaming", 18 | "error-creating-client": "Erreur lors de la création du client", 19 | "client-code-copied-to-clipboard": "Code client copié dans le presse-papiers", 20 | "error-connecting-to-host": "Erreur de connexion à l'hôte", 21 | "connection-lost": "Connexion perdue", 22 | "connection-failed": "Échec de la connexion", 23 | "connection-closed": "Connexion fermée", 24 | "connected": "Connecté", 25 | "unknown-connection-state": "État de connexion inconnu", 26 | "error-creating-host": "Erreur lors de la création de l'hôte", 27 | "error-starting-streaming": "Erreur lors du démarrage du streaming", 28 | "streaming-stopped": "Streaming arrêté", 29 | "error-stopping-streaming": "Erreur lors de l'arrêt du streaming", 30 | "you-are-now-disconnected": "Vous êtes maintenant déconnecté", 31 | "code-is-empty": "Le code est vide", 32 | "host-code-copied-to-clipboard": "Code hôte copié dans le presse-papiers", 33 | "waiting-for-client-to-connect": "En attente de la connexion du client", 34 | "make-sure-to-pass-the-code-to-the-client": "Assurez-vous de transmettre le code au client !", 35 | "first-step": "Première étape", 36 | "share-the-code-with-your-host": "Partagez le code avec votre hôte", 37 | "if-your-code-is-missing-from-your-clipboard-you-must-restart-the-process": "Si votre code ne se trouve pas dans votre presse-papier, vous devez redémarrer le processus.", 38 | "connect-to-host": "Se connecter à l'Hôte", 39 | "second-step": "Deuxième étape", 40 | "get-the-code-from-your-host": "Obtenez le code de votre hôte", 41 | "connect-to-stream": "Se connecter au flux", 42 | "language-selector-title": "Sélecteur de Langue", 43 | "turn-servers-title": "Serveurs TURN", 44 | "selfhost-turn-link": "Auto-hébergement TURN", 45 | "config_title": "Configuration", 46 | "config": "Modifier", 47 | "advance": "Avancé", 48 | "delete": "Supprimer", 49 | "create_group": "Créer un nouveau groupe", 50 | "no_servers": "Il n'y a pas de serveurs", 51 | "new_server": "Nouveau serveur", 52 | "username": "Nom d'utilisateur", 53 | "password": "Mot de passe", 54 | "no_groups_warning": "Attention, vous n'avez pas de serveur, cela peut entraîner des problèmes de connexion avec d'autres ordinateurs.", 55 | "restore_default_servers": "Restaurer les valeurs par défaut", 56 | "server-group-update": "Groupe de serveurs mis à jour avec succès", 57 | "default-loading-title": "En attente de résolution de la connexion !", 58 | "default-loading-message": "Assurez-vous de rester sur cette fenêtre", 59 | "are-you-sure-you-want-to-leave": "Êtes-vous certain de vouloir quitter ?", 60 | "error-copying-host-code-to-clipboard": "Erreur lors de la copie du code hôte dans le presse-papiers", 61 | "error-copying-client-code-to-clipboard": "Erreur lors de la copie du code client dans le presse-papiers", 62 | "connection-established-successfully": "Connexion établie avec succès", 63 | "tutorial_btn": "Tutoriel", 64 | "tutorial_language_title": "Choisir une langue", 65 | "tutorial_language_description": "Différentes langues sont disponibles", 66 | "tutorial_config_title": "Aller à la configuration", 67 | "tutorial_config_description": "Vous pouvez généralement le faire en cliquant sur le bouton", 68 | "tutorial_stun_title": "Voir les serveurs STUN", 69 | "tutorial_stun_description": "Les serveurs STUN sont importants pour établir des connexions", 70 | "tutorial_group_server_title": "Modifier la configuration STUN", 71 | "tutorial_group_server_description": "Vous pouvez ajouter, modifier ou supprimer des serveurs", 72 | "tutorial_go_back_title": "Revenir en arrière", 73 | "tutorial_go_back_description": "Revenons au menu précédent", 74 | "tutorial_turn_title": "Voici les serveurs TURN", 75 | "tutorial_turn_description": "Les serveurs TURN sont importants si vous voulez établir des connexions derrière des pare-feux stricts ou en utilisant un internet mobile", 76 | "tutorial_done_text": "Terminé", 77 | "tutorial_next_text": "Suivant", 78 | "tutorial_prev_text": "Précédent", 79 | "tutorial_play_title": "Vous êtes prêt à jouer", 80 | "tutorial_play_description": "Sélectionnez maintenant votre mode de jeu préféré (sur le web, il n'y en a qu'un) et suivez les instructions", 81 | "share-the-code-with-your-client": "Partagez le code avec votre client" 82 | } -------------------------------------------------------------------------------- /frontend/src/lib/i18n/i18n.ts: -------------------------------------------------------------------------------- 1 | import { browser } from '$app/environment' 2 | import { init, register, getLocaleFromNavigator, locale} from 'svelte-i18n' 3 | 4 | const defaultLocale = 'en' 5 | 6 | register('en', () => import('./en.json')) 7 | register('es', () => import('./es.json')) 8 | register('gl', () => import('./gl.json')) 9 | register('ru', () => import('./ru.json')) 10 | register('fr', () => import('./fr.json')) 11 | 12 | init({ 13 | fallbackLocale: defaultLocale, 14 | initialLocale: browser ? new Intl.Locale(getLocaleFromLocalStorage() ?? getLocaleFromNavigator() ?? defaultLocale).language : defaultLocale, 15 | }) 16 | 17 | export function getLocaleFromLocalStorage() { 18 | const locale_stored = localStorage.getItem('locale') 19 | return locale_stored 20 | } 21 | 22 | function saveLocaleToLocalStorage(locale: string) { 23 | browser && localStorage.setItem('locale', locale) 24 | } 25 | 26 | locale.subscribe((value) => { 27 | console.log('locale changed to', value) 28 | saveLocaleToLocalStorage(value ?? defaultLocale) 29 | }) 30 | 31 | 32 | -------------------------------------------------------------------------------- /frontend/src/lib/i18n/ru.json: -------------------------------------------------------------------------------- 1 | { 2 | "add": "Добавь", 3 | "advance": "Расширенные", 4 | "are-you-sure-you-want-to-leave": "Ты уверен, что хочешь уйти?", 5 | "client-code-copied-to-clipboard": "Клиентский код, скопированный в буфер обмена", 6 | "client_card_cta": "Создать клиента", 7 | "client_card_description": "Подключите свой геймпад и играйте в потоковом режиме", 8 | "client_card_title": "Клиент", 9 | "code-is-empty": "Код пуст", 10 | "config": "Модифицировать", 11 | "config_title": "Конфигурация", 12 | "connect-to-client": "Подключение к клиенту", 13 | "connect-to-host": "Подключиться к хосту", 14 | "connect-to-stream": "Подключиться к потоку", 15 | "connected": "Связанный", 16 | "connection-closed": "Соединение закрыто", 17 | "connection-failed": "Не удалось установить соединение", 18 | "connection-lost": "Соединение потеряно", 19 | "connection-established-successfully": "Соединение успешно установлено", 20 | "tutorial_btn": "Руководство", 21 | "default-loading-message": "Обязательно сосредоточьтесь на этом окне.", 22 | "create_group": "Создать новую группу", 23 | "default-loading-title": "Ожидание разрешения соединения!", 24 | "delete": "Удалить", 25 | "error-connecting-to-host": "Ошибка подключения к хосту", 26 | "error-copying-client-code-to-clipboard": "Ошибка копирования кода клиента в буфер обмена.", 27 | "error-copying-host-code-to-clipboard": "Ошибка копирования кода хоста в буфер обмена.", 28 | "error-creating-client": "Ошибка создания клиента", 29 | "error-creating-host": "Ошибка создания хоста", 30 | "error-starting-streaming": "Ошибка начала потоковой передачи", 31 | "error-stopping-streaming": "Ошибка остановки потоковой передачи", 32 | "first-step": "Первый шаг", 33 | "get-the-code-from-your-host": "Получите код от вашего хостера", 34 | "get-your-host-code": "Получите ваш клиентский код", 35 | "host-code-copied-to-clipboard": "Код хоста скопирован в буфер обмена.", 36 | "host_card_cta": "Запустить хост", 37 | "host_card_description": "Делитесь своей игрой с клиентами и транслируйте ее в потоковом режиме", 38 | "host_card_title": "Хозяин", 39 | "if-your-code-is-missing-from-your-clipboard-you-must-restart-the-process": "Если ваш код отсутствует в буфере обмена, вам необходимо перезапустить процесс.", 40 | "language-selector-title": "Выбор языка", 41 | "main_title_choose": "Выбирать", 42 | "main_title_role": "роль", 43 | "main_title_your": "твой", 44 | "make-sure-to-pass-the-code-to-the-client": "¡Обязательно передайте код клиенту!", 45 | "new_server": "Новый сервер", 46 | "no_groups_warning": "Будьте осторожны, у вас нет сервера, это может привести к тому, что вы не сможете правильно подключиться к другим компьютерам.", 47 | "no_servers": "Нет серверов", 48 | "password": "Пароль", 49 | "paste-here-code": "Вставьте сюда код", 50 | "restore_default_servers": "Восстановить значения по умолчанию", 51 | "second-step": "Второй шаг", 52 | "selfhost-turn-link": "TURN Селфхост", 53 | "server-group-update": "Группа серверов успешно обновлена", 54 | "server-list-link": "Список серверов", 55 | "start-streaming": "Начать трансляцию", 56 | "share-the-code-with-your-host": "Поделитесь кодом с вашим хостером", 57 | "streaming-stopped": "Трансляция остановлена", 58 | "stun-servers-title": "STUN серверы", 59 | "turn-servers-title": "Серверы TURN", 60 | "tutorial_language_description": "Доступны разные языки", 61 | "tutorial_language_title": "Выберите язык", 62 | "unknown-connection-state": "Неизвестное состояние соединения", 63 | "username": "Имя пользователя", 64 | "waiting-for-client-to-connect": "Ожидание подключения клиента", 65 | "you-are-now-disconnected": "Вы сейчас отключены", 66 | "tutorial_config_title": "Перейти к конфигурации", 67 | "tutorial_config_description": "Обычно это можно сделать, нажав кнопку", 68 | "tutorial_stun_title": "Посмотрите STUN-серверы", 69 | "tutorial_stun_description": "Серверы STUN важны для установления соединений.", 70 | "tutorial_group_server_title": "Отредактируйте конфигурацию STUN", 71 | "tutorial_group_server_description": "Вы можете добавить больше или отредактировать/удалить существующие серверы.", 72 | "tutorial_go_back_title": "Возвращаться", 73 | "tutorial_go_back_description": "Вернёмся в предыдущее меню", 74 | "tutorial_turn_title": "Вот серверы TURN", 75 | "tutorial_turn_description": "Серверы TURN важны, если вы хотите установить соединения за строгими брандмауэрами или использовать мобильный Интернет.", 76 | "tutorial_done_text": "Сделанный", 77 | "tutorial_next_text": "Следующий", 78 | "tutorial_prev_text": "Предыдущий", 79 | "tutorial_play_title": "Вы готовы играть", 80 | "tutorial_play_description": "Теперь выберите предпочитаемый режим игры (в сети он только один) и следуйте инструкциям внутри.", 81 | "share-the-code-with-your-client": "Поделитесь кодом с вашим клиентом" 82 | } 83 | -------------------------------------------------------------------------------- /frontend/src/lib/keyboard/keyboard_hook.ts: -------------------------------------------------------------------------------- 1 | type keyHandler = (keycode: string) => void 2 | 3 | export function handleKeyDown(callback: keyHandler) { 4 | 5 | const handler = (event: KeyboardEvent) => { 6 | event.preventDefault() 7 | event.stopPropagation() 8 | return callback(event.key + '_1'); 9 | } 10 | 11 | document.addEventListener('keydown', handler, true); 12 | 13 | return handler 14 | } 15 | 16 | export function unhandleKeyDown(callback: ReturnType) { 17 | document.removeEventListener("keydown", callback) 18 | } 19 | 20 | export function handleKeyUp(callback: keyHandler) { 21 | 22 | const handler = (event: KeyboardEvent) => { 23 | event.preventDefault() 24 | event.stopPropagation() 25 | return callback(event.key + '_0'); 26 | } 27 | 28 | document.addEventListener('keyup', handler, true); 29 | 30 | return handler 31 | } 32 | 33 | export function unhandleKeyUp(callback: ReturnType) { 34 | document.removeEventListener("keyup", callback) 35 | } -------------------------------------------------------------------------------- /frontend/src/lib/layout/BackwardButton.svelte: -------------------------------------------------------------------------------- 1 | 18 | 19 | 35 | -------------------------------------------------------------------------------- /frontend/src/lib/layout/PageTransition.svelte: -------------------------------------------------------------------------------- 1 | 17 | 18 | {#key key} 19 |
20 | {@render children?.()} 21 |
22 | {/key} 23 | -------------------------------------------------------------------------------- /frontend/src/lib/layout/icons/DeleteIcon.svelte: -------------------------------------------------------------------------------- 1 | 14 | -------------------------------------------------------------------------------- /frontend/src/lib/layout/icons/GamepadIcon.svelte: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/lib/layout/icons/KeyboardIcon.svelte: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/lib/layout/icons/PencilIcon.svelte: -------------------------------------------------------------------------------- 1 | 14 | -------------------------------------------------------------------------------- /frontend/src/lib/layout/icons/TrashIcon.svelte: -------------------------------------------------------------------------------- 1 | 16 | -------------------------------------------------------------------------------- /frontend/src/lib/layout/useSortable.svelte.ts: -------------------------------------------------------------------------------- 1 | // source: https://dev.to/jdgamble555/svelte-5-and-sortablejs-5h6j 2 | import Sortable from 'sortablejs'; 3 | 4 | export const useSortable = ( 5 | getter: () => HTMLElement | null, 6 | options?: Sortable.Options 7 | ) => { 8 | $effect(() => { 9 | const sortableEl = getter(); 10 | const sortable = sortableEl ? 11 | Sortable.create(sortableEl, options) 12 | : null; 13 | return () => sortable?.destroy(); 14 | }); 15 | } 16 | 17 | export function reorder( 18 | array: T[], 19 | evt: Sortable.SortableEvent 20 | ): $state.Snapshot[] { 21 | 22 | // should have no effect on stores or regular array 23 | const workArray = $state.snapshot(array); 24 | 25 | // get changes 26 | const { oldIndex, newIndex } = evt; 27 | 28 | if (oldIndex === undefined || newIndex === undefined) { 29 | return workArray; 30 | } 31 | if (newIndex === oldIndex) { 32 | return workArray; 33 | } 34 | 35 | // move elements 36 | const target = workArray[oldIndex]; 37 | const increment = newIndex < oldIndex ? -1 : 1; 38 | 39 | for (let k = oldIndex; k !== newIndex; k += increment) { 40 | workArray[k] = workArray[k + increment]; 41 | } 42 | workArray[newIndex] = target; 43 | return workArray; 44 | } -------------------------------------------------------------------------------- /frontend/src/lib/loading/Loading.svelte: -------------------------------------------------------------------------------- 1 | 21 | 22 | 23 | 28 | 29 | -------------------------------------------------------------------------------- /frontend/src/lib/loading/loading_hook.ts: -------------------------------------------------------------------------------- 1 | import { writable, get } from 'svelte/store'; 2 | import { _ } from 'svelte-i18n'; 3 | 4 | interface LoadingStore { 5 | loading: boolean; 6 | title?: string; 7 | message?: string; 8 | } 9 | 10 | const defaultLoadingStore: LoadingStore = { 11 | loading: false 12 | }; 13 | 14 | const loadingWritable = writable(defaultLoadingStore); 15 | 16 | export function toogleLoading() { 17 | const currentLoading = get(loadingWritable); 18 | 19 | if (!currentLoading.message && !currentLoading.title) { 20 | loadingWritable.update((store) => { 21 | const translatedLoading = { 22 | ...store, 23 | title: get(_)('default-loading-title'), 24 | message: get(_)('default-loading-message') 25 | }; 26 | 27 | const loading = !store.loading; 28 | 29 | if (loading) { 30 | return { ...translatedLoading, loading }; 31 | } 32 | 33 | return defaultLoadingStore; 34 | }); 35 | return; 36 | } 37 | 38 | loadingWritable.update((store) => { 39 | const loading = !store.loading; 40 | 41 | if (loading) { 42 | return { ...store, loading }; 43 | } 44 | 45 | return defaultLoadingStore; 46 | }); 47 | } 48 | 49 | export function setLoadingTitle(title: string) { 50 | loadingWritable.update((store) => ({ ...store, title })); 51 | } 52 | 53 | export function setLoadingMessage(message: string) { 54 | loadingWritable.update((store) => ({ ...store, message })); 55 | } 56 | 57 | export default loadingWritable; 58 | -------------------------------------------------------------------------------- /frontend/src/lib/logger/logger.ts: -------------------------------------------------------------------------------- 1 | export default async function log(info: string) { 2 | 3 | try { 4 | // const { LogPrint } = await import("$lib/wailsjs/runtime/runtime") 5 | // LogPrint(info + '\n') 6 | } finally { 7 | console.log(info) 8 | } 9 | 10 | } -------------------------------------------------------------------------------- /frontend/src/lib/toast/Toast.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 | {#if $toast.show} 15 | 16 |
17 |
18 | {$toast.message} 19 |
20 |
21 | {/if} 22 | 23 | 38 | -------------------------------------------------------------------------------- /frontend/src/lib/toast/toast_hook.ts: -------------------------------------------------------------------------------- 1 | import { writable } from 'svelte/store'; 2 | 3 | interface Toast { 4 | show: boolean; 5 | message: string; 6 | type: ToastType; 7 | } 8 | 9 | export enum ToastType { 10 | INFO = 'info', 11 | SUCCESS = 'success', 12 | ERROR = 'error' 13 | } 14 | 15 | const toastWritable = writable({ 16 | show: false, 17 | message: '', 18 | type: ToastType.SUCCESS 19 | }); 20 | 21 | let timer: number | undefined; 22 | 23 | export function showToast(message: string, type: Toast['type']) { 24 | 25 | setTimeout(() => { 26 | toastWritable.set({ 27 | show: true, 28 | message, 29 | type 30 | }); 31 | 32 | if (timer) clearTimeout(timer); 33 | 34 | timer = window.setTimeout(() => { 35 | hideToast(); 36 | timer = undefined; 37 | }, 2000); 38 | 39 | }, 500); 40 | 41 | } 42 | 43 | export function hideToast() { 44 | toastWritable.set({ 45 | show: false, 46 | message: '', 47 | type: ToastType.SUCCESS 48 | }); 49 | } 50 | 51 | export default toastWritable; 52 | -------------------------------------------------------------------------------- /frontend/src/lib/tutorial/driver.ts: -------------------------------------------------------------------------------- 1 | import { goto } from '$app/navigation'; 2 | import { driver } from 'driver.js'; 3 | import type { Driver, DriveStep } from 'driver.js'; 4 | import 'driver.js/dist/driver.css'; 5 | import { _ } from 'svelte-i18n'; 6 | import { get } from 'svelte/store'; 7 | 8 | // hazer un singletone para el tutorial 9 | let tutorialDriver: Driver; 10 | let currentStep = 0; 11 | 12 | const TUTORIAL_DELAY = 750; 13 | 14 | export function StartTutorial(selectedStep: number = 0) { 15 | if (tutorialDriver) { 16 | tutorialDriver.destroy(); 17 | } 18 | 19 | tutorialDriver = driver({ 20 | animate: true, 21 | smoothScroll: true, 22 | stagePadding: 1, 23 | stageRadius: 1, 24 | doneBtnText: get(_)('tutorial_done_text'), 25 | nextBtnText: get(_)('tutorial_next_text'), 26 | prevBtnText: get(_)('tutorial_prev_text') 27 | }); 28 | 29 | const driverSteps: DriveStep[] = [ 30 | { 31 | element: '#tutorial-config-btn', 32 | popover: { 33 | title: get(_)('tutorial_config_title'), 34 | description: get(_)('tutorial_config_description'), 35 | onNextClick: () => { 36 | goto('/mode/config'); 37 | goNextTutorial(); 38 | } 39 | } 40 | }, 41 | { 42 | element: '#tutorial-language', 43 | popover: { 44 | title: get(_)('tutorial_language_title'), 45 | description: get(_)('tutorial_language_description'), 46 | onNextClick: () => { 47 | goNextTutorial(); 48 | }, 49 | onPrevClick: () => { 50 | goto('/'); 51 | goPrevTutorial(); 52 | } 53 | } 54 | }, 55 | { 56 | element: '#tutorial-stun-card', 57 | popover: { 58 | title: get(_)('tutorial_stun_title'), 59 | description: get(_)('tutorial_stun_description'), 60 | onNextClick: () => { 61 | goto('/mode/config/advanced/stun'); 62 | goNextTutorial(); 63 | } 64 | } 65 | }, 66 | { 67 | element: '#tutorial-group-server', 68 | popover: { 69 | title: get(_)('tutorial_group_server_title'), 70 | description: get(_)('tutorial_group_server_description'), 71 | onPrevClick: () => { 72 | goto('/mode/config'); 73 | goPrevTutorial(); 74 | }, 75 | onNextClick: () => { 76 | goNextTutorial(); 77 | } 78 | } 79 | }, 80 | { 81 | element: '#tutorial-back-btn', 82 | popover: { 83 | title: get(_)('tutorial_go_back_title'), 84 | description: get(_)('tutorial_go_back_description'), 85 | onPrevClick: () => { 86 | goPrevTutorial(); 87 | }, 88 | onNextClick: () => { 89 | goto('/mode/config'); 90 | goNextTutorial(); 91 | } 92 | } 93 | }, 94 | { 95 | element: '#tutorial-turn-card', 96 | popover: { 97 | title: get(_)('tutorial_turn_title'), 98 | description: get(_)('tutorial_turn_description'), 99 | onPrevClick: () => { 100 | goto('/mode/config/advanced/stun'); 101 | goPrevTutorial(); 102 | }, 103 | onNextClick: () => { 104 | goNextTutorial(); 105 | } 106 | } 107 | }, 108 | { 109 | element: '#tutorial-back-btn', 110 | popover: { 111 | title: get(_)('tutorial_go_back_title'), 112 | description: get(_)('tutorial_go_back_description'), 113 | onPrevClick: () => { 114 | goPrevTutorial(); 115 | }, 116 | onNextClick: () => { 117 | goto('/'); 118 | goNextTutorial(); 119 | } 120 | } 121 | }, 122 | { 123 | element: '#tutorial-play', 124 | popover: { 125 | title: get(_)('tutorial_play_title'), 126 | description: get(_)('tutorial_play_description'), 127 | onPrevClick: () => { 128 | goto("/mode/config") 129 | goPrevTutorial(); 130 | }, 131 | } 132 | 133 | } 134 | ]; 135 | 136 | tutorialDriver.setSteps(driverSteps); 137 | tutorialDriver.drive(selectedStep); 138 | } 139 | 140 | function goNextTutorial(duration: number = TUTORIAL_DELAY) { 141 | setTimeout(() => { 142 | currentStep = currentStep + 1; 143 | tutorialDriver?.moveNext(); 144 | }, duration); 145 | } 146 | 147 | function goPrevTutorial(duration: number = TUTORIAL_DELAY) { 148 | setTimeout(() => { 149 | currentStep = currentStep - 1; 150 | tutorialDriver?.movePrevious(); 151 | }, duration); 152 | } 153 | 154 | _.subscribe(() => { 155 | if (tutorialDriver) { 156 | StartTutorial(currentStep); 157 | } 158 | }); 159 | -------------------------------------------------------------------------------- /frontend/src/lib/wailsjs/go/bindings/App.d.ts: -------------------------------------------------------------------------------- 1 | // Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL 2 | // This file is automatically generated. DO NOT EDIT 3 | import {webrtc} from '../models'; 4 | 5 | export function GetCurrentOS():Promise; 6 | 7 | export function IsGamepadEnabled():Promise; 8 | 9 | export function IsKeyboardEnabled():Promise; 10 | 11 | export function NotifyCloseClient():Promise; 12 | 13 | export function NotifyCreateClient():Promise; 14 | 15 | export function OpenViGEmWizard():Promise; 16 | 17 | export function ToogleGamepad():Promise; 18 | 19 | export function ToogleKeyboard():Promise; 20 | 21 | export function TryClosePeerConnection():Promise; 22 | 23 | export function TryCreateHost(arg1:Array,arg2:string):Promise; 24 | -------------------------------------------------------------------------------- /frontend/src/lib/wailsjs/go/bindings/App.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | // Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL 3 | // This file is automatically generated. DO NOT EDIT 4 | 5 | export function GetCurrentOS() { 6 | return window['go']['bindings']['App']['GetCurrentOS'](); 7 | } 8 | 9 | export function IsGamepadEnabled() { 10 | return window['go']['bindings']['App']['IsGamepadEnabled'](); 11 | } 12 | 13 | export function IsKeyboardEnabled() { 14 | return window['go']['bindings']['App']['IsKeyboardEnabled'](); 15 | } 16 | 17 | export function NotifyCloseClient() { 18 | return window['go']['bindings']['App']['NotifyCloseClient'](); 19 | } 20 | 21 | export function NotifyCreateClient() { 22 | return window['go']['bindings']['App']['NotifyCreateClient'](); 23 | } 24 | 25 | export function OpenViGEmWizard() { 26 | return window['go']['bindings']['App']['OpenViGEmWizard'](); 27 | } 28 | 29 | export function ToogleGamepad() { 30 | return window['go']['bindings']['App']['ToogleGamepad'](); 31 | } 32 | 33 | export function ToogleKeyboard() { 34 | return window['go']['bindings']['App']['ToogleKeyboard'](); 35 | } 36 | 37 | export function TryClosePeerConnection() { 38 | return window['go']['bindings']['App']['TryClosePeerConnection'](); 39 | } 40 | 41 | export function TryCreateHost(arg1, arg2) { 42 | return window['go']['bindings']['App']['TryCreateHost'](arg1, arg2); 43 | } 44 | -------------------------------------------------------------------------------- /frontend/src/lib/wailsjs/go/models.ts: -------------------------------------------------------------------------------- 1 | export namespace webrtc { 2 | 3 | export class ICEServer { 4 | urls: string[]; 5 | username?: string; 6 | credential?: any; 7 | credentialType?: number; 8 | 9 | static createFrom(source: any = {}) { 10 | return new ICEServer(source); 11 | } 12 | 13 | constructor(source: any = {}) { 14 | if ('string' === typeof source) source = JSON.parse(source); 15 | this.urls = source["urls"]; 16 | this.username = source["username"]; 17 | this.credential = source["credential"]; 18 | this.credentialType = source["credentialType"]; 19 | } 20 | } 21 | 22 | } 23 | 24 | -------------------------------------------------------------------------------- /frontend/src/lib/wailsjs/runtime/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@wailsapp/runtime", 3 | "version": "2.0.0", 4 | "description": "Wails Javascript runtime library", 5 | "main": "runtime.js", 6 | "types": "runtime.d.ts", 7 | "scripts": { 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/wailsapp/wails.git" 12 | }, 13 | "keywords": [ 14 | "Wails", 15 | "Javascript", 16 | "Go" 17 | ], 18 | "author": "Lea Anthony ", 19 | "license": "MIT", 20 | "bugs": { 21 | "url": "https://github.com/wailsapp/wails/issues" 22 | }, 23 | "homepage": "https://github.com/wailsapp/wails#readme" 24 | } 25 | -------------------------------------------------------------------------------- /frontend/src/lib/webrtc/host_webrtc_hook.ts: -------------------------------------------------------------------------------- 1 | import { 2 | TryCreateHost as createHostFn, 3 | TryClosePeerConnection as closeConnectionFn 4 | } from '$lib/wailsjs/go/bindings/App'; 5 | 6 | import { _ } from 'svelte-i18n' 7 | import { get } from 'svelte/store'; 8 | import { showToast, ToastType } from '$lib/toast/toast_hook'; 9 | import { goto } from '$app/navigation'; 10 | import { toogleLoading, setLoadingMessage, setLoadingTitle } from '$lib/loading/loading_hook'; 11 | import { StopStreaming } from '$lib/webrtc/stream/host_stream_hook'; 12 | import type { ICEServer } from '$lib/webrtc/ice'; 13 | import { exportStunServers } from './stun_servers'; 14 | import { exportTurnServers } from './turn_servers'; 15 | import { isLinux } from '$lib/detection/detect_os'; 16 | import { IS_RUNNING_EXTERNAL } from '$lib/detection/onwebsite'; 17 | 18 | const BROWSER_BASE_URL = "http://localhost:8080/mode/host/connection"; 19 | 20 | let host: boolean = false; 21 | 22 | enum ConnectionState { 23 | Connected = 'CONNECTED', 24 | Failed = 'FAILED', 25 | Disconnected = 'DISCONNECTED' 26 | } 27 | 28 | export async function CreateHost(client: string) { 29 | try { 30 | 31 | const ICEServers: ICEServer[] = [ 32 | ...exportStunServers(), 33 | ...exportTurnServers() 34 | ] 35 | 36 | const hostCode = await createHostFn(ICEServers, client); 37 | 38 | if (isError(hostCode)) { 39 | throw new Error(hostCode); 40 | } 41 | 42 | if (navigator && navigator.clipboard && navigator.clipboard.writeText) { 43 | navigator.clipboard.writeText(hostCode).catch(() => { 44 | showToast(get(_)('error-copying-host-code-to-clipboard'), ToastType.ERROR); 45 | }); 46 | showToast(get(_)('host-code-copied-to-clipboard'), ToastType.SUCCESS); 47 | } else { 48 | showToast(get(_)('error-copying-host-code-to-clipboard'), ToastType.ERROR); 49 | } 50 | 51 | toogleLoading(); 52 | setLoadingMessage(get(_)('waiting-for-client-to-connect')); 53 | setLoadingTitle(get(_)('make-sure-to-pass-the-code-to-the-client')); 54 | 55 | const {EventsOnce} = await import("$lib/wailsjs/runtime/runtime") 56 | 57 | EventsOnce('connection_state', async (state: ConnectionState) => { 58 | toogleLoading(); 59 | 60 | switch (state.toUpperCase()) { 61 | case ConnectionState.Connected: 62 | showToast(get(_)('connected'), ToastType.SUCCESS); 63 | host = true; 64 | if (await isLinux()) { 65 | const {BrowserOpenURL} = await import("$lib/wailsjs/runtime/runtime") 66 | BrowserOpenURL(BROWSER_BASE_URL); 67 | } 68 | goto('/mode/host/connection'); 69 | break; 70 | case ConnectionState.Failed: 71 | showToast(get(_)('connection-failed'), ToastType.ERROR); 72 | goto('/'); 73 | break; 74 | default: 75 | showToast(get(_)('unknown-connection-state'), ToastType.ERROR); 76 | break; 77 | } 78 | }); 79 | } catch (e) { 80 | showToast(get(_)('error-creating-host'), ToastType.ERROR); 81 | } 82 | } 83 | 84 | function isError(err: string) { 85 | return err.toUpperCase().includes('ERROR'); 86 | } 87 | 88 | export function CloseHostConnection(fn?: () => void) { 89 | if (!host) return; 90 | closeConnectionFn(); 91 | if (fn) fn(); 92 | host = false; 93 | StopStreaming(); 94 | } 95 | 96 | export async function ListenForConnectionChanges() { 97 | 98 | if (IS_RUNNING_EXTERNAL) return; 99 | 100 | const {EventsOn, EventsOff} = await import("$lib/wailsjs/runtime/runtime") 101 | 102 | const connectionStateCancelEventListener = EventsOn( 103 | 'connection_state', 104 | (state: ConnectionState) => { 105 | switch (state.toUpperCase()) { 106 | case ConnectionState.Connected: 107 | showToast(get(_)('connected'), ToastType.SUCCESS); 108 | host = true; 109 | goto('/mode/host/connection'); 110 | break; 111 | case ConnectionState.Failed: 112 | showToast(get(_)('connection-failed'), ToastType.ERROR); 113 | goto('/'); 114 | break; 115 | case ConnectionState.Disconnected: 116 | showToast(get(_)('connection-lost'), ToastType.ERROR); 117 | host = false; 118 | goto('/'); 119 | connectionStateCancelEventListener(); 120 | EventsOff("connection_state") 121 | break; 122 | } 123 | } 124 | ); 125 | } 126 | -------------------------------------------------------------------------------- /frontend/src/lib/webrtc/ice.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface ServersConfig { 3 | [group: string]: ICEServer; 4 | } 5 | 6 | export interface ICEServer { 7 | urls: string[]; 8 | username?: string; 9 | credential?: string; 10 | } -------------------------------------------------------------------------------- /frontend/src/lib/webrtc/stream/CodecList.svelte: -------------------------------------------------------------------------------- 1 | 20 | 21 |
22 | 23 |
24 |
25 |

26 | {$_("codec-list")} 27 |

28 |

29 | {$_("codec-list-preference")} 30 |

31 | 32 |
33 |
34 |
    35 | {#each preferedCodecsOrdered.value as codec} 36 | {#key codec} 37 |
  • 38 | {codec} 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 |
  • 48 | {/key} 49 | {/each} 50 |
51 |
52 | 53 |
54 | 55 | 58 |
59 | 60 | 61 | -------------------------------------------------------------------------------- /frontend/src/lib/webrtc/stream/client_stream_hook.ts: -------------------------------------------------------------------------------- 1 | import { exportStunServers } from '$lib/webrtc/stun_servers'; 2 | import { setConsumingStream, type SignalingData } from '$lib/webrtc/stream/stream_signal_hook.svelte'; 3 | import { exportTurnServers } from '$lib/webrtc/turn_servers'; 4 | import { getSortedVideoCodecs} from './stream_config.svelte'; 5 | 6 | let peerConnection: RTCPeerConnection | undefined; 7 | let inboundStream: MediaStream | null = null; 8 | 9 | function initStreamingPeerConnection() { 10 | if (peerConnection) { 11 | peerConnection.close(); 12 | } 13 | 14 | peerConnection = new RTCPeerConnection({ 15 | iceServers: [...exportStunServers(), ...exportTurnServers()] 16 | }); 17 | } 18 | 19 | async function CreateClientStream( 20 | signalingChannel: RTCDataChannel, 21 | videoElement: HTMLVideoElement 22 | ) { 23 | initStreamingPeerConnection(); 24 | 25 | if (!videoElement || !peerConnection) throw new Error('Error creating stream'); 26 | 27 | const transceiver = peerConnection.addTransceiver("video", { direction: "recvonly" }); 28 | 29 | transceiver.setCodecPreferences(getSortedVideoCodecs()); 30 | 31 | peerConnection.onconnectionstatechange = () => { 32 | if (!peerConnection) return; 33 | 34 | const connectionTerminatedOptions: RTCPeerConnectionState[] = ["disconnected", "failed", "closed"] 35 | 36 | if (connectionTerminatedOptions.includes(peerConnection.connectionState)) { 37 | CloseStreamClientConnection() 38 | } 39 | }; 40 | 41 | peerConnection.onicecandidate = (e) => { 42 | if (!e.candidate) return; 43 | 44 | const data: SignalingData = { 45 | type: 'candidate', 46 | candidate: e.candidate.toJSON(), 47 | role: 'client' 48 | }; 49 | 50 | signalingChannel.send(JSON.stringify(data)); 51 | }; 52 | 53 | peerConnection.ontrack = (ev) => { 54 | 55 | if (ev.streams && ev.streams[0]) { 56 | ev.streams[0].getTracks().forEach(t => t.addEventListener("ended", () => {CloseStreamClientConnection()}, true) ) 57 | videoElement.srcObject = ev.streams[0]; 58 | videoElement.play(); 59 | } else { 60 | if (!inboundStream) { 61 | inboundStream = new MediaStream(); 62 | videoElement.srcObject = inboundStream; 63 | videoElement.play(); 64 | } 65 | ev.track.addEventListener("ended", () => {CloseStreamClientConnection()}, true) 66 | inboundStream.addTrack(ev.track); 67 | inboundStream.getTracks().forEach(t => t.addEventListener("ended", () => {CloseStreamClientConnection()}, true)) 68 | } 69 | }; 70 | 71 | 72 | const offer = await peerConnection.createOffer({ 73 | offerToReceiveAudio: true, 74 | offerToReceiveVideo: true, 75 | iceRestart: true 76 | }); 77 | 78 | await peerConnection.setLocalDescription(offer); 79 | 80 | const data: SignalingData = { 81 | type: 'offer', 82 | offer: offer, 83 | role: 'client' 84 | }; 85 | 86 | signalingChannel.send(JSON.stringify(data)); 87 | 88 | signalingChannel.onmessage = async (e) => { 89 | const { type, answer, candidate, role } = JSON.parse(e.data) as SignalingData; 90 | 91 | if (!peerConnection) { 92 | return; 93 | } 94 | 95 | if (role !== 'host') { 96 | return; 97 | } 98 | 99 | switch (type) { 100 | case 'answer': 101 | if (!answer) return; 102 | await peerConnection.setRemoteDescription(answer); 103 | break; 104 | case 'candidate': 105 | try {await peerConnection.addIceCandidate(candidate)} catch {/** */} 106 | break; 107 | } 108 | }; 109 | 110 | 111 | } 112 | 113 | function CloseStreamClientConnection() { 114 | setConsumingStream(false) 115 | if (!peerConnection) return; 116 | peerConnection.close(); 117 | peerConnection = undefined; 118 | } 119 | 120 | export { CreateClientStream, CloseStreamClientConnection }; 121 | -------------------------------------------------------------------------------- /frontend/src/lib/webrtc/stream/stream_config.svelte.ts: -------------------------------------------------------------------------------- 1 | import { browser } from '$app/environment' 2 | 3 | export enum FIXED_RESOLUTIONS { 4 | resolution1080p = "1080", 5 | resolution720p = "720", 6 | resolution480p = "480", 7 | resolution360p = "360" 8 | } 9 | 10 | export const RESOLUTIONS: Map = new Map() 11 | 12 | RESOLUTIONS.set(FIXED_RESOLUTIONS.resolution1080p, {width: 1920, height: 1080}) 13 | RESOLUTIONS.set(FIXED_RESOLUTIONS.resolution720p,{width: 1280, height: 720}) 14 | RESOLUTIONS.set(FIXED_RESOLUTIONS.resolution480p, {width:854, height: 480}) 15 | RESOLUTIONS.set(FIXED_RESOLUTIONS.resolution360p, {width: 640, height:360}) 16 | 17 | export const DEFAULT_MAX_FRAMERATE = 60 18 | export const DEFAULT_IDEAL_FRAMERATE = 30 19 | 20 | const DEFAULT_PREFERED_CODECS = ["video/VP9","video/AV1","video/H264", "video/VP8"] 21 | 22 | export const preferedCodecsOrdered = $state({value: getStoredPreferedCodecsOrdered()}) 23 | 24 | export const usePreferedCodecsOrderedStorage = () => { 25 | $effect(() => { 26 | localStorage.setItem("codecs-list", JSON.stringify(preferedCodecsOrdered.value)) 27 | }) 28 | } 29 | 30 | export function restoreDefaultCodecs() { 31 | preferedCodecsOrdered.value = DEFAULT_PREFERED_CODECS; 32 | } 33 | 34 | function getStoredPreferedCodecsOrdered() { 35 | 36 | if (browser) { 37 | const stored: string[] = JSON.parse(localStorage.getItem("codecs-list") ?? '[]') 38 | 39 | if (stored && stored.length > 0) { 40 | return stored 41 | } 42 | 43 | } 44 | 45 | return DEFAULT_PREFERED_CODECS 46 | 47 | } 48 | 49 | try { 50 | getSortedVideoCodecs().forEach(codec => { 51 | console.log(codec.mimeType); 52 | }) 53 | } catch (e) { 54 | console.error("Error getting codecs", e); 55 | } 56 | 57 | export function getSortedVideoCodecs() { 58 | 59 | const codecs = RTCRtpReceiver.getCapabilities("video")?.codecs; 60 | 61 | if (!codecs) return []; 62 | 63 | console.log(preferedCodecsOrdered.value) 64 | 65 | return codecs.sort((a, b) => { 66 | const indexA = preferedCodecsOrdered.value.indexOf(a.mimeType); 67 | const indexB = preferedCodecsOrdered.value.indexOf(b.mimeType); 68 | const orderA = indexA >= 0 ? indexA : Number.MAX_VALUE; 69 | const orderB = indexB >= 0 ? indexB : Number.MAX_VALUE; 70 | return orderA - orderB; 71 | }); 72 | } 73 | -------------------------------------------------------------------------------- /frontend/src/lib/webrtc/stream/stream_signal_hook.svelte.ts: -------------------------------------------------------------------------------- 1 | export interface SignalingData { 2 | type: 'offer' | 'answer' | 'candidate'; 3 | offer?: RTCSessionDescriptionInit; 4 | answer?: RTCSessionDescriptionInit; 5 | candidate?: RTCIceCandidateInit; 6 | role: 'host' | 'client'; 7 | } 8 | 9 | export const consumingStream = $state({value:false}) 10 | 11 | export function setConsumingStream(value: boolean) { 12 | consumingStream.value = value 13 | } 14 | 15 | export function getConsumingStream() { 16 | return consumingStream.value 17 | } 18 | 19 | export const streaming = $state({value: false}) 20 | 21 | export function setStreaming(value: boolean) { 22 | streaming.value = value 23 | } -------------------------------------------------------------------------------- /frontend/src/lib/webrtc/stun_servers.ts: -------------------------------------------------------------------------------- 1 | import { get, writable } from 'svelte/store'; 2 | import type { ServersConfig, ICEServer } from '$lib/webrtc/ice'; 3 | 4 | const defaultStunServers = [ 5 | 'stun:stun.l.google.com:19302', 6 | 'stun:stun.ipfire.org:3478', 7 | 'stun:stun.l.google.com:19305' 8 | ]; 9 | 10 | const defaultStunConfig: Readonly = { 11 | default: { 12 | urls: defaultStunServers 13 | } 14 | }; 15 | 16 | const stunServersStore = writable( 17 | JSON.parse(localStorage.getItem('stunServers') ?? 'false') || defaultStunConfig 18 | ); 19 | 20 | stunServersStore.subscribe((stunServers) => 21 | localStorage.setItem('stunServers', JSON.stringify(stunServers)) 22 | ); 23 | 24 | function removeServerFromGroup(group: string, url: string) { 25 | stunServersStore.update((stunServers) => { 26 | stunServers[group].urls = stunServers[group].urls.filter((server) => server !== url); 27 | return stunServers; 28 | }); 29 | } 30 | 31 | function modifyGroup(name: string, newName?: string, username?: string, credential?: string) { 32 | 33 | console.log({name, newName, username, credential}); 34 | if (newName) { 35 | stunServersStore.update((stunServers) => { 36 | stunServers[newName] = stunServers[name]; 37 | if (username) stunServers[newName].username = username; 38 | if (credential) stunServers[newName].credential = credential; 39 | delete stunServers[name]; 40 | return stunServers; 41 | }); 42 | 43 | return; 44 | } 45 | 46 | stunServersStore.update((stunServers) => { 47 | stunServers[name].username = username; 48 | stunServers[name].credential = credential; 49 | return stunServers; 50 | }); 51 | } 52 | 53 | function addServerToGroup(group: string, url: string) { 54 | stunServersStore.update((stunServers) => { 55 | stunServers[group].urls.push('stun:' + url); 56 | return stunServers; 57 | }); 58 | } 59 | 60 | function createServerGroup(name: string, username?: string, credential?: string) { 61 | const newServer = { 62 | [name]: { 63 | urls: [], 64 | username: username, 65 | credential: credential 66 | } 67 | }; 68 | stunServersStore.update((stunServers) => { 69 | return { 70 | ...stunServers, 71 | ...newServer 72 | }; 73 | }); 74 | } 75 | 76 | function deleteServerGroup(name: string) { 77 | stunServersStore.update((stunServers) => { 78 | delete stunServers[name]; 79 | return stunServers; 80 | }); 81 | } 82 | 83 | function exportStunServers(): ICEServer[] { 84 | const servers = get(stunServersStore); 85 | const serversArray = Object.keys(servers).map((key) => { 86 | return { 87 | urls: servers[key].urls, 88 | ...(servers[key].username && { username: servers[key].username }), 89 | ...(servers[key].credential && { credential: servers[key].credential }) 90 | }; 91 | }); 92 | 93 | return serversArray; 94 | } 95 | 96 | export { 97 | stunServersStore, 98 | addServerToGroup, 99 | removeServerFromGroup, 100 | createServerGroup, 101 | deleteServerGroup, 102 | modifyGroup, 103 | exportStunServers, 104 | defaultStunConfig 105 | }; 106 | -------------------------------------------------------------------------------- /frontend/src/lib/webrtc/turn_servers.ts: -------------------------------------------------------------------------------- 1 | import { get, writable } from 'svelte/store'; 2 | import type { ServersConfig, ICEServer } from '$lib/webrtc/ice'; 3 | 4 | const defaultTurnConfig: Readonly = { 5 | 6 | }; 7 | 8 | const turnServersStore = writable( 9 | JSON.parse(localStorage.getItem('turnServers') ?? 'false') || defaultTurnConfig 10 | ); 11 | 12 | turnServersStore.subscribe((turnServers) => 13 | localStorage.setItem('turnServers', JSON.stringify(turnServers)) 14 | ); 15 | 16 | function removeServerFromGroup(group: string, url: string) { 17 | turnServersStore.update((turnServers) => { 18 | turnServers[group].urls = turnServers[group].urls.filter((server) => server !== url); 19 | return turnServers; 20 | }); 21 | } 22 | 23 | function modifyGroup(name: string, newName?: string, username?: string, credential?: string) { 24 | if (newName) { 25 | turnServersStore.update((turnServers) => { 26 | turnServers[newName] = turnServers[name]; 27 | 28 | turnServers[newName].username = username; 29 | turnServers[newName].credential = credential; 30 | delete turnServers[name]; 31 | return turnServers; 32 | }); 33 | 34 | return; 35 | } 36 | 37 | turnServersStore.update((turnServers) => { 38 | turnServers[name].username = username; 39 | turnServers[name].credential = credential; 40 | return turnServers; 41 | }); 42 | } 43 | 44 | function addServerToGroup(group: string, url: string) { 45 | turnServersStore.update((turnServers) => { 46 | turnServers[group].urls.push('turn:' + url); 47 | return turnServers; 48 | }); 49 | } 50 | 51 | function createServerGroup(name: string, username?: string, credential?: string) { 52 | const newServer = { 53 | [name]: { 54 | urls: [], 55 | username: username, 56 | credential: credential 57 | } 58 | }; 59 | turnServersStore.update((turnServers) => { 60 | return { 61 | ...turnServers, 62 | ...newServer 63 | }; 64 | }); 65 | } 66 | 67 | function deleteServerGroup(name: string) { 68 | turnServersStore.update((turnServers) => { 69 | delete turnServers[name]; 70 | return turnServers; 71 | }); 72 | } 73 | 74 | function exportTurnServers(): ICEServer[] { 75 | const servers = get(turnServersStore); 76 | const serversArray = Object.keys(servers).map((key) => { 77 | return { 78 | urls: servers[key].urls, 79 | ...(servers[key].username && { username: servers[key].username }), 80 | ...(servers[key].credential && { credential: servers[key].credential }) 81 | }; 82 | }); 83 | 84 | return serversArray; 85 | } 86 | 87 | export { 88 | turnServersStore, 89 | addServerToGroup, 90 | removeServerFromGroup, 91 | createServerGroup, 92 | deleteServerGroup, 93 | modifyGroup, 94 | exportTurnServers, 95 | defaultTurnConfig 96 | }; 97 | -------------------------------------------------------------------------------- /frontend/src/lib/websocket/ws.ts: -------------------------------------------------------------------------------- 1 | class WS extends WebSocket { 2 | 3 | private static url = "ws://localhost:8080/ws" 4 | 5 | static #instance: WS | null; 6 | private constructor() { 7 | try {super(WS.url)} catch (e) { 8 | console.error(e) 9 | } 10 | } 11 | 12 | 13 | public static get instance(): WS { 14 | if (!WS.#instance) { 15 | WS.#instance = new WS(); 16 | } 17 | 18 | return WS.#instance; 19 | } 20 | 21 | public close(code?: number, reason?: string) { 22 | this.close(code, reason) 23 | WS.#instance = null; 24 | } 25 | } 26 | 27 | const ws = () => WS.instance 28 | 29 | export default ws -------------------------------------------------------------------------------- /frontend/src/routes/+error.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 |

6 | 9 | {page.status} 10 | 11 | - 12 | 15 | {page.error?.message} 16 | 17 |

18 | -------------------------------------------------------------------------------- /frontend/src/routes/+layout.svelte: -------------------------------------------------------------------------------- 1 | 29 | 30 | 31 | LibreRemotePlay - Web Client 32 | 33 | 34 | 35 | 66 | 67 | 68 |
69 |
70 | {@render children?.()} 71 |
72 |
73 |
74 | 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /frontend/src/routes/+layout.ts: -------------------------------------------------------------------------------- 1 | export const ssr = false; 2 | export const prerender = true; 3 | export const trailingSlash = 'always'; 4 | 5 | import { browser } from '$app/environment' 6 | import '$lib/i18n/i18n' // Import to initialize. Important :) 7 | import { getLocaleFromNavigator, locale, waitLocale } from 'svelte-i18n' 8 | import type { LayoutLoad } from './$types' 9 | import { getLocaleFromLocalStorage } from '$lib/i18n/i18n'; 10 | 11 | export const load: LayoutLoad = async () => { 12 | if (browser) { 13 | locale.set(getLocaleFromLocalStorage() ?? getLocaleFromNavigator()) 14 | } 15 | await waitLocale() 16 | } -------------------------------------------------------------------------------- /frontend/src/routes/+page.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 16 | 17 |

18 | {$_('main_title_choose')} 21 | 22 | {$_('main_title_your')} 23 | 24 | {$_('main_title_role')} 28 |

29 |
30 | {#if !onwebsite} 31 |
34 |
35 |

{$_('host_card_title')}

36 |

{$_('host_card_description')}

37 | {$_('host_card_cta')} 38 |
39 |
40 | {/if} 41 |
44 |
45 |

{$_('client_card_title')}

46 |

{$_('client_card_description')}

47 | {$_('client_card_cta')} 48 |
49 |
50 |
51 | -------------------------------------------------------------------------------- /frontend/src/routes/mode/+layout.svelte: -------------------------------------------------------------------------------- 1 | 44 | 45 | 46 | 47 | {@render children?.()} 48 | -------------------------------------------------------------------------------- /frontend/src/routes/mode/client/+layout.svelte: -------------------------------------------------------------------------------- 1 | 23 | 24 | {@render children?.()} 25 | 26 | 27 | 28 | 34 | -------------------------------------------------------------------------------- /frontend/src/routes/mode/client/connection/+page.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 |
18 | {#if consumingStream.value} 19 | 21 | 22 | {:else} 23 | 24 | 25 | 26 | 28 | 29 | {/if} 30 |
31 | 32 | -------------------------------------------------------------------------------- /frontend/src/routes/mode/config/+page.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 |

11 | {$_('config_title')} 14 | 15 |

16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | -------------------------------------------------------------------------------- /frontend/src/routes/mode/config/StunServers.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 |
9 |
10 | 13 | 14 | {$_('advance')} 15 | 16 |
17 | {$_('server-list-link')} 23 | 24 |
25 | 26 |
27 |
28 |
29 | -------------------------------------------------------------------------------- /frontend/src/routes/mode/config/TurnServers.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 |
9 |
10 | 13 | 14 | {$_('advance')} 15 | 16 | 17 |
18 | 19 | {$_('selfhost-turn-link')} 25 | 26 |
27 | 28 |
29 |
30 |
31 | -------------------------------------------------------------------------------- /frontend/src/routes/mode/config/ViGEmDownload.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 |
10 |
11 | ViGEmBus Driver 12 |
13 | 14 |

15 | You need to have installed to use the Host mode. 16 |

17 |

18 | If you already have it installed, ignore this, but if you skipped the installation, you can 19 | install it here. 20 |

21 | 46 |
47 | -------------------------------------------------------------------------------- /frontend/src/routes/mode/config/advanced/+layout.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | 11 | 12 | {@render children?.()} -------------------------------------------------------------------------------- /frontend/src/routes/mode/config/advanced/stun/+page.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /frontend/src/routes/mode/config/advanced/turn/+page.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /frontend/src/routes/mode/host/+page.svelte: -------------------------------------------------------------------------------- 1 | 23 | 24 |

25 | {$_('host_card_title')} 28 | 29 |

30 | 31 |
34 |
35 |
    36 |
  1. 37 | {#if !generatedCode} 38 |
    41 | {:else} 42 | 45 | 56 | 57 | {/if} 58 | 63 | 64 |

    65 | {$_('get-your-host-code')} 66 |

    67 |
    68 | 77 | 80 |
    81 |
  2. 82 |
  3. 83 |
    86 | 91 |

    92 | {$_('share-the-code-with-your-client')} 93 |

    94 |
  4. 95 |
    96 |
97 |
98 |
99 | -------------------------------------------------------------------------------- /frontend/src/routes/mode/host/connection/+page.svelte: -------------------------------------------------------------------------------- 1 | 46 | 47 |
48 |

49 | {$_('toogle_devices')} 50 |

51 |
52 | 55 | 58 |
59 |
60 | 61 | 62 |
63 |

{$_('relay-title')}

64 |

{$_('go-browser')}

65 |

{$_('warning-go-browser')}

66 |
67 |
68 | 69 | 70 |
71 |
72 |

{$_('resolutions')}

73 | 85 |
86 |
87 |

{$_('framerate')}

88 |
89 |
90 |

{$_('ideal-framerate')}

91 | 100 | 108 |
109 |
110 |

{$_('max-framerate')}

111 | 120 | 128 |
129 |
130 |
131 |
132 | 133 | 136 |
137 | 138 | 147 | -------------------------------------------------------------------------------- /frontend/src/service-worker.js: -------------------------------------------------------------------------------- 1 | /// 2 | import { build, files, version } from '$service-worker'; 3 | 4 | // Create a unique cache name for this deployment 5 | const CACHE = `cache-${version}`; 6 | 7 | const ASSETS = [ 8 | ...build, // the app itself 9 | ...files // everything in `static` 10 | ]; 11 | 12 | self.addEventListener('install', (event) => { 13 | // Create a new cache and add all files to it 14 | async function addFilesToCache() { 15 | const cache = await caches.open(CACHE); 16 | await cache.addAll(ASSETS); 17 | } 18 | 19 | event.waitUntil(addFilesToCache()); 20 | }); 21 | 22 | self.addEventListener('activate', (event) => { 23 | // Remove previous cached data from disk 24 | async function deleteOldCaches() { 25 | for (const key of await caches.keys()) { 26 | if (key !== CACHE) await caches.delete(key); 27 | } 28 | } 29 | 30 | event.waitUntil(deleteOldCaches()); 31 | }); 32 | 33 | self.addEventListener('fetch', (event) => { 34 | // ignore POST requests etc 35 | if (event.request.method !== 'GET') return; 36 | 37 | async function respond() { 38 | const url = new URL(event.request.url); 39 | const cache = await caches.open(CACHE); 40 | 41 | // `build`/`files` can always be served from the cache 42 | if (ASSETS.includes(url.pathname)) { 43 | const response = await cache.match(url.pathname); 44 | 45 | if (response) { 46 | return response; 47 | } 48 | } 49 | 50 | // for everything else, try the network first, but 51 | // fall back to the cache if we're offline 52 | try { 53 | const response = await fetch(event.request); 54 | 55 | // if we're offline, fetch can return a value that is not a Response 56 | // instead of throwing - and we can't pass this non-Response to respondWith 57 | if (!(response instanceof Response)) { 58 | throw new Error('invalid response from fetch'); 59 | } 60 | 61 | if (response.status === 200) { 62 | cache.put(event.request, response.clone()); 63 | } 64 | 65 | return response; 66 | } catch (err) { 67 | const response = await cache.match(event.request); 68 | 69 | if (response) { 70 | return response; 71 | } 72 | 73 | // if there's no cache, then just error out 74 | // as there is nothing we can do to respond to this request 75 | throw err; 76 | } 77 | } 78 | 79 | event.respondWith(respond()); 80 | }); -------------------------------------------------------------------------------- /frontend/static/AppImages.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/AppImages.zip -------------------------------------------------------------------------------- /frontend/static/AppImages/android/android-launchericon-144-144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/AppImages/android/android-launchericon-144-144.png -------------------------------------------------------------------------------- /frontend/static/AppImages/android/android-launchericon-192-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/AppImages/android/android-launchericon-192-192.png -------------------------------------------------------------------------------- /frontend/static/AppImages/android/android-launchericon-48-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/AppImages/android/android-launchericon-48-48.png -------------------------------------------------------------------------------- /frontend/static/AppImages/android/android-launchericon-512-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/AppImages/android/android-launchericon-512-512.png -------------------------------------------------------------------------------- /frontend/static/AppImages/android/android-launchericon-72-72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/AppImages/android/android-launchericon-72-72.png -------------------------------------------------------------------------------- /frontend/static/AppImages/android/android-launchericon-96-96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/AppImages/android/android-launchericon-96-96.png -------------------------------------------------------------------------------- /frontend/static/AppImages/ios/100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/AppImages/ios/100.png -------------------------------------------------------------------------------- /frontend/static/AppImages/ios/1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/AppImages/ios/1024.png -------------------------------------------------------------------------------- /frontend/static/AppImages/ios/114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/AppImages/ios/114.png -------------------------------------------------------------------------------- /frontend/static/AppImages/ios/120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/AppImages/ios/120.png -------------------------------------------------------------------------------- /frontend/static/AppImages/ios/128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/AppImages/ios/128.png -------------------------------------------------------------------------------- /frontend/static/AppImages/ios/144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/AppImages/ios/144.png -------------------------------------------------------------------------------- /frontend/static/AppImages/ios/152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/AppImages/ios/152.png -------------------------------------------------------------------------------- /frontend/static/AppImages/ios/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/AppImages/ios/16.png -------------------------------------------------------------------------------- /frontend/static/AppImages/ios/167.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/AppImages/ios/167.png -------------------------------------------------------------------------------- /frontend/static/AppImages/ios/180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/AppImages/ios/180.png -------------------------------------------------------------------------------- /frontend/static/AppImages/ios/192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/AppImages/ios/192.png -------------------------------------------------------------------------------- /frontend/static/AppImages/ios/20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/AppImages/ios/20.png -------------------------------------------------------------------------------- /frontend/static/AppImages/ios/256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/AppImages/ios/256.png -------------------------------------------------------------------------------- /frontend/static/AppImages/ios/29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/AppImages/ios/29.png -------------------------------------------------------------------------------- /frontend/static/AppImages/ios/32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/AppImages/ios/32.png -------------------------------------------------------------------------------- /frontend/static/AppImages/ios/40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/AppImages/ios/40.png -------------------------------------------------------------------------------- /frontend/static/AppImages/ios/50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/AppImages/ios/50.png -------------------------------------------------------------------------------- /frontend/static/AppImages/ios/512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/AppImages/ios/512.png -------------------------------------------------------------------------------- /frontend/static/AppImages/ios/57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/AppImages/ios/57.png -------------------------------------------------------------------------------- /frontend/static/AppImages/ios/58.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/AppImages/ios/58.png -------------------------------------------------------------------------------- /frontend/static/AppImages/ios/60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/AppImages/ios/60.png -------------------------------------------------------------------------------- /frontend/static/AppImages/ios/64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/AppImages/ios/64.png -------------------------------------------------------------------------------- /frontend/static/AppImages/ios/72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/AppImages/ios/72.png -------------------------------------------------------------------------------- /frontend/static/AppImages/ios/76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/AppImages/ios/76.png -------------------------------------------------------------------------------- /frontend/static/AppImages/ios/80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/AppImages/ios/80.png -------------------------------------------------------------------------------- /frontend/static/AppImages/ios/87.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/AppImages/ios/87.png -------------------------------------------------------------------------------- /frontend/static/AppImages/windows11/LargeTile.scale-100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/AppImages/windows11/LargeTile.scale-100.png -------------------------------------------------------------------------------- /frontend/static/AppImages/windows11/LargeTile.scale-125.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/AppImages/windows11/LargeTile.scale-125.png -------------------------------------------------------------------------------- /frontend/static/AppImages/windows11/LargeTile.scale-150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/AppImages/windows11/LargeTile.scale-150.png -------------------------------------------------------------------------------- /frontend/static/AppImages/windows11/LargeTile.scale-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/AppImages/windows11/LargeTile.scale-200.png -------------------------------------------------------------------------------- /frontend/static/AppImages/windows11/LargeTile.scale-400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/AppImages/windows11/LargeTile.scale-400.png -------------------------------------------------------------------------------- /frontend/static/AppImages/windows11/SmallTile.scale-100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/AppImages/windows11/SmallTile.scale-100.png -------------------------------------------------------------------------------- /frontend/static/AppImages/windows11/SmallTile.scale-125.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/AppImages/windows11/SmallTile.scale-125.png -------------------------------------------------------------------------------- /frontend/static/AppImages/windows11/SmallTile.scale-150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/AppImages/windows11/SmallTile.scale-150.png -------------------------------------------------------------------------------- /frontend/static/AppImages/windows11/SmallTile.scale-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/AppImages/windows11/SmallTile.scale-200.png -------------------------------------------------------------------------------- /frontend/static/AppImages/windows11/SmallTile.scale-400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/AppImages/windows11/SmallTile.scale-400.png -------------------------------------------------------------------------------- /frontend/static/AppImages/windows11/SplashScreen.scale-100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/AppImages/windows11/SplashScreen.scale-100.png -------------------------------------------------------------------------------- /frontend/static/AppImages/windows11/SplashScreen.scale-125.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/AppImages/windows11/SplashScreen.scale-125.png -------------------------------------------------------------------------------- /frontend/static/AppImages/windows11/SplashScreen.scale-150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/AppImages/windows11/SplashScreen.scale-150.png -------------------------------------------------------------------------------- /frontend/static/AppImages/windows11/SplashScreen.scale-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/AppImages/windows11/SplashScreen.scale-200.png -------------------------------------------------------------------------------- /frontend/static/AppImages/windows11/SplashScreen.scale-400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/AppImages/windows11/SplashScreen.scale-400.png -------------------------------------------------------------------------------- /frontend/static/AppImages/windows11/Square150x150Logo.scale-100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/AppImages/windows11/Square150x150Logo.scale-100.png -------------------------------------------------------------------------------- /frontend/static/AppImages/windows11/Square150x150Logo.scale-125.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/AppImages/windows11/Square150x150Logo.scale-125.png -------------------------------------------------------------------------------- /frontend/static/AppImages/windows11/Square150x150Logo.scale-150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/AppImages/windows11/Square150x150Logo.scale-150.png -------------------------------------------------------------------------------- /frontend/static/AppImages/windows11/Square150x150Logo.scale-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/AppImages/windows11/Square150x150Logo.scale-200.png -------------------------------------------------------------------------------- /frontend/static/AppImages/windows11/Square150x150Logo.scale-400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/AppImages/windows11/Square150x150Logo.scale-400.png -------------------------------------------------------------------------------- /frontend/static/AppImages/windows11/Square44x44Logo.altform-lightunplated_targetsize-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/AppImages/windows11/Square44x44Logo.altform-lightunplated_targetsize-16.png -------------------------------------------------------------------------------- /frontend/static/AppImages/windows11/Square44x44Logo.altform-lightunplated_targetsize-20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/AppImages/windows11/Square44x44Logo.altform-lightunplated_targetsize-20.png -------------------------------------------------------------------------------- /frontend/static/AppImages/windows11/Square44x44Logo.altform-lightunplated_targetsize-24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/AppImages/windows11/Square44x44Logo.altform-lightunplated_targetsize-24.png -------------------------------------------------------------------------------- /frontend/static/AppImages/windows11/Square44x44Logo.altform-lightunplated_targetsize-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/AppImages/windows11/Square44x44Logo.altform-lightunplated_targetsize-256.png -------------------------------------------------------------------------------- /frontend/static/AppImages/windows11/Square44x44Logo.altform-lightunplated_targetsize-30.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/AppImages/windows11/Square44x44Logo.altform-lightunplated_targetsize-30.png -------------------------------------------------------------------------------- /frontend/static/AppImages/windows11/Square44x44Logo.altform-lightunplated_targetsize-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/AppImages/windows11/Square44x44Logo.altform-lightunplated_targetsize-32.png -------------------------------------------------------------------------------- /frontend/static/AppImages/windows11/Square44x44Logo.altform-lightunplated_targetsize-36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/AppImages/windows11/Square44x44Logo.altform-lightunplated_targetsize-36.png -------------------------------------------------------------------------------- /frontend/static/AppImages/windows11/Square44x44Logo.altform-lightunplated_targetsize-40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/AppImages/windows11/Square44x44Logo.altform-lightunplated_targetsize-40.png -------------------------------------------------------------------------------- /frontend/static/AppImages/windows11/Square44x44Logo.altform-lightunplated_targetsize-44.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/AppImages/windows11/Square44x44Logo.altform-lightunplated_targetsize-44.png -------------------------------------------------------------------------------- /frontend/static/AppImages/windows11/Square44x44Logo.altform-lightunplated_targetsize-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/AppImages/windows11/Square44x44Logo.altform-lightunplated_targetsize-48.png -------------------------------------------------------------------------------- /frontend/static/AppImages/windows11/Square44x44Logo.altform-lightunplated_targetsize-60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/AppImages/windows11/Square44x44Logo.altform-lightunplated_targetsize-60.png -------------------------------------------------------------------------------- /frontend/static/AppImages/windows11/Square44x44Logo.altform-lightunplated_targetsize-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/AppImages/windows11/Square44x44Logo.altform-lightunplated_targetsize-64.png -------------------------------------------------------------------------------- /frontend/static/AppImages/windows11/Square44x44Logo.altform-lightunplated_targetsize-72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/AppImages/windows11/Square44x44Logo.altform-lightunplated_targetsize-72.png -------------------------------------------------------------------------------- /frontend/static/AppImages/windows11/Square44x44Logo.altform-lightunplated_targetsize-80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/AppImages/windows11/Square44x44Logo.altform-lightunplated_targetsize-80.png -------------------------------------------------------------------------------- /frontend/static/AppImages/windows11/Square44x44Logo.altform-lightunplated_targetsize-96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/AppImages/windows11/Square44x44Logo.altform-lightunplated_targetsize-96.png -------------------------------------------------------------------------------- /frontend/static/AppImages/windows11/Square44x44Logo.altform-unplated_targetsize-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/AppImages/windows11/Square44x44Logo.altform-unplated_targetsize-16.png -------------------------------------------------------------------------------- /frontend/static/AppImages/windows11/Square44x44Logo.altform-unplated_targetsize-20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/AppImages/windows11/Square44x44Logo.altform-unplated_targetsize-20.png -------------------------------------------------------------------------------- /frontend/static/AppImages/windows11/Square44x44Logo.altform-unplated_targetsize-24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/AppImages/windows11/Square44x44Logo.altform-unplated_targetsize-24.png -------------------------------------------------------------------------------- /frontend/static/AppImages/windows11/Square44x44Logo.altform-unplated_targetsize-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/AppImages/windows11/Square44x44Logo.altform-unplated_targetsize-256.png -------------------------------------------------------------------------------- /frontend/static/AppImages/windows11/Square44x44Logo.altform-unplated_targetsize-30.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/AppImages/windows11/Square44x44Logo.altform-unplated_targetsize-30.png -------------------------------------------------------------------------------- /frontend/static/AppImages/windows11/Square44x44Logo.altform-unplated_targetsize-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/AppImages/windows11/Square44x44Logo.altform-unplated_targetsize-32.png -------------------------------------------------------------------------------- /frontend/static/AppImages/windows11/Square44x44Logo.altform-unplated_targetsize-36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/AppImages/windows11/Square44x44Logo.altform-unplated_targetsize-36.png -------------------------------------------------------------------------------- /frontend/static/AppImages/windows11/Square44x44Logo.altform-unplated_targetsize-40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/AppImages/windows11/Square44x44Logo.altform-unplated_targetsize-40.png -------------------------------------------------------------------------------- /frontend/static/AppImages/windows11/Square44x44Logo.altform-unplated_targetsize-44.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/AppImages/windows11/Square44x44Logo.altform-unplated_targetsize-44.png -------------------------------------------------------------------------------- /frontend/static/AppImages/windows11/Square44x44Logo.altform-unplated_targetsize-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/AppImages/windows11/Square44x44Logo.altform-unplated_targetsize-48.png -------------------------------------------------------------------------------- /frontend/static/AppImages/windows11/Square44x44Logo.altform-unplated_targetsize-60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/AppImages/windows11/Square44x44Logo.altform-unplated_targetsize-60.png -------------------------------------------------------------------------------- /frontend/static/AppImages/windows11/Square44x44Logo.altform-unplated_targetsize-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/AppImages/windows11/Square44x44Logo.altform-unplated_targetsize-64.png -------------------------------------------------------------------------------- /frontend/static/AppImages/windows11/Square44x44Logo.altform-unplated_targetsize-72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/AppImages/windows11/Square44x44Logo.altform-unplated_targetsize-72.png -------------------------------------------------------------------------------- /frontend/static/AppImages/windows11/Square44x44Logo.altform-unplated_targetsize-80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/AppImages/windows11/Square44x44Logo.altform-unplated_targetsize-80.png -------------------------------------------------------------------------------- /frontend/static/AppImages/windows11/Square44x44Logo.altform-unplated_targetsize-96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/AppImages/windows11/Square44x44Logo.altform-unplated_targetsize-96.png -------------------------------------------------------------------------------- /frontend/static/AppImages/windows11/Square44x44Logo.scale-100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/AppImages/windows11/Square44x44Logo.scale-100.png -------------------------------------------------------------------------------- /frontend/static/AppImages/windows11/Square44x44Logo.scale-125.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/AppImages/windows11/Square44x44Logo.scale-125.png -------------------------------------------------------------------------------- /frontend/static/AppImages/windows11/Square44x44Logo.scale-150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/AppImages/windows11/Square44x44Logo.scale-150.png -------------------------------------------------------------------------------- /frontend/static/AppImages/windows11/Square44x44Logo.scale-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/AppImages/windows11/Square44x44Logo.scale-200.png -------------------------------------------------------------------------------- /frontend/static/AppImages/windows11/Square44x44Logo.scale-400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/AppImages/windows11/Square44x44Logo.scale-400.png -------------------------------------------------------------------------------- /frontend/static/AppImages/windows11/Square44x44Logo.targetsize-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/AppImages/windows11/Square44x44Logo.targetsize-16.png -------------------------------------------------------------------------------- /frontend/static/AppImages/windows11/Square44x44Logo.targetsize-20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/AppImages/windows11/Square44x44Logo.targetsize-20.png -------------------------------------------------------------------------------- /frontend/static/AppImages/windows11/Square44x44Logo.targetsize-24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/AppImages/windows11/Square44x44Logo.targetsize-24.png -------------------------------------------------------------------------------- /frontend/static/AppImages/windows11/Square44x44Logo.targetsize-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/AppImages/windows11/Square44x44Logo.targetsize-256.png -------------------------------------------------------------------------------- /frontend/static/AppImages/windows11/Square44x44Logo.targetsize-30.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/AppImages/windows11/Square44x44Logo.targetsize-30.png -------------------------------------------------------------------------------- /frontend/static/AppImages/windows11/Square44x44Logo.targetsize-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/AppImages/windows11/Square44x44Logo.targetsize-32.png -------------------------------------------------------------------------------- /frontend/static/AppImages/windows11/Square44x44Logo.targetsize-36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/AppImages/windows11/Square44x44Logo.targetsize-36.png -------------------------------------------------------------------------------- /frontend/static/AppImages/windows11/Square44x44Logo.targetsize-40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/AppImages/windows11/Square44x44Logo.targetsize-40.png -------------------------------------------------------------------------------- /frontend/static/AppImages/windows11/Square44x44Logo.targetsize-44.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/AppImages/windows11/Square44x44Logo.targetsize-44.png -------------------------------------------------------------------------------- /frontend/static/AppImages/windows11/Square44x44Logo.targetsize-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/AppImages/windows11/Square44x44Logo.targetsize-48.png -------------------------------------------------------------------------------- /frontend/static/AppImages/windows11/Square44x44Logo.targetsize-60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/AppImages/windows11/Square44x44Logo.targetsize-60.png -------------------------------------------------------------------------------- /frontend/static/AppImages/windows11/Square44x44Logo.targetsize-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/AppImages/windows11/Square44x44Logo.targetsize-64.png -------------------------------------------------------------------------------- /frontend/static/AppImages/windows11/Square44x44Logo.targetsize-72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/AppImages/windows11/Square44x44Logo.targetsize-72.png -------------------------------------------------------------------------------- /frontend/static/AppImages/windows11/Square44x44Logo.targetsize-80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/AppImages/windows11/Square44x44Logo.targetsize-80.png -------------------------------------------------------------------------------- /frontend/static/AppImages/windows11/Square44x44Logo.targetsize-96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/AppImages/windows11/Square44x44Logo.targetsize-96.png -------------------------------------------------------------------------------- /frontend/static/AppImages/windows11/StoreLogo.scale-100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/AppImages/windows11/StoreLogo.scale-100.png -------------------------------------------------------------------------------- /frontend/static/AppImages/windows11/StoreLogo.scale-125.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/AppImages/windows11/StoreLogo.scale-125.png -------------------------------------------------------------------------------- /frontend/static/AppImages/windows11/StoreLogo.scale-150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/AppImages/windows11/StoreLogo.scale-150.png -------------------------------------------------------------------------------- /frontend/static/AppImages/windows11/StoreLogo.scale-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/AppImages/windows11/StoreLogo.scale-200.png -------------------------------------------------------------------------------- /frontend/static/AppImages/windows11/StoreLogo.scale-400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/AppImages/windows11/StoreLogo.scale-400.png -------------------------------------------------------------------------------- /frontend/static/AppImages/windows11/Wide310x150Logo.scale-100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/AppImages/windows11/Wide310x150Logo.scale-100.png -------------------------------------------------------------------------------- /frontend/static/AppImages/windows11/Wide310x150Logo.scale-125.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/AppImages/windows11/Wide310x150Logo.scale-125.png -------------------------------------------------------------------------------- /frontend/static/AppImages/windows11/Wide310x150Logo.scale-150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/AppImages/windows11/Wide310x150Logo.scale-150.png -------------------------------------------------------------------------------- /frontend/static/AppImages/windows11/Wide310x150Logo.scale-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/AppImages/windows11/Wide310x150Logo.scale-200.png -------------------------------------------------------------------------------- /frontend/static/AppImages/windows11/Wide310x150Logo.scale-400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/AppImages/windows11/Wide310x150Logo.scale-400.png -------------------------------------------------------------------------------- /frontend/static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/favicon.png -------------------------------------------------------------------------------- /frontend/static/gamepad.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/static/sounds/open_modal.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/sounds/open_modal.mp3 -------------------------------------------------------------------------------- /frontend/static/sounds/open_modal.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/sounds/open_modal.wav -------------------------------------------------------------------------------- /frontend/static/sounds/page_transition.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/sounds/page_transition.mp3 -------------------------------------------------------------------------------- /frontend/static/sounds/page_transition.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/sounds/page_transition.wav -------------------------------------------------------------------------------- /frontend/static/wasm/signal.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "compress/gzip" 6 | "encoding/base64" 7 | "encoding/json" 8 | "io" 9 | "syscall/js" 10 | ) 11 | 12 | func main() { 13 | c := make(chan struct{}, 0) 14 | 15 | js.Global().Set("signalEncode", js.FuncOf(signalEncode)) 16 | js.Global().Set("signalDecode", js.FuncOf(signalDecode)) 17 | 18 | <-c 19 | } 20 | 21 | // signalEncode encodes the input in base64 22 | // It can optionally zip the input before encoding 23 | func signalEncode(this js.Value, objStr []js.Value) interface{} { 24 | 25 | obj := new(interface{}) 26 | 27 | json.Unmarshal([]byte(objStr[0].String()), obj) 28 | 29 | b, err := json.Marshal(obj) 30 | if err != nil { 31 | panic(err) 32 | } 33 | 34 | b = signalZip(b) 35 | 36 | return js.ValueOf(base64.StdEncoding.EncodeToString(b)) 37 | } 38 | 39 | // signalDecode decodes the input from base64 40 | // It can optionally unzip the input after decoding 41 | func signalDecode(this js.Value, in []js.Value) interface{} { 42 | b, err := base64.StdEncoding.DecodeString(in[0].String()) 43 | if err != nil { 44 | panic(err) 45 | } 46 | 47 | b = signalUnzip(b) 48 | 49 | obj := new(interface{}) 50 | 51 | err = json.Unmarshal(b, obj) 52 | if err != nil { 53 | panic(err) 54 | } 55 | 56 | b, err = json.Marshal(obj) 57 | 58 | if err != nil { 59 | panic(err) 60 | } 61 | 62 | return js.ValueOf(string(b)) 63 | } 64 | 65 | func signalZip(in []byte) []byte { 66 | var b bytes.Buffer 67 | gz := gzip.NewWriter(&b) 68 | _, err := gz.Write(in) 69 | if err != nil { 70 | panic(err) 71 | } 72 | err = gz.Flush() 73 | if err != nil { 74 | panic(err) 75 | } 76 | err = gz.Close() 77 | if err != nil { 78 | panic(err) 79 | } 80 | return b.Bytes() 81 | } 82 | 83 | func signalUnzip(in []byte) []byte { 84 | var b bytes.Buffer 85 | _, err := b.Write(in) 86 | if err != nil { 87 | panic(err) 88 | } 89 | r, err := gzip.NewReader(&b) 90 | if err != nil { 91 | panic(err) 92 | } 93 | res, err := io.ReadAll(r) 94 | if err != nil { 95 | panic(err) 96 | } 97 | return res 98 | } 99 | -------------------------------------------------------------------------------- /frontend/static/wasm/signal.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/frontend/static/wasm/signal.wasm -------------------------------------------------------------------------------- /frontend/svelte.config.js: -------------------------------------------------------------------------------- 1 | import adapter from "@sveltejs/adapter-static" 2 | import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; 3 | 4 | /** @type {import('@sveltejs/kit').Config} */ 5 | const config = { 6 | // Consult https://kit.svelte.dev/docs/integrations#preprocessors 7 | // for more information about preprocessors 8 | preprocess: vitePreprocess(), 9 | 10 | kit: { 11 | // adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list. 12 | // If your environment is not supported or you settled on a specific environment, switch out the adapter. 13 | // See https://kit.svelte.dev/docs/adapters for more information about adapters. 14 | adapter: adapter({ 15 | pages: 'build', 16 | assets: 'build', 17 | fallback: "index.html", 18 | precompress: true, 19 | strict: true, 20 | }), 21 | serviceWorker: { 22 | register: false, 23 | } 24 | } 25 | }; 26 | 27 | export default config; 28 | -------------------------------------------------------------------------------- /frontend/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: ['./src/**/*.{html,js,svelte,ts}'], 4 | theme: { 5 | extend: {}, 6 | }, 7 | plugins: [require("daisyui")], 8 | daisyui: { 9 | themes: ["corporate"] 10 | }, 11 | } 12 | 13 | -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.svelte-kit/tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "checkJs": true, 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "resolveJsonModule": true, 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "strict": true, 12 | }, 13 | 14 | // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias 15 | // 16 | // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes 17 | // from the referenced tsconfig.json - TypeScript does not merge them in 18 | } 19 | -------------------------------------------------------------------------------- /frontend/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { sveltekit } from '@sveltejs/kit/vite'; 2 | import { defineConfig } from 'vite'; 3 | 4 | export default defineConfig({ 5 | plugins: [ 6 | sveltekit(), 7 | ], 8 | build: { 9 | sourcemap: true, 10 | } 11 | }); 12 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/PiterWeb/RemoteController 2 | 3 | go 1.24.2 4 | 5 | require ( 6 | github.com/coder/websocket v1.8.12 7 | github.com/jbdemonte/virtual-device v1.1.0 8 | github.com/pion/webrtc/v3 v3.3.5 9 | ) 10 | 11 | require github.com/gorilla/websocket v1.5.3 // indirect 12 | 13 | require ( 14 | github.com/PiterWeb/LibreRemotePlaySignals v1.1.0 15 | github.com/bep/debounce v1.2.1 // indirect 16 | github.com/dblohm7/wingoes v0.0.0-20240820181039-f2b84150679e // indirect 17 | github.com/ebitengine/purego v0.8.2 // indirect 18 | github.com/gen2brain/shm v0.1.1 // indirect 19 | github.com/go-ole/go-ole v1.3.0 // indirect 20 | github.com/godbus/dbus/v5 v5.1.0 // indirect 21 | github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect 22 | github.com/jezek/xgb v1.1.1 // indirect 23 | github.com/kbinani/screenshot v0.0.0-20250118074034-a3924b7bbc8c // indirect 24 | github.com/labstack/echo/v4 v4.13.3 // indirect 25 | github.com/labstack/gommon v0.4.2 // indirect 26 | github.com/leaanthony/go-ansi-parser v1.6.1 // indirect 27 | github.com/leaanthony/gosod v1.0.4 // indirect 28 | github.com/leaanthony/slicer v1.6.0 // indirect 29 | github.com/leaanthony/u v1.1.1 // indirect 30 | github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 // indirect 31 | github.com/lxn/win v0.0.0-20210218163916-a377121e959e // indirect 32 | github.com/mattn/go-colorable v0.1.13 // indirect 33 | github.com/mattn/go-isatty v0.0.20 // indirect 34 | github.com/otiai10/gosseract v2.2.1+incompatible // indirect 35 | github.com/otiai10/mint v1.6.3 // indirect 36 | github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect 37 | github.com/pkg/errors v0.9.1 // indirect 38 | github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect 39 | github.com/rivo/uniseg v0.4.7 // indirect 40 | github.com/robotn/xgb v0.10.0 // indirect 41 | github.com/robotn/xgbutil v0.10.0 // indirect 42 | github.com/samber/lo v1.49.1 // indirect 43 | github.com/shirou/gopsutil/v4 v4.25.1 // indirect 44 | github.com/tailscale/win v0.0.0-20250213223159-5992cb43ca35 // indirect 45 | github.com/tklauser/go-sysconf v0.3.14 // indirect 46 | github.com/tklauser/numcpus v0.9.0 // indirect 47 | github.com/tkrajina/go-reflector v0.5.8 // indirect 48 | github.com/valyala/bytebufferpool v1.0.0 // indirect 49 | github.com/valyala/fasttemplate v1.2.2 // indirect 50 | github.com/vcaesar/gops v0.40.0 // indirect 51 | github.com/vcaesar/imgo v0.40.2 // indirect 52 | github.com/vcaesar/keycode v0.10.1 // indirect 53 | github.com/vcaesar/tt v0.20.1 // indirect 54 | github.com/wailsapp/go-webview2 v1.0.19 // indirect 55 | github.com/wailsapp/mimetype v1.4.1 // indirect 56 | github.com/wlynxg/anet v0.0.5 // indirect 57 | github.com/yusufpapurcu/wmi v1.2.4 // indirect 58 | golang.org/x/exp v0.0.0-20250215185904-eff6e970281f // indirect 59 | golang.org/x/image v0.24.0 // indirect 60 | golang.org/x/text v0.22.0 // indirect 61 | ) 62 | 63 | require ( 64 | github.com/davecgh/go-spew v1.1.1 // indirect 65 | github.com/go-vgo/robotgo v0.110.6 66 | github.com/google/uuid v1.6.0 // indirect 67 | github.com/pion/datachannel v1.5.9 // indirect 68 | github.com/pion/dtls/v2 v2.2.12 // indirect 69 | github.com/pion/ice/v2 v2.3.36 // indirect 70 | github.com/pion/interceptor v0.1.37 // indirect 71 | github.com/pion/logging v0.2.2 // indirect 72 | github.com/pion/mdns v0.0.12 // indirect 73 | github.com/pion/randutil v0.1.0 // indirect 74 | github.com/pion/rtcp v1.2.14 // indirect 75 | github.com/pion/rtp v1.8.9 // indirect 76 | github.com/pion/sctp v1.8.33 // indirect 77 | github.com/pion/sdp/v3 v3.0.9 // indirect 78 | github.com/pion/srtp/v2 v2.0.20 // indirect 79 | github.com/pion/stun v0.6.1 // indirect 80 | github.com/pion/transport/v2 v2.2.10 // indirect 81 | github.com/pion/turn/v2 v2.1.6 // indirect 82 | github.com/pmezard/go-difflib v1.0.0 // indirect 83 | github.com/pquerna/ffjson v0.0.0-20190930134022-aa0246cd15f7 84 | github.com/stretchr/testify v1.10.0 // indirect 85 | github.com/wailsapp/wails/v2 v2.10.1 86 | golang.org/x/crypto v0.33.0 // indirect 87 | golang.org/x/net v0.35.0 // indirect 88 | golang.org/x/sys v0.30.0 // indirect 89 | gopkg.in/yaml.v3 v3.0.1 // indirect 90 | ) 91 | -------------------------------------------------------------------------------- /go.work: -------------------------------------------------------------------------------- 1 | go 1.24.2 2 | 3 | use . 4 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "embed" 5 | "log" 6 | 7 | "github.com/PiterWeb/RemoteController/src/bindings" 8 | appLogger "github.com/PiterWeb/RemoteController/src/logger" 9 | "github.com/PiterWeb/RemoteController/src/oninit" 10 | "github.com/wailsapp/wails/v2" 11 | "github.com/wailsapp/wails/v2/pkg/logger" 12 | "github.com/wailsapp/wails/v2/pkg/options" 13 | "github.com/wailsapp/wails/v2/pkg/options/assetserver" 14 | "github.com/wailsapp/wails/v2/pkg/options/windows" 15 | ) 16 | 17 | //go:embed frontend/build/* 18 | var assets embed.FS 19 | 20 | func main() { 21 | 22 | go func() { 23 | 24 | err := oninit.Execute(assets) 25 | 26 | if err != nil { 27 | log.Println(err) 28 | } 29 | 30 | }() 31 | 32 | logFile := appLogger.InitLogger() 33 | 34 | defer logFile.Close() 35 | 36 | log.Println("LibreRemotePlay Starting app") 37 | 38 | // Create an instance of the app structure 39 | app := bindings.NewApp() 40 | 41 | // Create application with options 42 | // Create application with options 43 | err := wails.Run(&options.App{ 44 | Title: "Remote Controller", 45 | Width: 1024, 46 | Height: 768, 47 | DisableResize: false, 48 | Fullscreen: false, 49 | StartHidden: false, 50 | HideWindowOnClose: false, 51 | BackgroundColour: &options.RGBA{R: 75, G: 107, B: 251, A: 255}, 52 | AssetServer: &assetserver.Options{ 53 | Assets: assets, 54 | }, 55 | Menu: nil, 56 | Logger: nil, 57 | LogLevel: logger.DEBUG, 58 | OnStartup: app.Startup, 59 | OnBeforeClose: app.BeforeClose, 60 | OnShutdown: app.Shutdown, 61 | WindowStartState: options.Normal, 62 | EnableDefaultContextMenu: true, 63 | Bind: []interface{}{ 64 | app, 65 | }, 66 | // Windows platform specific options 67 | Windows: &windows.Options{ 68 | WebviewIsTransparent: false, 69 | WindowIsTranslucent: false, 70 | DisableWindowIcon: true, 71 | Theme: windows.Theme(windows.Acrylic), 72 | WebviewUserDataPath: "", 73 | }, 74 | }) 75 | 76 | if err != nil { 77 | log.Println(err) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/bin/ViGEmBus_1.22.0_x64_x86_arm64.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/src/bin/ViGEmBus_1.22.0_x64_x86_arm64.exe -------------------------------------------------------------------------------- /src/bin/ViGEmClient_x64.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/src/bin/ViGEmClient_x64.dll -------------------------------------------------------------------------------- /src/bin/ViGEmClient_x86.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiterWeb/LibreRemotePlay/ae51f21ec420e2a8178a837a8b270242d7f63c34/src/bin/ViGEmClient_x86.dll -------------------------------------------------------------------------------- /src/bin/bin_windows.go: -------------------------------------------------------------------------------- 1 | package bin 2 | 3 | import ( 4 | _ "embed" 5 | ) 6 | 7 | //go:embed ViGEmClient_x64.dll 8 | var ViGEmClient_x64 []byte 9 | 10 | //go:embed ViGEmClient_x86.dll 11 | var ViGEmClient_x86 []byte 12 | 13 | //go:embed ViGEmBus_1.22.0_x64_x86_arm64.exe 14 | var ViGEmBus_exe []byte 15 | -------------------------------------------------------------------------------- /src/bindings/app.go: -------------------------------------------------------------------------------- 1 | // Binding for JS to Go 2 | // This package is responsible for the communication between the JS and Go code. 3 | package bindings 4 | 5 | import ( 6 | "context" 7 | "log" 8 | "strings" 9 | "sync" 10 | 11 | "runtime" 12 | 13 | "github.com/PiterWeb/RemoteController/src/devices/gamepad" 14 | "github.com/PiterWeb/RemoteController/src/devices/keyboard" 15 | net "github.com/PiterWeb/RemoteController/src/net/webrtc" 16 | "github.com/pion/webrtc/v3" 17 | wailsRuntime "github.com/wailsapp/wails/v2/pkg/runtime" 18 | ) 19 | 20 | var triggerEnd chan struct{} = make(chan struct{}) 21 | 22 | var openPeer bool = false 23 | var openPeerMutex sync.Mutex 24 | 25 | // App struct 26 | type App struct { 27 | ctx context.Context 28 | } 29 | 30 | // NewApp creates a new App application struct 31 | func NewApp() *App { 32 | return &App{} 33 | } 34 | 35 | // Startup is called at application Startup 36 | func (a *App) Startup(ctx context.Context) { 37 | // Perform your setup here 38 | a.ctx = ctx 39 | } 40 | 41 | // BeforeClose is called when the application is about to quit, 42 | // either by clicking the window close button or calling runtime.Quit. 43 | // Returning true will cause the application to continue, false will continue shutdown as normal. 44 | func (a *App) BeforeClose(ctx context.Context) (prevent bool) { 45 | 46 | openPeerMutex.Lock() 47 | defer openPeerMutex.Unlock() 48 | 49 | if !openPeer { 50 | prevent = false 51 | return prevent 52 | } 53 | 54 | // Show a dialog to confirm the user wants to quit 55 | option, err := wailsRuntime.MessageDialog(ctx, wailsRuntime.MessageDialogOptions{ 56 | Type: wailsRuntime.QuestionDialog, 57 | Title: "Quit", 58 | Message: "Are you sure you want to quit?", 59 | Buttons: []string{"Yes", "No"}, 60 | DefaultButton: "No", 61 | CancelButton: "No", 62 | }) 63 | 64 | if err != nil { 65 | return a.BeforeClose(ctx) 66 | } 67 | 68 | if option == "Yes" { 69 | prevent = false 70 | return prevent 71 | } 72 | 73 | prevent = true 74 | return prevent 75 | } 76 | 77 | // Shutdown is called at application termination 78 | func (a *App) Shutdown(ctx context.Context) { 79 | // Perform your teardown here 80 | a.TryClosePeerConnection() 81 | close(triggerEnd) 82 | } 83 | 84 | func (a *App) NotifyCreateClient() { 85 | 86 | openPeerMutex.Lock() 87 | defer openPeerMutex.Unlock() 88 | 89 | openPeer = true 90 | println("NotifyCreateClient") 91 | } 92 | 93 | func (a *App) NotifyCloseClient() { 94 | 95 | openPeerMutex.Lock() 96 | defer openPeerMutex.Unlock() 97 | 98 | openPeer = false 99 | println("NotifyCloseClient") 100 | } 101 | 102 | // Create a Host Peer, it receives the offer encoded and returns the encoded answer response 103 | func (a *App) TryCreateHost(ICEServers []webrtc.ICEServer, offerEncoded string) (value string) { 104 | 105 | openPeerMutex.Lock() 106 | defer openPeerMutex.Unlock() 107 | 108 | if openPeer { 109 | triggerEnd <- struct{}{} 110 | } 111 | 112 | openPeer = true 113 | 114 | defer func() { 115 | 116 | if err := recover(); err != nil { 117 | 118 | log.Println(err) 119 | 120 | openPeerMutex.Lock() 121 | defer openPeerMutex.Unlock() 122 | openPeer = false 123 | value = "ERROR" 124 | } 125 | 126 | }() 127 | 128 | answerResponse := make(chan string) 129 | 130 | go net.InitHost(a.ctx, ICEServers, offerEncoded, answerResponse, triggerEnd) 131 | 132 | return <-answerResponse 133 | 134 | } 135 | 136 | // Closes the peer connection and returns a boolean indication if a connection existed and was closed or not 137 | func (a *App) TryClosePeerConnection() bool { 138 | 139 | openPeerMutex.Lock() 140 | defer openPeerMutex.Unlock() 141 | 142 | if !openPeer { 143 | return false 144 | } 145 | 146 | triggerEnd <- struct{}{} 147 | 148 | openPeer = false 149 | 150 | return true 151 | 152 | } 153 | 154 | func (a *App) ToogleGamepad() { 155 | gamepad.GamepadEnabled.Toogle() 156 | } 157 | 158 | func (a *App) IsGamepadEnabled() bool { 159 | return gamepad.GamepadEnabled.IsEnabled() 160 | } 161 | 162 | func (a *App) ToogleKeyboard() { 163 | keyboard.KeyboardEnabled.Toogle() 164 | } 165 | 166 | func (a *App) IsKeyboardEnabled() bool { 167 | return keyboard.KeyboardEnabled.IsEnabled() 168 | } 169 | 170 | func (a *App) GetCurrentOS() string { 171 | return strings.ToUpper(runtime.GOOS) 172 | } 173 | 174 | func (a *App) LogPrintln(info string) { 175 | log.Println(info) 176 | } 177 | -------------------------------------------------------------------------------- /src/bindings/app_darwin.go: -------------------------------------------------------------------------------- 1 | package bindings 2 | 3 | func (a *App) OpenViGEmWizard() (err string) { 4 | 5 | return "" 6 | } 7 | -------------------------------------------------------------------------------- /src/bindings/app_linux.go: -------------------------------------------------------------------------------- 1 | package bindings 2 | 3 | func (a *App) OpenViGEmWizard() (err string) { 4 | 5 | return "" 6 | } 7 | -------------------------------------------------------------------------------- /src/bindings/app_windows.go: -------------------------------------------------------------------------------- 1 | package bindings 2 | 3 | import ( 4 | "github.com/PiterWeb/RemoteController/src/devices/gamepad" 5 | ) 6 | 7 | func (a *App) OpenViGEmWizard() (err string) { 8 | 9 | return gamepad.OpenViGEmWizard().Error() 10 | 11 | } 12 | -------------------------------------------------------------------------------- /src/devices/device.go: -------------------------------------------------------------------------------- 1 | package devices 2 | 3 | import ( 4 | "sync/atomic" 5 | ) 6 | 7 | type DeviceEnabledI interface { 8 | Toogle() 9 | IsEnabled() bool 10 | Enable() *DeviceEnabled 11 | Disable() *DeviceEnabled 12 | } 13 | 14 | type DeviceEnabled struct { 15 | enabled atomic.Int32 16 | } 17 | 18 | func (d *DeviceEnabled) Toogle() { 19 | d.enabled.Store(1 - d.enabled.Load()) 20 | } 21 | 22 | func (d *DeviceEnabled) IsEnabled() bool { 23 | return d.enabled.Load() == 1 24 | } 25 | 26 | func (d *DeviceEnabled) Enable() *DeviceEnabled { 27 | d.enabled.Store(1) 28 | return d 29 | } 30 | 31 | func (d *DeviceEnabled) Disable() *DeviceEnabled { 32 | d.enabled.Store(0) 33 | return d 34 | } 35 | -------------------------------------------------------------------------------- /src/devices/gamepad/common.go: -------------------------------------------------------------------------------- 1 | package gamepad 2 | 3 | import ( 4 | "math" 5 | 6 | "github.com/PiterWeb/RemoteController/src/devices" 7 | ) 8 | 9 | const ( 10 | threshold float64 = 1e-9 11 | ) 12 | 13 | var ( 14 | prevThumbLY float64 15 | prevThumbRY float64 16 | ) 17 | 18 | var GamepadEnabled = new(devices.DeviceEnabled).Enable() 19 | 20 | // Struct for GamepadAPI for XINPUT gamepads 21 | type GamepadAPIXState struct { 22 | Axes [4]float64 23 | Buttons [16]gamepadButton 24 | Connected bool 25 | ID string 26 | Index int 27 | } 28 | 29 | type gamepadButton struct { 30 | Pressed bool 31 | Value float64 32 | } 33 | 34 | func fixLYAxis(value float64) float64 { 35 | 36 | if math.Abs(value-prevThumbLY) <= threshold { 37 | return prevThumbLY 38 | } 39 | 40 | prevThumbLY = -value 41 | return -value 42 | 43 | } 44 | 45 | func fixRYAxis(value float64) float64 { 46 | 47 | if math.Abs(value-prevThumbRY) <= threshold { 48 | return prevThumbRY 49 | } 50 | 51 | prevThumbRY = -value 52 | return -value 53 | 54 | } 55 | -------------------------------------------------------------------------------- /src/devices/gamepad/handler_unix.go: -------------------------------------------------------------------------------- 1 | //go:build unix 2 | 3 | package gamepad 4 | 5 | import ( 6 | "log" 7 | 8 | "github.com/jbdemonte/virtual-device/gamepad" 9 | "github.com/pion/webrtc/v3" 10 | "github.com/pquerna/ffjson/ffjson" 11 | ) 12 | 13 | func HandleGamepad(gamepadChannel *webrtc.DataChannel) { 14 | 15 | if gamepadChannel.Label() != "controller" { 16 | return 17 | } 18 | 19 | var virtualGamepad gamepad.VirtualGamepad 20 | 21 | // Create a virtual device 22 | gamepadChannel.OnOpen(func() { 23 | 24 | var err error 25 | 26 | virtualGamepad, err = generateVirtualDevice() 27 | 28 | if err != nil { 29 | log.Println(err) 30 | } 31 | 32 | }) 33 | 34 | defer func() { 35 | if err := recover(); err != nil { 36 | if virtualGamepad != nil { 37 | virtualGamepad.Unregister() 38 | } 39 | } 40 | }() 41 | 42 | lastPad := GamepadAPIXState{ 43 | Connected: false, 44 | } 45 | 46 | // Update the virtual device 47 | gamepadChannel.OnMessage(func(msg webrtc.DataChannelMessage) { 48 | 49 | if !GamepadEnabled.IsEnabled() { 50 | return 51 | } 52 | 53 | if virtualGamepad == nil { 54 | log.Println("VirtualGamepad is not defined") 55 | return 56 | } 57 | 58 | actualPad := GamepadAPIXState{} 59 | 60 | err := ffjson.Unmarshal(msg.Data, &actualPad) 61 | 62 | if err != nil { 63 | log.Println(err) 64 | return 65 | } 66 | 67 | updateVirtualDevice(virtualGamepad, actualPad, lastPad) 68 | 69 | lastPad = actualPad 70 | 71 | }) 72 | 73 | // Free the virtualGamepad 74 | gamepadChannel.OnClose(func() { 75 | 76 | if virtualGamepad == nil { 77 | return 78 | } 79 | 80 | err := virtualGamepad.Unregister() 81 | 82 | if err != nil { 83 | log.Println(err) 84 | } 85 | 86 | }) 87 | } 88 | 89 | func generateVirtualDevice() (gamepad.VirtualGamepad, error) { 90 | 91 | g := gamepad.NewXBox360() 92 | 93 | err := g.Register() 94 | if err != nil { 95 | return nil, err 96 | } 97 | 98 | return g, nil 99 | 100 | } 101 | 102 | func updateVirtualDevice(virtualGamepad gamepad.VirtualGamepad, actualPad GamepadAPIXState, lastPad GamepadAPIXState) { 103 | 104 | for i, v := range actualPad.Axes { 105 | if actualPad.Axes[i] == lastPad.Axes[i] { 106 | continue 107 | } 108 | 109 | switch i { 110 | case 0: 111 | virtualGamepad.MoveLeftStickX(float32(-fixLYAxis(v))) 112 | case 1: 113 | virtualGamepad.MoveLeftStickY(float32(v)) 114 | case 2: 115 | virtualGamepad.MoveRightStickX(float32(v)) 116 | case 3: 117 | virtualGamepad.MoveRightStickY(float32(-fixRYAxis(v))) 118 | } 119 | 120 | } 121 | 122 | for i := range actualPad.Buttons { 123 | if actualPad.Buttons[i].Pressed == lastPad.Buttons[i].Pressed { 124 | continue 125 | } 126 | 127 | if actualPad.Buttons[i].Pressed { 128 | virtualGamepad.Press(buttonAPIXStateToVirtualGamepadButton[i]) 129 | } else { 130 | virtualGamepad.Release(buttonAPIXStateToVirtualGamepadButton[i]) 131 | } 132 | 133 | } 134 | 135 | } 136 | 137 | var buttonAPIXStateToVirtualGamepadButton = map[int]gamepad.Button{ 138 | 0: gamepad.ButtonSouth, 139 | 1: gamepad.ButtonEast, 140 | 2: gamepad.ButtonWest, 141 | 3: gamepad.ButtonNorth, 142 | 4: gamepad.ButtonL1, 143 | 5: gamepad.ButtonR1, 144 | 6: gamepad.ButtonL2, 145 | 7: gamepad.ButtonR2, 146 | 8: gamepad.ButtonSelect, 147 | 9: gamepad.ButtonStart, 148 | 10: gamepad.ButtonL3, 149 | 11: gamepad.ButtonR3, 150 | 12: gamepad.ButtonUp, 151 | 13: gamepad.ButtonDown, 152 | 14: gamepad.ButtonLeft, 153 | 15: gamepad.ButtonRight, 154 | } 155 | -------------------------------------------------------------------------------- /src/devices/gamepad/handler_windows.go: -------------------------------------------------------------------------------- 1 | package gamepad 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/pion/webrtc/v3" 7 | "github.com/pquerna/ffjson/ffjson" 8 | ) 9 | 10 | var buttonValueToHexMap = map[int]uint16{ 11 | 0: 0x1000, 12 | 1: 0x2000, 13 | 2: 0x4000, 14 | 3: 0x8000, 15 | 4: 0x0100, 16 | 5: 0x0200, 17 | 8: 0x0020, 18 | 9: 0x0010, 19 | 10: 0x0040, 20 | 11: 0x0080, 21 | 12: 0x0001, 22 | 13: 0x0002, 23 | 14: 0x0004, 24 | 15: 0x0008, 25 | } 26 | 27 | var virtualDevice EmulatedDevice 28 | 29 | func HandleGamepad(gamepadChannel *webrtc.DataChannel) { 30 | 31 | if gamepadChannel.Label() != "controller" { 32 | return 33 | } 34 | 35 | virtualState := new(ViGEmState) 36 | 37 | // Create a virtual device 38 | gamepadChannel.OnOpen(func() { 39 | 40 | var err error = nil 41 | virtualDevice, err = GenerateVirtualDevice() 42 | 43 | if err != nil { 44 | panic(err) 45 | } 46 | 47 | }) 48 | 49 | defer func() { 50 | if err := recover(); err != nil { 51 | FreeTargetAndDisconnect(virtualDevice) 52 | } 53 | }() 54 | 55 | gamepadChannel.OnClose(func() { 56 | 57 | FreeTargetAndDisconnect(virtualDevice) 58 | 59 | }) 60 | 61 | // Update the virtual device 62 | gamepadChannel.OnMessage(func(msg webrtc.DataChannelMessage) { 63 | 64 | if !GamepadEnabled.IsEnabled() { 65 | return 66 | } 67 | 68 | var pad GamepadAPIXState 69 | 70 | ffjson.Unmarshal(msg.Data, &pad) 71 | 72 | go UpdateVirtualDevice(virtualDevice, pad, virtualState) 73 | 74 | }) 75 | 76 | } 77 | 78 | func gamepadAPIXToXInput(gms GamepadAPIXState) XInputState { 79 | 80 | return XInputState{ 81 | ID: ID(gms.Index), 82 | Connected: gms.Connected, 83 | Packet: uint32(time.Now().Nanosecond()), // Different values trigger update 84 | Raw: RawControls{ 85 | Buttons: convertGamepadButtons(gms.Buttons), 86 | LeftTrigger: convertFloatToUint8(gms.Buttons[6].Value), 87 | RightTrigger: convertFloatToUint8(gms.Buttons[7].Value), 88 | ThumbLX: convertFloatToInt16(gms.Axes[0]), 89 | ThumbLY: convertFloatToInt16(fixLYAxis(gms.Axes[1])), 90 | ThumbRX: convertFloatToInt16(gms.Axes[2]), 91 | ThumbRY: convertFloatToInt16(fixRYAxis(gms.Axes[3])), 92 | }, 93 | } 94 | 95 | } 96 | 97 | func convertGamepadButtons(buttons [16]gamepadButton) Button { 98 | var result Button 99 | 100 | for i, button := range buttons { 101 | if button.Pressed { 102 | result += Button(buttonValueToHexMap[i]) 103 | } 104 | } 105 | return result 106 | } 107 | 108 | func convertFloatToUint8(value float64) uint8 { 109 | return uint8(value * 255) 110 | } 111 | 112 | func convertFloatToInt16(value float64) int16 { 113 | return int16(value * 32767) 114 | } 115 | -------------------------------------------------------------------------------- /src/devices/gamepad/vigem_emulate_windows.go: -------------------------------------------------------------------------------- 1 | package gamepad 2 | 3 | import ( 4 | _ "embed" 5 | "fmt" 6 | "unsafe" 7 | ) 8 | 9 | type clientVirtualGamepad uintptr 10 | 11 | type EmulatedDevice struct { 12 | client clientVirtualGamepad 13 | pad uintptr 14 | } 15 | 16 | type ViGEmState struct { 17 | DwPacketNumber DWORD 18 | Gamepad _ViGEm_GAMEPAD 19 | } 20 | 21 | type _ViGEm_GAMEPAD struct { 22 | wButtons WORD 23 | bLeftTrigger BYTE 24 | bRightTrigger BYTE 25 | sThumbLX SHORT 26 | sThumbLY SHORT 27 | sThumbRX SHORT 28 | sThumbRY SHORT 29 | } 30 | 31 | func initializeEmulatedDevice() (clientVirtualGamepad, error) { 32 | 33 | client, _, err := vigem_alloc_proc.Call() 34 | 35 | err = handleVigemError(err) 36 | 37 | if err != nil { 38 | return 0, err 39 | } 40 | 41 | if unsafe.Pointer(&client) == nil { 42 | return 0, fmt.Errorf("not enough memory to do that") 43 | } 44 | 45 | retval, _, err := vigem_connect_proc.Call(client) 46 | 47 | err = handleVigemError(err) 48 | 49 | if err != nil { 50 | return 0, err 51 | } 52 | 53 | if !VIGEM_SUCCESS(retval) { 54 | return 0, fmt.Errorf("vigem bus connection failed with error code: 0x%X", retval) 55 | } 56 | 57 | return clientVirtualGamepad(client), nil 58 | } 59 | 60 | func UpdateVirtualDevice(device EmulatedDevice, rg GamepadAPIXState, virtualState *ViGEmState) { 61 | 62 | // Get Real Input and convert to Virtual 63 | 64 | realState := gamepadAPIXToXInput(rg) 65 | 66 | realState.ToXInput(virtualState) 67 | 68 | // Update the virtual gamepad 69 | vigem_target_x360_update_proc.Call(uintptr(device.client), device.pad, uintptr(unsafe.Pointer(&virtualState.Gamepad))) 70 | 71 | } 72 | 73 | func GenerateVirtualDevice() (EmulatedDevice, error) { 74 | 75 | device := new(EmulatedDevice) 76 | 77 | client, err := initializeEmulatedDevice() 78 | 79 | if err != nil { 80 | return *device, err 81 | } 82 | 83 | pad, _, err := vigem_target_x360_alloc_proc.Call() 84 | 85 | err = handleVigemError(err) 86 | 87 | if err != nil { 88 | return *device, err 89 | } 90 | 91 | device.client = client 92 | device.pad = pad 93 | 94 | pir, _, err := vigem_target_add_proc.Call(uintptr(client), pad) 95 | 96 | err = handleVigemError(err) 97 | 98 | if err != nil { 99 | return *device, err 100 | } 101 | 102 | if !VIGEM_SUCCESS(pir) { 103 | return *device, fmt.Errorf("target plugin failed with error code: 0x%x", pir) 104 | } 105 | 106 | return *device, nil 107 | 108 | } 109 | 110 | func FreeTargetAndDisconnect(device EmulatedDevice) { 111 | 112 | vigem_target_remove_proc.Call(uintptr(device.client), device.pad) 113 | vigem_target_free_proc.Call(device.pad) 114 | 115 | vigem_disconnect_proc.Call(uintptr(device.client)) 116 | vigem_free_proc.Call(uintptr(device.client)) 117 | 118 | } 119 | 120 | func (gamepad *_ViGEm_GAMEPAD) UpdateFromRawState(state RawControls) { 121 | 122 | gamepad.wButtons = WORD(state.Buttons) 123 | gamepad.bLeftTrigger = BYTE(state.LeftTrigger) 124 | gamepad.bRightTrigger = BYTE(state.RightTrigger) 125 | gamepad.sThumbLX = SHORT(state.ThumbLX) 126 | gamepad.sThumbLY = SHORT(state.ThumbLY) 127 | gamepad.sThumbRX = SHORT(state.ThumbRX) 128 | gamepad.sThumbRY = SHORT(state.ThumbRY) 129 | 130 | } 131 | -------------------------------------------------------------------------------- /src/devices/gamepad/vigemwraper_windows.go: -------------------------------------------------------------------------------- 1 | package gamepad 2 | 3 | import ( 4 | "os" 5 | "os/exec" 6 | "strconv" 7 | "syscall" 8 | 9 | "github.com/PiterWeb/RemoteController/src/bin" 10 | ) 11 | 12 | type ( 13 | BOOL = uint32 14 | BOOLEAN = byte 15 | BYTE = byte 16 | DWORD = uint32 17 | DWORD64 = uint64 18 | HANDLE = uintptr 19 | HLOCAL = uintptr 20 | LARGE_INTEGER = int64 21 | LONG = int32 22 | LPVOID = uintptr 23 | SIZE_T = uintptr 24 | UINT = uint32 25 | ULONG_PTR = uintptr 26 | ULONGLONG = uint64 27 | WORD = uint16 28 | SHORT = int16 29 | ) 30 | 31 | type VIGEM_ERROR uintptr 32 | 33 | const ( 34 | VIGEM_ERROR_NONE VIGEM_ERROR = 0x20000000 35 | ViGEm_DLL_FILE_NAME string = "ViGEmClient.dll" 36 | ViGEm_EXE_FILE_NAME string = "ViGEmBus.exe" 37 | ViGEm_INSTALATION_SUCESS_FILE_NAME string = ".vigemsetup" 38 | ) 39 | 40 | var ( 41 | vigemDLL *syscall.LazyDLL 42 | vigem_free_proc *syscall.LazyProc 43 | vigem_disconnect_proc *syscall.LazyProc 44 | vigem_alloc_proc *syscall.LazyProc 45 | vigem_connect_proc *syscall.LazyProc 46 | vigem_target_x360_alloc_proc *syscall.LazyProc 47 | vigem_target_add_proc *syscall.LazyProc 48 | vigem_target_x360_update_proc *syscall.LazyProc 49 | vigem_target_remove_proc *syscall.LazyProc 50 | vigem_target_free_proc *syscall.LazyProc 51 | ) 52 | 53 | func InitViGEm() error { 54 | 55 | path, err := os.Getwd() 56 | 57 | if err != nil { 58 | return err 59 | } 60 | 61 | if _, err := os.ReadFile("./" + ViGEm_INSTALATION_SUCESS_FILE_NAME); err != nil { 62 | OpenViGEmWizard() 63 | } 64 | 65 | dllFile, err := os.Create("./" + ViGEm_DLL_FILE_NAME) 66 | 67 | if err != nil { 68 | return err 69 | } 70 | 71 | defer dllFile.Close() 72 | 73 | if strconv.IntSize == 32 { 74 | // x86 Architecture 75 | dllFile.Write(bin.ViGEmClient_x86) 76 | } else if strconv.IntSize == 64 { 77 | // x64 Architecture 78 | dllFile.Write(bin.ViGEmClient_x64) 79 | } 80 | 81 | vigemDLL = syscall.NewLazyDLL(path + "/" + ViGEm_DLL_FILE_NAME) 82 | vigem_disconnect_proc = vigemDLL.NewProc("vigem_disconnect") 83 | vigem_free_proc = vigemDLL.NewProc("vigem_free") 84 | vigem_alloc_proc = vigemDLL.NewProc("vigem_alloc") 85 | vigem_connect_proc = vigemDLL.NewProc("vigem_connect") 86 | vigem_target_x360_alloc_proc = vigemDLL.NewProc("vigem_target_x360_alloc") 87 | vigem_target_add_proc = vigemDLL.NewProc("vigem_target_add") 88 | vigem_target_remove_proc = vigemDLL.NewProc("vigem_target_remove") 89 | vigem_target_free_proc = vigemDLL.NewProc("vigem_target_free") 90 | vigem_target_x360_update_proc = vigemDLL.NewProc("vigem_target_x360_update") 91 | 92 | if _, err = os.Create("./" + ViGEm_INSTALATION_SUCESS_FILE_NAME); err != nil { 93 | return err 94 | } 95 | 96 | return nil 97 | 98 | } 99 | 100 | func OpenViGEmWizard() error { 101 | 102 | exeFile, err := os.Create("./" + ViGEm_EXE_FILE_NAME) 103 | 104 | if err != nil { 105 | return err 106 | } 107 | 108 | defer os.Remove("./" + ViGEm_EXE_FILE_NAME) 109 | 110 | if _, err = exeFile.Write(bin.ViGEmBus_exe); err != nil { 111 | return err 112 | } 113 | 114 | if err = exeFile.Close(); err != nil { 115 | return err 116 | } 117 | 118 | exeCmd := exec.Command("./" + ViGEm_EXE_FILE_NAME) 119 | 120 | if err = exeCmd.Start(); err != nil { 121 | return err 122 | } 123 | 124 | return exeCmd.Wait() 125 | } 126 | 127 | func VIGEM_SUCCESS(val uintptr) bool { 128 | return val == uintptr(VIGEM_ERROR_NONE) 129 | } 130 | 131 | func CloseViGEmDLL() { 132 | 133 | syscall.FreeLibrary(syscall.Handle(vigemDLL.Handle())) 134 | 135 | } 136 | 137 | func handleVigemError(err error) error { 138 | 139 | if err != syscall.Errno(0) { 140 | return err 141 | } 142 | 143 | return nil 144 | 145 | } 146 | -------------------------------------------------------------------------------- /src/devices/gamepad/xinput_windows.go: -------------------------------------------------------------------------------- 1 | package gamepad 2 | 3 | import ( 4 | "math" 5 | ) 6 | 7 | type ID byte 8 | 9 | type All [ControllerCount]XInputState 10 | 11 | type XInputState struct { 12 | ID ID 13 | Connected bool 14 | Packet uint32 15 | Raw RawControls 16 | } 17 | 18 | type RawControls struct { 19 | Buttons Button 20 | LeftTrigger uint8 21 | RightTrigger uint8 22 | ThumbLX int16 23 | ThumbLY int16 24 | ThumbRX int16 25 | ThumbRY int16 26 | } 27 | 28 | func (state XInputState) ToXInput(virtualState *ViGEmState) { 29 | 30 | virtualState.DwPacketNumber = uint32(state.Packet) 31 | virtualState.Gamepad.UpdateFromRawState(state.Raw) 32 | } 33 | 34 | func (state *XInputState) Pressed(button Button) bool { return state.Raw.Buttons&button != 0 } 35 | 36 | type Thumb struct{ X, Y, Magnitude float32 } 37 | 38 | func (state *XInputState) RectDPad() (thumb Thumb) { 39 | if state.Pressed(DPadUp) { 40 | thumb.Y += 1 41 | } 42 | if state.Pressed(DPadDown) { 43 | thumb.Y -= 1 44 | } 45 | if state.Pressed(DPadLeft) { 46 | thumb.X -= 1 47 | } 48 | if state.Pressed(DPadRight) { 49 | thumb.X += 1 50 | } 51 | if thumb.X != 0 || thumb.Y != 0 { 52 | thumb.Magnitude = 1 53 | } 54 | return 55 | } 56 | 57 | func (state *XInputState) RoundDPad() (thumb Thumb) { 58 | thumb = state.RectDPad() 59 | if thumb.X != 0 && thumb.Y != 0 { 60 | thumb.X *= isqrt2 61 | thumb.Y *= isqrt2 62 | } 63 | return 64 | } 65 | 66 | func round16(rx, ry, deadzone int16) (thumb Thumb) { 67 | //TODO: use sqrt32 68 | fx, fy := float64(rx), float64(ry) 69 | thumb.Magnitude = float32(math.Sqrt(fx*fx + fy*fy)) 70 | 71 | thumb.X = float32(rx) / thumb.Magnitude 72 | thumb.Y = float32(ry) / thumb.Magnitude 73 | 74 | if thumb.Magnitude > float32(deadzone) { 75 | if thumb.Magnitude > 32767 { 76 | thumb.Magnitude = 32767 77 | } 78 | thumb.Magnitude = (thumb.Magnitude - float32(deadzone)) / float32(32767-deadzone) 79 | } else { 80 | thumb.Magnitude = 0 81 | } 82 | 83 | thumb.X *= thumb.Magnitude 84 | thumb.Y *= thumb.Magnitude 85 | 86 | return 87 | } 88 | 89 | func (state *XInputState) RoundLeft() Thumb { 90 | return round16(state.Raw.ThumbLX, state.Raw.ThumbLY, LeftThumbDeadZone) 91 | } 92 | 93 | func (state *XInputState) RoundRight() Thumb { 94 | return round16(state.Raw.ThumbRX, state.Raw.ThumbRY, RightThumbDeadZone) 95 | } 96 | 97 | func linear16(v, deadzone int16) float32 { 98 | if v < -deadzone { 99 | return float32(v+deadzone) / float32(32767-deadzone) 100 | } 101 | if v > deadzone { 102 | return float32(v-deadzone) / float32(32767-deadzone) 103 | } 104 | return 0 105 | } 106 | 107 | func rect16(rx, ry, deadzone int16) (thumb Thumb) { 108 | thumb.X = linear16(rx, deadzone) 109 | thumb.Y = linear16(ry, deadzone) 110 | if thumb.X != 0 && thumb.Y != 0 { 111 | thumb.Magnitude = 1 112 | } 113 | return 114 | } 115 | 116 | func (state *XInputState) RectLeft() Thumb { 117 | return rect16(state.Raw.ThumbLX, state.Raw.ThumbLY, LeftThumbDeadZone) 118 | } 119 | 120 | func (state *XInputState) RectRight() Thumb { 121 | return rect16(state.Raw.ThumbRX, state.Raw.ThumbRY, RightThumbDeadZone) 122 | } 123 | 124 | // func (state *XInputState) Vibrate(left, right uint16) { 125 | // if !state.Connected { 126 | // return 127 | // } 128 | // Vibrate(state.ID, &Vibration{left, right}) 129 | // } 130 | 131 | type Vibration struct { 132 | LeftMotor uint16 133 | RightMotor uint16 134 | } 135 | 136 | const ( 137 | ControllerCount = ID(4) 138 | TriggerThreshold = 30 139 | LeftThumbDeadZone = 7849 140 | RightThumbDeadZone = 8689 141 | 142 | sqrt2 = 1.4142135623730950488 143 | isqrt2 = 1 / sqrt2 144 | ) 145 | 146 | type Button uint16 147 | 148 | const ( 149 | DPadUp Button = 0x0001 150 | DPadDown Button = 0x0002 151 | DPadLeft Button = 0x0004 152 | DPadRight Button = 0x0008 153 | 154 | Start Button = 0x0010 155 | Back Button = 0x0020 156 | 157 | LeftThumb Button = 0x0040 158 | RightThumb Button = 0x0080 159 | 160 | LeftShoulder Button = 0x0100 161 | RightShoulder Button = 0x0200 162 | 163 | ButtonA Button = 0x1000 164 | ButtonB Button = 0x2000 165 | ButtonX Button = 0x4000 166 | ButtonY Button = 0x8000 167 | ) 168 | -------------------------------------------------------------------------------- /src/devices/keyboard/handler.go: -------------------------------------------------------------------------------- 1 | package keyboard 2 | 3 | import ( 4 | "log" 5 | "strings" 6 | 7 | "github.com/PiterWeb/RemoteController/src/devices" 8 | "github.com/go-vgo/robotgo" 9 | "github.com/pion/webrtc/v3" 10 | ) 11 | 12 | var KeyboardEnabled = new(devices.DeviceEnabled).Disable() 13 | 14 | func HandleKeyboard(d *webrtc.DataChannel) error { 15 | 16 | if d.Label() != "keyboard" { 17 | return nil 18 | } 19 | 20 | d.OnOpen(func() { 21 | log.Println("keyboard data channel is open") 22 | }) 23 | 24 | keyState := make(map[string]bool) 25 | 26 | d.OnMessage(func(msg webrtc.DataChannelMessage) { 27 | 28 | if !KeyboardEnabled.IsEnabled() { 29 | return 30 | } 31 | 32 | if !msg.IsString || msg.Data == nil { 33 | return 34 | } 35 | 36 | keyParts := strings.Split(string(msg.Data), "_") 37 | 38 | if len(keyParts) < 2 { 39 | return 40 | } 41 | 42 | key := mapJSKeyToRobotGo(keyParts[0]) 43 | 44 | if key == "" { 45 | log.Println("keyboard key not found: ", keyParts[0]) 46 | return 47 | } 48 | 49 | if keyParts[1] == "1" { 50 | if keyState[key] { 51 | return 52 | } 53 | keyState[key] = true 54 | _ = robotgo.KeyDown(key) 55 | return 56 | } else { 57 | if !keyState[key] { 58 | return 59 | } 60 | keyState[key] = false 61 | _ = robotgo.KeyUp(key) 62 | return 63 | } 64 | 65 | }) 66 | 67 | return nil 68 | 69 | } 70 | -------------------------------------------------------------------------------- /src/devices/keyboard/map_js_keys_go.go: -------------------------------------------------------------------------------- 1 | package keyboard 2 | 3 | import "strings" 4 | 5 | func mapJSKeyToRobotGo(jsKey string) string { 6 | keyMap := map[string]string{ 7 | "Enter": "enter", 8 | "Escape": "esc", 9 | "Backspace": "backspace", 10 | "Tab": "tab", 11 | " ": "space", 12 | "ArrowUp": "up", 13 | "ArrowDown": "down", 14 | "ArrowLeft": "left", 15 | "ArrowRight": "right", 16 | "Shift": "shift", 17 | "Control": "ctrl", 18 | "Alt": "alt", 19 | "CapsLock": "capslock", 20 | } 21 | 22 | // Map (F1 - F12) keys 23 | if strings.HasPrefix(jsKey, "F") && len(jsKey) > 1 { 24 | return strings.ToLower(jsKey) // "F1" → "f1" 25 | } 26 | 27 | if val, exists := keyMap[jsKey]; exists { 28 | return val 29 | } 30 | 31 | if len(jsKey) == 1 { 32 | return jsKey 33 | } 34 | 35 | return "" 36 | } 37 | -------------------------------------------------------------------------------- /src/logger/logger.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "log" 5 | "os" 6 | ) 7 | 8 | func InitLogger() *os.File { 9 | 10 | LOG_FILE := "./LibreRemotePlay.log" 11 | 12 | logFile, err := os.OpenFile(LOG_FILE, os.O_APPEND|os.O_RDWR|os.O_CREATE, 0644) 13 | 14 | if err != nil { 15 | log.Fatal("logger file can not be opened") 16 | } 17 | 18 | log.SetOutput(logFile) 19 | 20 | log.SetFlags(log.Lshortfile | log.LstdFlags) 21 | 22 | return logFile 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/net/http/http_linux.go: -------------------------------------------------------------------------------- 1 | // HTTP server 2 | package http 3 | 4 | import ( 5 | "embed" 6 | "fmt" 7 | "io/fs" 8 | "net/http" 9 | "strings" 10 | ) 11 | 12 | func InitHTTPAssets(clientPort int, assets embed.FS) error { 13 | 14 | staticFS, err := fs.Sub(assets, "frontend/build") 15 | 16 | if err != nil { 17 | return err 18 | } 19 | 20 | http.Handle("GET /", FileMiddleware(staticFS, http.FileServer(http.FS(staticFS)))) 21 | 22 | err = http.ListenAndServe(fmt.Sprintf(":%d", clientPort), nil) 23 | 24 | if err != nil { 25 | return err 26 | } 27 | 28 | return nil 29 | 30 | } 31 | 32 | // If .html of the route is available it loads the .html otherwise try to load the given path 33 | func FileMiddleware(staticFS fs.FS, next http.Handler) http.Handler { 34 | 35 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 36 | 37 | pathSplited := strings.SplitN(r.URL.Path, "/", 2) 38 | 39 | if len(pathSplited) != 2 { 40 | next.ServeHTTP(w, r) 41 | return 42 | } 43 | 44 | data, err := fs.ReadFile(staticFS, pathSplited[1]+".html") 45 | 46 | if err != nil { 47 | next.ServeHTTP(w, r) 48 | return 49 | } 50 | 51 | w.WriteHeader(200) 52 | _, err = w.Write(data) 53 | 54 | if err != nil { 55 | http.Error(w, err.Error(), http.StatusInternalServerError) 56 | } 57 | }) 58 | } 59 | -------------------------------------------------------------------------------- /src/net/webrtc/host.go: -------------------------------------------------------------------------------- 1 | // Package webrtc provides all the related functions for WebRTC communication 2 | package webrtc 3 | 4 | import ( 5 | "context" 6 | "log" 7 | "strings" 8 | 9 | // "github.com/PiterWeb/RemoteController/src/plugins" 10 | "github.com/PiterWeb/RemoteController/src/devices/gamepad" 11 | "github.com/PiterWeb/RemoteController/src/devices/keyboard" 12 | "github.com/PiterWeb/RemoteController/src/net/webrtc/streaming_signal" 13 | "github.com/pion/webrtc/v3" 14 | "github.com/wailsapp/wails/v2/pkg/runtime" 15 | ) 16 | 17 | var defaultSTUNServers = []string{"stun:stun.l.google.com:19305", "stun:stun.l.google.com:19302", "stun:stun.ipfire.org:3478"} 18 | 19 | func InitHost(ctx context.Context, ICEServers []webrtc.ICEServer, offerEncodedWithCandidates string, answerResponse chan<- string, triggerEnd <-chan struct{}) { 20 | 21 | candidates := []webrtc.ICECandidateInit{} 22 | 23 | if len(ICEServers) == 0 { 24 | ICEServers = []webrtc.ICEServer{ 25 | { 26 | URLs: defaultSTUNServers, 27 | }, 28 | } 29 | } 30 | 31 | // Prepare the configuration 32 | config := webrtc.Configuration{ 33 | ICEServers: ICEServers, 34 | } 35 | 36 | defer func() { 37 | if err := recover(); err != nil { 38 | answerResponse <- "Error" 39 | } 40 | }() 41 | 42 | peerConnection, err := webrtc.NewAPI().NewPeerConnection(config) 43 | if err != nil { 44 | return 45 | } 46 | 47 | defer func() { 48 | if err := peerConnection.Close(); err != nil { 49 | log.Printf("cannot close peerConnection: %v\n", err) 50 | } 51 | }() 52 | 53 | // Reload plugins in case a new plugin was added or configuration changed 54 | // plugins.ReloadPlugins() 55 | 56 | // Register data channel creation handling 57 | peerConnection.OnDataChannel(func(d *webrtc.DataChannel) { 58 | 59 | gamepad.HandleGamepad(d) 60 | streaming_signal.HandleStreamingSignal(ctx, d) 61 | keyboard.HandleKeyboard(d) 62 | // plugins.HandleServerPlugins(d) 63 | 64 | }) 65 | 66 | peerConnection.OnICECandidate(func(c *webrtc.ICECandidate) { 67 | 68 | if c == nil { 69 | answerResponse <- signalEncode(*peerConnection.LocalDescription()) + ";" + signalEncode(candidates) 70 | return 71 | } 72 | 73 | candidates = append(candidates, (*c).ToJSON()) 74 | 75 | }) 76 | 77 | // Set the handler for Peer connection state 78 | // This will notify you when the peer has connected/disconnected 79 | peerConnection.OnConnectionStateChange(func(s webrtc.PeerConnectionState) { 80 | log.Printf("Peer Connection State has changed: %s\n", s.String()) 81 | 82 | runtime.EventsEmit(ctx, "connection_state", s.String()) 83 | 84 | if s == webrtc.PeerConnectionStateFailed { 85 | 86 | peerConnection.Close() 87 | 88 | } 89 | }) 90 | 91 | offerEncodedWithCandidatesSplited := strings.Split(offerEncodedWithCandidates, ";") 92 | 93 | offer := webrtc.SessionDescription{} 94 | signalDecode(offerEncodedWithCandidatesSplited[0], &offer) 95 | 96 | var receivedCandidates []webrtc.ICECandidateInit 97 | 98 | signalDecode(offerEncodedWithCandidatesSplited[1], &receivedCandidates) 99 | 100 | if err := peerConnection.SetRemoteDescription(offer); err != nil { 101 | panic(err) 102 | } 103 | 104 | for _, candidate := range receivedCandidates { 105 | if err := peerConnection.AddICECandidate(candidate); err != nil { 106 | log.Println(err) 107 | continue 108 | } 109 | } 110 | 111 | // Create an answer to send to the other process 112 | answer, err := peerConnection.CreateAnswer(nil) 113 | if err != nil { 114 | panic(err) 115 | } 116 | 117 | // Sets the LocalDescription, and starts our UDP listeners 118 | err = peerConnection.SetLocalDescription(answer) 119 | if err != nil { 120 | panic(err) 121 | } 122 | 123 | // Block until cancel by user 124 | <-triggerEnd 125 | 126 | // Close the signaling channel 127 | runtime.EventsOff(ctx, "streaming-signal-server") 128 | 129 | } 130 | -------------------------------------------------------------------------------- /src/net/webrtc/signal.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package webrtc 5 | 6 | import ( 7 | "bytes" 8 | "compress/gzip" 9 | "encoding/base64" 10 | "encoding/json" 11 | "io" 12 | ) 13 | 14 | // Allows compressing offer/answer to bypass terminal input limits. 15 | const compress = true 16 | 17 | // signalEncode encodes the input in base64 18 | // It can optionally zip the input before encoding 19 | func signalEncode(obj interface{}) string { 20 | b, err := json.Marshal(obj) 21 | if err != nil { 22 | panic(err) 23 | } 24 | 25 | if compress { 26 | b = signalZip(b) 27 | } 28 | 29 | return base64.StdEncoding.EncodeToString(b) 30 | } 31 | 32 | // signalDecode decodes the input from base64 33 | // It can optionally unzip the input after decoding 34 | func signalDecode(in string, obj interface{}) { 35 | b, err := base64.StdEncoding.DecodeString(in) 36 | if err != nil { 37 | panic(err) 38 | } 39 | 40 | if compress { 41 | b = signalUnzip(b) 42 | } 43 | 44 | err = json.Unmarshal(b, obj) 45 | if err != nil { 46 | panic(err) 47 | } 48 | } 49 | 50 | func signalZip(in []byte) []byte { 51 | var b bytes.Buffer 52 | gz := gzip.NewWriter(&b) 53 | _, err := gz.Write(in) 54 | if err != nil { 55 | panic(err) 56 | } 57 | err = gz.Flush() 58 | if err != nil { 59 | panic(err) 60 | } 61 | err = gz.Close() 62 | if err != nil { 63 | panic(err) 64 | } 65 | return b.Bytes() 66 | } 67 | 68 | func signalUnzip(in []byte) []byte { 69 | var b bytes.Buffer 70 | _, err := b.Write(in) 71 | if err != nil { 72 | panic(err) 73 | } 74 | r, err := gzip.NewReader(&b) 75 | if err != nil { 76 | panic(err) 77 | } 78 | res, err := io.ReadAll(r) 79 | if err != nil { 80 | panic(err) 81 | } 82 | return res 83 | } 84 | -------------------------------------------------------------------------------- /src/net/webrtc/streaming_signal/handler_darwin.go: -------------------------------------------------------------------------------- 1 | package streaming_signal 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/pion/webrtc/v3" 7 | ) 8 | 9 | func HandleStreamingSignal(ctx context.Context, streamingSignalChannel *webrtc.DataChannel) { 10 | 11 | if streamingSignalChannel.Label() != "streaming-signal" { 12 | return 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/net/webrtc/streaming_signal/handler_linux.go: -------------------------------------------------------------------------------- 1 | package streaming_signal 2 | 3 | import ( 4 | "context" 5 | "log" 6 | 7 | "github.com/coder/websocket" 8 | "github.com/pion/webrtc/v3" 9 | ) 10 | 11 | func HandleStreamingSignal(ctx context.Context, streamingSignalChannel *webrtc.DataChannel) { 12 | 13 | if streamingSignalChannel.Label() != "streaming-signal" { 14 | return 15 | } 16 | 17 | wsClient, _, err := websocket.Dial(context.Background(), "ws://localhost:8080/ws", nil) 18 | 19 | if err != nil { 20 | log.Println(err) 21 | return 22 | } 23 | 24 | defer func() { 25 | if err := recover(); err != nil { 26 | wsClient.Close(websocket.StatusInternalError, "Fatal error on client") 27 | } 28 | }() 29 | 30 | go func() { 31 | 32 | defer wsClient.Close(websocket.StatusInternalError, "Client terminated") 33 | 34 | for { 35 | t, data, err := wsClient.Read(context.Background()) 36 | 37 | if err != nil { 38 | log.Println(err) 39 | continue 40 | } 41 | 42 | if t != websocket.MessageText { 43 | continue 44 | } 45 | 46 | streamingSignalChannel.SendText(string(data)) 47 | 48 | } 49 | }() 50 | 51 | streamingSignalChannel.OnMessage(func(msg webrtc.DataChannelMessage) { 52 | 53 | wsClient.Write(context.Background(), websocket.MessageText, msg.Data) 54 | 55 | }) 56 | 57 | } 58 | -------------------------------------------------------------------------------- /src/net/webrtc/streaming_signal/handler_windows.go: -------------------------------------------------------------------------------- 1 | package streaming_signal 2 | 3 | import ( 4 | "context" 5 | "log" 6 | 7 | "github.com/pion/webrtc/v3" 8 | "github.com/wailsapp/wails/v2/pkg/runtime" 9 | ) 10 | 11 | func HandleStreamingSignal(ctx context.Context, streamingSignalChannel *webrtc.DataChannel) { 12 | 13 | if streamingSignalChannel.Label() != "streaming-signal" { 14 | return 15 | } 16 | 17 | streamingSignalChannel.OnMessage(func(msg webrtc.DataChannelMessage) { 18 | 19 | runtime.EventsEmit(ctx, "streaming-signal-client", string(msg.Data)) 20 | 21 | }) 22 | 23 | runtime.EventsOn(ctx, "streaming-signal-server", func(data ...interface{}) { 24 | 25 | if len(data) == 0 { 26 | return 27 | } 28 | 29 | signalingData, ok := data[0].(string) 30 | 31 | if !ok { 32 | log.Println(data[0], ok) 33 | return 34 | } 35 | 36 | _ = streamingSignalChannel.SendText(signalingData) 37 | 38 | }) 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/net/websocket/websocket_linux.go: -------------------------------------------------------------------------------- 1 | // Websocket server 2 | package websocket 3 | 4 | import ( 5 | "context" 6 | "io" 7 | "log" 8 | "net/http" 9 | 10 | "github.com/coder/websocket" 11 | ) 12 | 13 | var conns = map[string]*websocket.Conn{} 14 | 15 | func SetupWebsocketHandler() { 16 | 17 | http.HandleFunc("GET /ws", func(w http.ResponseWriter, r *http.Request) { 18 | 19 | c, err := websocket.Accept(w, r, nil) 20 | if err != nil { 21 | log.Println(err) 22 | return 23 | } 24 | 25 | defer func() { 26 | err := c.CloseNow() 27 | 28 | if err != nil { 29 | log.Println(err) 30 | } 31 | }() 32 | 33 | // Set the context as needed. Use of r.Context() is not recommended 34 | // to avoid surprising behavior (see http.Hijacker). 35 | ctx, cancel := context.WithCancel(context.Background()) 36 | defer cancel() 37 | 38 | wsBroadcast(ctx, r, c) 39 | 40 | c.Close(websocket.StatusNormalClosure, "Client connection closed") 41 | 42 | }) 43 | 44 | } 45 | 46 | func wsBroadcast(ctx context.Context, r *http.Request, ws *websocket.Conn) { 47 | conns[r.RemoteAddr] = ws 48 | 49 | defer func() { 50 | 51 | // If panic it will recover and liberate resources 52 | _ = recover() 53 | 54 | for addr := range conns { 55 | if r.RemoteAddr == addr { 56 | delete(conns, addr) 57 | break 58 | } 59 | } 60 | 61 | }() 62 | 63 | for { 64 | typ, reader, err := ws.Reader(ctx) 65 | if err != nil { 66 | log.Println(err) 67 | break 68 | } 69 | 70 | for addr, con := range conns { 71 | 72 | if r.RemoteAddr == addr { 73 | continue 74 | } 75 | 76 | writer, err := con.Writer(ctx, typ) 77 | 78 | if err != nil { 79 | log.Println(err) 80 | continue 81 | } 82 | 83 | _, err = io.Copy(writer, reader) 84 | 85 | if err != nil { 86 | log.Println(err) 87 | continue 88 | } 89 | 90 | // log.Println("Message sended to ", addr) 91 | 92 | err = writer.Close() 93 | 94 | if err != nil { 95 | log.Println(err) 96 | continue 97 | } 98 | 99 | } 100 | 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/oninit/oninit_darwin.go: -------------------------------------------------------------------------------- 1 | package oninit 2 | 3 | import ( 4 | "embed" 5 | 6 | LRPSignals "github.com/PiterWeb/LibreRemotePlaySignals/v1" 7 | ) 8 | 9 | func Execute(assets embed.FS) error { 10 | 11 | easyConnectPort := uint16(8081) 12 | ips_channel := make(chan []string) 13 | 14 | err := LRPSignals.InitServer(easyConnectPort, ips_channel) 15 | 16 | defer close(ips_channel) 17 | 18 | return err 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/oninit/oninit_linux.go: -------------------------------------------------------------------------------- 1 | package oninit 2 | 3 | import ( 4 | "embed" 5 | 6 | LRPSignals "github.com/PiterWeb/LibreRemotePlaySignals/v1" 7 | "github.com/PiterWeb/RemoteController/src/net/http" 8 | "github.com/PiterWeb/RemoteController/src/net/websocket" 9 | ) 10 | 11 | func Execute(assets embed.FS) error { 12 | 13 | serverPort := 8080 14 | 15 | websocket.SetupWebsocketHandler() 16 | 17 | err := http.InitHTTPAssets(serverPort, assets) 18 | 19 | if err != nil { 20 | return err 21 | } 22 | 23 | easyConnectPort := uint16(8081) 24 | ips_channel := make(chan []string) 25 | 26 | err = LRPSignals.InitServer(easyConnectPort, ips_channel) 27 | 28 | defer close(ips_channel) 29 | 30 | return err 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/oninit/oninit_windows.go: -------------------------------------------------------------------------------- 1 | package oninit 2 | 3 | import ( 4 | "embed" 5 | 6 | LRPSignals "github.com/PiterWeb/LibreRemotePlaySignals/v1" 7 | "github.com/PiterWeb/RemoteController/src/devices/gamepad" 8 | ) 9 | 10 | func Execute(assets embed.FS) error { 11 | err := gamepad.InitViGEm() 12 | 13 | if err != nil { 14 | return err 15 | } 16 | 17 | easyConnectPort := uint16(8081) 18 | ips_channel := make(chan []string) 19 | 20 | err = LRPSignals.InitServer(easyConnectPort, ips_channel) 21 | 22 | defer close(ips_channel) 23 | 24 | return err 25 | 26 | } 27 | -------------------------------------------------------------------------------- /wails.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "RemoteController", 3 | "outputfilename": "LibreRemotePlay", 4 | "frontend:install": "pnpm install", 5 | "frontend:build": "pnpm run build", 6 | "frontend:dev:watcher": "pnpm run dev", 7 | "frontend:dev:serverUrl": "auto", 8 | "wailsjsdir": "./frontend/src/lib", 9 | "author": { 10 | "name": "PiterDev", 11 | "email": "piterzdev@gmail.com" 12 | } 13 | } 14 | --------------------------------------------------------------------------------