mirror of https://github.com/jetkvm/kvm.git
Compare commits
22 Commits
07aaca36b2
...
d4bc6b613a
| Author | SHA1 | Date |
|---|---|---|
|
|
d4bc6b613a | |
|
|
5f3dd89d55 | |
|
|
1dda6184da | |
|
|
825d0311d6 | |
|
|
f3fe78af5d | |
|
|
867ed88c6e | |
|
|
2e1b6f199c | |
|
|
94a388336e | |
|
|
d0b3781aaa | |
|
|
a4f0c0d298 | |
|
|
7feb92c9c7 | |
|
|
4592269dd1 | |
|
|
e1815237eb | |
|
|
af8fff7cee | |
|
|
7389467c2f | |
|
|
d61ea2195b | |
|
|
c459929a91 | |
|
|
3dd8645295 | |
|
|
fefbc7611f | |
|
|
f7beae3b9b | |
|
|
eacc2a6621 | |
|
|
58b72add90 |
|
|
@ -15,7 +15,7 @@ jobs:
|
||||||
if: github.event_name != 'pull_request_review' || github.event.review.state == 'approved'
|
if: github.event_name != 'pull_request_review' || github.event.review.state == 'approved'
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
- name: Set up Node.js
|
- name: Set up Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
uses: actions/checkout@ff7abcd0c3c05ccf6adc123a8cd1fd4fb30fb493 # v4.2.2
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@fa96338abe5531f6e34c5cc0bbe28c1a533d5505 # v4.2.1
|
uses: actions/setup-go@fa96338abe5531f6e34c5cc0bbe28c1a533d5505 # v4.2.1
|
||||||
with:
|
with:
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
- name: Set up Node.js
|
- name: Set up Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
|
|
|
||||||
39
go.mod
39
go.mod
|
|
@ -6,32 +6,31 @@ require (
|
||||||
github.com/Masterminds/semver/v3 v3.4.0
|
github.com/Masterminds/semver/v3 v3.4.0
|
||||||
github.com/beevik/ntp v1.4.3
|
github.com/beevik/ntp v1.4.3
|
||||||
github.com/coder/websocket v1.8.13
|
github.com/coder/websocket v1.8.13
|
||||||
github.com/coreos/go-oidc/v3 v3.14.1
|
github.com/coreos/go-oidc/v3 v3.15.0
|
||||||
github.com/creack/pty v1.1.24
|
github.com/creack/pty v1.1.24
|
||||||
github.com/fsnotify/fsnotify v1.9.0
|
github.com/fsnotify/fsnotify v1.9.0
|
||||||
github.com/gin-contrib/logger v1.2.6
|
github.com/gin-contrib/logger v1.2.6
|
||||||
github.com/gin-gonic/gin v1.10.1
|
github.com/gin-gonic/gin v1.10.1
|
||||||
github.com/go-co-op/gocron/v2 v2.16.3
|
github.com/go-co-op/gocron/v2 v2.16.5
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/guregu/null/v6 v6.0.0
|
github.com/guregu/null/v6 v6.0.0
|
||||||
github.com/gwatts/rootcerts v0.0.0-20250601184604-370a9a75f341
|
github.com/gwatts/rootcerts v0.0.0-20250901182336-dc5ae18bd79f
|
||||||
github.com/hanwen/go-fuse/v2 v2.8.0
|
|
||||||
github.com/pion/logging v0.2.4
|
github.com/pion/logging v0.2.4
|
||||||
github.com/pion/mdns/v2 v2.0.7
|
github.com/pion/mdns/v2 v2.0.7
|
||||||
github.com/pion/webrtc/v4 v4.1.3
|
github.com/pion/webrtc/v4 v4.1.4
|
||||||
github.com/pojntfx/go-nbd v0.3.2
|
github.com/pojntfx/go-nbd v0.3.2
|
||||||
github.com/prometheus/client_golang v1.22.0
|
github.com/prometheus/client_golang v1.23.0
|
||||||
github.com/prometheus/common v0.65.0
|
github.com/prometheus/common v0.66.0
|
||||||
github.com/prometheus/procfs v0.16.1
|
github.com/prometheus/procfs v0.17.0
|
||||||
github.com/psanford/httpreadat v0.1.0
|
github.com/psanford/httpreadat v0.1.0
|
||||||
github.com/rs/zerolog v1.34.0
|
github.com/rs/zerolog v1.34.0
|
||||||
github.com/sourcegraph/tf-dag v0.2.2-0.20250131204052-3e8ff1477b4f
|
github.com/sourcegraph/tf-dag v0.2.2-0.20250131204052-3e8ff1477b4f
|
||||||
github.com/stretchr/testify v1.10.0
|
github.com/stretchr/testify v1.11.1
|
||||||
github.com/vishvananda/netlink v1.3.1
|
github.com/vishvananda/netlink v1.3.1
|
||||||
go.bug.st/serial v1.6.4
|
go.bug.st/serial v1.6.4
|
||||||
golang.org/x/crypto v0.40.0
|
golang.org/x/crypto v0.41.0
|
||||||
golang.org/x/net v0.41.0
|
golang.org/x/net v0.43.0
|
||||||
golang.org/x/sys v0.34.0
|
golang.org/x/sys v0.35.0
|
||||||
)
|
)
|
||||||
|
|
||||||
replace github.com/pojntfx/go-nbd v0.3.2 => github.com/chemhack/go-nbd v0.0.0-20241006125820-59e45f5b1e7b
|
replace github.com/pojntfx/go-nbd v0.3.2 => github.com/chemhack/go-nbd v0.0.0-20241006125820-59e45f5b1e7b
|
||||||
|
|
@ -51,6 +50,7 @@ require (
|
||||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
github.com/go-playground/validator/v10 v10.26.0 // indirect
|
github.com/go-playground/validator/v10 v10.26.0 // indirect
|
||||||
github.com/goccy/go-json v0.10.5 // indirect
|
github.com/goccy/go-json v0.10.5 // indirect
|
||||||
|
github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc // indirect
|
||||||
github.com/jonboulle/clockwork v0.5.0 // indirect
|
github.com/jonboulle/clockwork v0.5.0 // indirect
|
||||||
github.com/json-iterator/go v1.1.12 // indirect
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
|
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
|
||||||
|
|
@ -63,18 +63,18 @@ require (
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||||
github.com/pilebones/go-udev v0.9.1 // indirect
|
github.com/pilebones/go-udev v0.9.1 // indirect
|
||||||
github.com/pion/datachannel v1.5.10 // indirect
|
github.com/pion/datachannel v1.5.10 // indirect
|
||||||
github.com/pion/dtls/v3 v3.0.6 // indirect
|
github.com/pion/dtls/v3 v3.0.7 // indirect
|
||||||
github.com/pion/ice/v4 v4.0.10 // indirect
|
github.com/pion/ice/v4 v4.0.10 // indirect
|
||||||
github.com/pion/interceptor v0.1.40 // indirect
|
github.com/pion/interceptor v0.1.40 // indirect
|
||||||
github.com/pion/randutil v0.1.0 // indirect
|
github.com/pion/randutil v0.1.0 // indirect
|
||||||
github.com/pion/rtcp v1.2.15 // indirect
|
github.com/pion/rtcp v1.2.15 // indirect
|
||||||
github.com/pion/rtp v1.8.20 // indirect
|
github.com/pion/rtp v1.8.22 // indirect
|
||||||
github.com/pion/sctp v1.8.39 // indirect
|
github.com/pion/sctp v1.8.39 // indirect
|
||||||
github.com/pion/sdp/v3 v3.0.14 // indirect
|
github.com/pion/sdp/v3 v3.0.16 // indirect
|
||||||
github.com/pion/srtp/v3 v3.0.6 // indirect
|
github.com/pion/srtp/v3 v3.0.7 // indirect
|
||||||
github.com/pion/stun/v3 v3.0.0 // indirect
|
github.com/pion/stun/v3 v3.0.0 // indirect
|
||||||
github.com/pion/transport/v3 v3.0.7 // indirect
|
github.com/pion/transport/v3 v3.0.7 // indirect
|
||||||
github.com/pion/turn/v4 v4.0.2 // indirect
|
github.com/pion/turn/v4 v4.1.1 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/prometheus/client_model v0.6.2 // indirect
|
github.com/prometheus/client_model v0.6.2 // indirect
|
||||||
github.com/robfig/cron/v3 v3.0.1 // indirect
|
github.com/robfig/cron/v3 v3.0.1 // indirect
|
||||||
|
|
@ -85,7 +85,8 @@ require (
|
||||||
github.com/wlynxg/anet v0.0.5 // indirect
|
github.com/wlynxg/anet v0.0.5 // indirect
|
||||||
golang.org/x/arch v0.18.0 // indirect
|
golang.org/x/arch v0.18.0 // indirect
|
||||||
golang.org/x/oauth2 v0.30.0 // indirect
|
golang.org/x/oauth2 v0.30.0 // indirect
|
||||||
golang.org/x/text v0.27.0 // indirect
|
golang.org/x/text v0.28.0 // indirect
|
||||||
google.golang.org/protobuf v1.36.6 // indirect
|
google.golang.org/protobuf v1.36.8 // indirect
|
||||||
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|
|
||||||
80
go.sum
80
go.sum
|
|
@ -18,8 +18,8 @@ github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJ
|
||||||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||||
github.com/coder/websocket v1.8.13 h1:f3QZdXy7uGVz+4uCJy2nTZyM0yTBj8yANEHhqlXZ9FE=
|
github.com/coder/websocket v1.8.13 h1:f3QZdXy7uGVz+4uCJy2nTZyM0yTBj8yANEHhqlXZ9FE=
|
||||||
github.com/coder/websocket v1.8.13/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
|
github.com/coder/websocket v1.8.13/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
|
||||||
github.com/coreos/go-oidc/v3 v3.14.1 h1:9ePWwfdwC4QKRlCXsJGou56adA/owXczOzwKdOumLqk=
|
github.com/coreos/go-oidc/v3 v3.15.0 h1:R6Oz8Z4bqWR7VFQ+sPSvZPQv4x8M+sJkDO5ojgwlyAg=
|
||||||
github.com/coreos/go-oidc/v3 v3.14.1/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmrfah6hnSYEU=
|
github.com/coreos/go-oidc/v3 v3.15.0/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmrfah6hnSYEU=
|
||||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||||
github.com/creack/goselect v0.1.3 h1:MaGNMclRo7P2Jl21hBpR1Cn33ITSbKP6E49RtfblLKc=
|
github.com/creack/goselect v0.1.3 h1:MaGNMclRo7P2Jl21hBpR1Cn33ITSbKP6E49RtfblLKc=
|
||||||
github.com/creack/goselect v0.1.3/go.mod h1:a/NhLweNvqIYMuxcMOuWY516Cimucms3DglDzQP3hKY=
|
github.com/creack/goselect v0.1.3/go.mod h1:a/NhLweNvqIYMuxcMOuWY516Cimucms3DglDzQP3hKY=
|
||||||
|
|
@ -38,8 +38,8 @@ github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w
|
||||||
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
||||||
github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
|
github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
|
||||||
github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||||
github.com/go-co-op/gocron/v2 v2.16.3 h1:kYqukZqBa8RC2+AFAHnunmKcs9GRTjwBo8WRF3I6cbI=
|
github.com/go-co-op/gocron/v2 v2.16.5 h1:j228Jxk7bb9CF8LKR3gS+bK3rcjRUINjlVI+ZMp26Ss=
|
||||||
github.com/go-co-op/gocron/v2 v2.16.3/go.mod h1:aTf7/+5Jo2E+cyAqq625UQ6DzpkV96b22VHIUAt6l3c=
|
github.com/go-co-op/gocron/v2 v2.16.5/go.mod h1:zAfC/GFQ668qHxOVl/D68Jh5Ce7sDqX6TJnSQyRkRBc=
|
||||||
github.com/go-jose/go-jose/v4 v4.1.0 h1:cYSYxd3pw5zd2FSXk2vGdn9igQU2PS8MuxrCOCl0FdY=
|
github.com/go-jose/go-jose/v4 v4.1.0 h1:cYSYxd3pw5zd2FSXk2vGdn9igQU2PS8MuxrCOCl0FdY=
|
||||||
github.com/go-jose/go-jose/v4 v4.1.0/go.mod h1:GG/vqmYm3Von2nYiB2vGTXzdoNKE5tix5tuc6iAd+sw=
|
github.com/go-jose/go-jose/v4 v4.1.0/go.mod h1:GG/vqmYm3Von2nYiB2vGTXzdoNKE5tix5tuc6iAd+sw=
|
||||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||||
|
|
@ -58,12 +58,12 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX
|
||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc h1:GN2Lv3MGO7AS6PrRoT6yV5+wkrOpcszoIsO4+4ds248=
|
||||||
|
github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc/go.mod h1:+JKpmjMGhpgPL+rXZ5nsZieVzvarn86asRlBg4uNGnk=
|
||||||
github.com/guregu/null/v6 v6.0.0 h1:N14VRS+4di81i1PXRiprbQJ9EM9gqBa0+KVMeS/QSjQ=
|
github.com/guregu/null/v6 v6.0.0 h1:N14VRS+4di81i1PXRiprbQJ9EM9gqBa0+KVMeS/QSjQ=
|
||||||
github.com/guregu/null/v6 v6.0.0/go.mod h1:hrMIhIfrOZeLPZhROSn149tpw2gHkidAqxoXNyeX3iQ=
|
github.com/guregu/null/v6 v6.0.0/go.mod h1:hrMIhIfrOZeLPZhROSn149tpw2gHkidAqxoXNyeX3iQ=
|
||||||
github.com/gwatts/rootcerts v0.0.0-20250601184604-370a9a75f341 h1:zPrkLSKi7kKJoNJH4uUmsQ86+0/QqpwEns0NyNLwKv0=
|
github.com/gwatts/rootcerts v0.0.0-20250901182336-dc5ae18bd79f h1:08t2PbrkDgW2+mwCQ3jhKUBrCM9Bc9SeH5j2Dst3B+0=
|
||||||
github.com/gwatts/rootcerts v0.0.0-20250601184604-370a9a75f341/go.mod h1:5Kt9XkWvkGi2OHOq0QsGxebHmhCcqJ8KCbNg/a6+n+g=
|
github.com/gwatts/rootcerts v0.0.0-20250901182336-dc5ae18bd79f/go.mod h1:5Kt9XkWvkGi2OHOq0QsGxebHmhCcqJ8KCbNg/a6+n+g=
|
||||||
github.com/hanwen/go-fuse/v2 v2.8.0 h1:wV8rG7rmCz8XHSOwBZhG5YcVqcYjkzivjmbaMafPlAs=
|
|
||||||
github.com/hanwen/go-fuse/v2 v2.8.0/go.mod h1:yE6D2PqWwm3CbYRxFXV9xUd8Md5d6NG0WBs5spCswmI=
|
|
||||||
github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I=
|
github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I=
|
||||||
github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60=
|
github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60=
|
||||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
|
|
@ -92,8 +92,6 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/
|
||||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/moby/sys/mountinfo v0.7.2 h1:1shs6aH5s4o5H2zQLn796ADW1wMrIwHsyJ2v9KouLrg=
|
|
||||||
github.com/moby/sys/mountinfo v0.7.2/go.mod h1:1YOa8w8Ih7uW0wALDUgT1dTTSBrZ+HiBLGws92L2RU4=
|
|
||||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
|
@ -107,8 +105,8 @@ github.com/pilebones/go-udev v0.9.1 h1:uN72M1C1fgzhsVmBGEM8w9RD1JY4iVsPZpr+Z6rb3
|
||||||
github.com/pilebones/go-udev v0.9.1/go.mod h1:Bgcl07crebF3JSeS4+nuaRvhWFdCeFoBhXXeAp93XNo=
|
github.com/pilebones/go-udev v0.9.1/go.mod h1:Bgcl07crebF3JSeS4+nuaRvhWFdCeFoBhXXeAp93XNo=
|
||||||
github.com/pion/datachannel v1.5.10 h1:ly0Q26K1i6ZkGf42W7D4hQYR90pZwzFOjTq5AuCKk4o=
|
github.com/pion/datachannel v1.5.10 h1:ly0Q26K1i6ZkGf42W7D4hQYR90pZwzFOjTq5AuCKk4o=
|
||||||
github.com/pion/datachannel v1.5.10/go.mod h1:p/jJfC9arb29W7WrxyKbepTU20CFgyx5oLo8Rs4Py/M=
|
github.com/pion/datachannel v1.5.10/go.mod h1:p/jJfC9arb29W7WrxyKbepTU20CFgyx5oLo8Rs4Py/M=
|
||||||
github.com/pion/dtls/v3 v3.0.6 h1:7Hkd8WhAJNbRgq9RgdNh1aaWlZlGpYTzdqjy9x9sK2E=
|
github.com/pion/dtls/v3 v3.0.7 h1:bItXtTYYhZwkPFk4t1n3Kkf5TDrfj6+4wG+CZR8uI9Q=
|
||||||
github.com/pion/dtls/v3 v3.0.6/go.mod h1:iJxNQ3Uhn1NZWOMWlLxEEHAN5yX7GyPvvKw04v9bzYU=
|
github.com/pion/dtls/v3 v3.0.7/go.mod h1:uDlH5VPrgOQIw59irKYkMudSFprY9IEFCqz/eTz16f8=
|
||||||
github.com/pion/ice/v4 v4.0.10 h1:P59w1iauC/wPk9PdY8Vjl4fOFL5B+USq1+xbDcN6gT4=
|
github.com/pion/ice/v4 v4.0.10 h1:P59w1iauC/wPk9PdY8Vjl4fOFL5B+USq1+xbDcN6gT4=
|
||||||
github.com/pion/ice/v4 v4.0.10/go.mod h1:y3M18aPhIxLlcO/4dn9X8LzLLSma84cx6emMSu14FGw=
|
github.com/pion/ice/v4 v4.0.10/go.mod h1:y3M18aPhIxLlcO/4dn9X8LzLLSma84cx6emMSu14FGw=
|
||||||
github.com/pion/interceptor v0.1.40 h1:e0BjnPcGpr2CFQgKhrQisBU7V3GXK6wrfYrGYaU6Jq4=
|
github.com/pion/interceptor v0.1.40 h1:e0BjnPcGpr2CFQgKhrQisBU7V3GXK6wrfYrGYaU6Jq4=
|
||||||
|
|
@ -121,33 +119,33 @@ github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
|
||||||
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
|
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
|
||||||
github.com/pion/rtcp v1.2.15 h1:LZQi2JbdipLOj4eBjK4wlVoQWfrZbh3Q6eHtWtJBZBo=
|
github.com/pion/rtcp v1.2.15 h1:LZQi2JbdipLOj4eBjK4wlVoQWfrZbh3Q6eHtWtJBZBo=
|
||||||
github.com/pion/rtcp v1.2.15/go.mod h1:jlGuAjHMEXwMUHK78RgX0UmEJFV4zUKOFHR7OP+D3D0=
|
github.com/pion/rtcp v1.2.15/go.mod h1:jlGuAjHMEXwMUHK78RgX0UmEJFV4zUKOFHR7OP+D3D0=
|
||||||
github.com/pion/rtp v1.8.20 h1:8zcyqohadZE8FCBeGdyEvHiclPIezcwRQH9zfapFyYI=
|
github.com/pion/rtp v1.8.22 h1:8NCVDDF+uSJmMUkjLJVnIr/HX7gPesyMV1xFt5xozXc=
|
||||||
github.com/pion/rtp v1.8.20/go.mod h1:bAu2UFKScgzyFqvUKmbvzSdPr+NGbZtv6UB2hesqXBk=
|
github.com/pion/rtp v1.8.22/go.mod h1:rF5nS1GqbR7H/TCpKwylzeq6yDM+MM6k+On5EgeThEM=
|
||||||
github.com/pion/sctp v1.8.39 h1:PJma40vRHa3UTO3C4MyeJDQ+KIobVYRZQZ0Nt7SjQnE=
|
github.com/pion/sctp v1.8.39 h1:PJma40vRHa3UTO3C4MyeJDQ+KIobVYRZQZ0Nt7SjQnE=
|
||||||
github.com/pion/sctp v1.8.39/go.mod h1:cNiLdchXra8fHQwmIoqw0MbLLMs+f7uQ+dGMG2gWebE=
|
github.com/pion/sctp v1.8.39/go.mod h1:cNiLdchXra8fHQwmIoqw0MbLLMs+f7uQ+dGMG2gWebE=
|
||||||
github.com/pion/sdp/v3 v3.0.14 h1:1h7gBr9FhOWH5GjWWY5lcw/U85MtdcibTyt/o6RxRUI=
|
github.com/pion/sdp/v3 v3.0.16 h1:0dKzYO6gTAvuLaAKQkC02eCPjMIi4NuAr/ibAwrGDCo=
|
||||||
github.com/pion/sdp/v3 v3.0.14/go.mod h1:88GMahN5xnScv1hIMTqLdu/cOcUkj6a9ytbncwMCq2E=
|
github.com/pion/sdp/v3 v3.0.16/go.mod h1:9tyKzznud3qiweZcD86kS0ff1pGYB3VX+Bcsmkx6IXo=
|
||||||
github.com/pion/srtp/v3 v3.0.6 h1:E2gyj1f5X10sB/qILUGIkL4C2CqK269Xq167PbGCc/4=
|
github.com/pion/srtp/v3 v3.0.7 h1:QUElw0A/FUg3MP8/KNMZB3i0m8F9XeMnTum86F7S4bs=
|
||||||
github.com/pion/srtp/v3 v3.0.6/go.mod h1:BxvziG3v/armJHAaJ87euvkhHqWe9I7iiOy50K2QkhY=
|
github.com/pion/srtp/v3 v3.0.7/go.mod h1:qvnHeqbhT7kDdB+OGB05KA/P067G3mm7XBfLaLiaNF0=
|
||||||
github.com/pion/stun/v3 v3.0.0 h1:4h1gwhWLWuZWOJIJR9s2ferRO+W3zA/b6ijOI6mKzUw=
|
github.com/pion/stun/v3 v3.0.0 h1:4h1gwhWLWuZWOJIJR9s2ferRO+W3zA/b6ijOI6mKzUw=
|
||||||
github.com/pion/stun/v3 v3.0.0/go.mod h1:HvCN8txt8mwi4FBvS3EmDghW6aQJ24T+y+1TKjB5jyU=
|
github.com/pion/stun/v3 v3.0.0/go.mod h1:HvCN8txt8mwi4FBvS3EmDghW6aQJ24T+y+1TKjB5jyU=
|
||||||
github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0=
|
github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0=
|
||||||
github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo=
|
github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo=
|
||||||
github.com/pion/turn/v4 v4.0.2 h1:ZqgQ3+MjP32ug30xAbD6Mn+/K4Sxi3SdNOTFf+7mpps=
|
github.com/pion/turn/v4 v4.1.1 h1:9UnY2HB99tpDyz3cVVZguSxcqkJ1DsTSZ+8TGruh4fc=
|
||||||
github.com/pion/turn/v4 v4.0.2/go.mod h1:pMMKP/ieNAG/fN5cZiN4SDuyKsXtNTr0ccN7IToA1zs=
|
github.com/pion/turn/v4 v4.1.1/go.mod h1:2123tHk1O++vmjI5VSD0awT50NywDAq5A2NNNU4Jjs8=
|
||||||
github.com/pion/webrtc/v4 v4.1.3 h1:YZ67Boj9X/hk190jJZ8+HFGQ6DqSZ/fYP3sLAZv7c3c=
|
github.com/pion/webrtc/v4 v4.1.4 h1:/gK1ACGHXQmtyVVbJFQDxNoODg4eSRiFLB7t9r9pg8M=
|
||||||
github.com/pion/webrtc/v4 v4.1.3/go.mod h1:rsq+zQ82ryfR9vbb0L1umPJ6Ogq7zm8mcn9fcGnxomM=
|
github.com/pion/webrtc/v4 v4.1.4/go.mod h1:Oab9npu1iZtQRMic3K3toYq5zFPvToe/QBw7dMI2ok4=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
|
github.com/prometheus/client_golang v1.23.0 h1:ust4zpdl9r4trLY/gSjlm07PuiBq2ynaXXlptpfy8Uc=
|
||||||
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
|
github.com/prometheus/client_golang v1.23.0/go.mod h1:i/o0R9ByOnHX0McrTMTyhYvKE4haaf2mW08I+jGAjEE=
|
||||||
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||||
github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE=
|
github.com/prometheus/common v0.66.0 h1:K/rJPHrG3+AoQs50r2+0t7zMnMzek2Vbv31OFVsMeVY=
|
||||||
github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
|
github.com/prometheus/common v0.66.0/go.mod h1:Ux6NtV1B4LatamKE63tJBntoxD++xmtI/lK0VtEplN4=
|
||||||
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
|
github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0=
|
||||||
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
|
github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw=
|
||||||
github.com/psanford/httpreadat v0.1.0 h1:VleW1HS2zO7/4c7c7zNl33fO6oYACSagjJIyMIwZLUE=
|
github.com/psanford/httpreadat v0.1.0 h1:VleW1HS2zO7/4c7c7zNl33fO6oYACSagjJIyMIwZLUE=
|
||||||
github.com/psanford/httpreadat v0.1.0/go.mod h1:Zg7P+TlBm3bYbyHTKv/EdtSJZn3qwbPwpfZ/I9GKCRE=
|
github.com/psanford/httpreadat v0.1.0/go.mod h1:Zg7P+TlBm3bYbyHTKv/EdtSJZn3qwbPwpfZ/I9GKCRE=
|
||||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||||
|
|
@ -167,8 +165,8 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
|
||||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||||
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
|
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
|
||||||
|
|
@ -185,10 +183,10 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||||
golang.org/x/arch v0.18.0 h1:WN9poc33zL4AzGxqf8VtpKUnGvMi8O9lhNyBMF/85qc=
|
golang.org/x/arch v0.18.0 h1:WN9poc33zL4AzGxqf8VtpKUnGvMi8O9lhNyBMF/85qc=
|
||||||
golang.org/x/arch v0.18.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
|
golang.org/x/arch v0.18.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
|
||||||
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
|
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
|
||||||
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
|
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
|
||||||
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
|
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
|
||||||
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
|
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
|
||||||
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
|
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
|
||||||
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
|
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
|
||||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
|
@ -196,15 +194,17 @@ golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
|
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||||
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
|
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
||||||
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
|
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
||||||
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
|
||||||
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,162 @@
|
||||||
|
package kvm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/jetkvm/kvm/internal/hidrpc"
|
||||||
|
"github.com/jetkvm/kvm/internal/usbgadget"
|
||||||
|
)
|
||||||
|
|
||||||
|
func handleHidRPCMessage(message hidrpc.Message, session *Session) {
|
||||||
|
var rpcErr error
|
||||||
|
|
||||||
|
switch message.Type() {
|
||||||
|
case hidrpc.TypeHandshake:
|
||||||
|
message, err := hidrpc.NewHandshakeMessage().Marshal()
|
||||||
|
if err != nil {
|
||||||
|
logger.Warn().Err(err).Msg("failed to marshal handshake message")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := session.HidChannel.Send(message); err != nil {
|
||||||
|
logger.Warn().Err(err).Msg("failed to send handshake message")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
session.hidRPCAvailable = true
|
||||||
|
case hidrpc.TypeKeypressReport, hidrpc.TypeKeyboardReport:
|
||||||
|
keysDownState, err := handleHidRPCKeyboardInput(message)
|
||||||
|
if keysDownState != nil {
|
||||||
|
session.reportHidRPCKeysDownState(*keysDownState)
|
||||||
|
}
|
||||||
|
rpcErr = err
|
||||||
|
case hidrpc.TypePointerReport:
|
||||||
|
pointerReport, err := message.PointerReport()
|
||||||
|
if err != nil {
|
||||||
|
logger.Warn().Err(err).Msg("failed to get pointer report")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rpcErr = rpcAbsMouseReport(pointerReport.X, pointerReport.Y, pointerReport.Button)
|
||||||
|
case hidrpc.TypeMouseReport:
|
||||||
|
mouseReport, err := message.MouseReport()
|
||||||
|
if err != nil {
|
||||||
|
logger.Warn().Err(err).Msg("failed to get mouse report")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rpcErr = rpcRelMouseReport(mouseReport.DX, mouseReport.DY, mouseReport.Button)
|
||||||
|
default:
|
||||||
|
logger.Warn().Uint8("type", uint8(message.Type())).Msg("unknown HID RPC message type")
|
||||||
|
}
|
||||||
|
|
||||||
|
if rpcErr != nil {
|
||||||
|
logger.Warn().Err(rpcErr).Msg("failed to handle HID RPC message")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func onHidMessage(data []byte, session *Session) {
|
||||||
|
scopedLogger := hidRPCLogger.With().Bytes("data", data).Logger()
|
||||||
|
scopedLogger.Debug().Msg("HID RPC message received")
|
||||||
|
|
||||||
|
if len(data) < 1 {
|
||||||
|
scopedLogger.Warn().Int("length", len(data)).Msg("received empty data in HID RPC message handler")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var message hidrpc.Message
|
||||||
|
|
||||||
|
if err := hidrpc.Unmarshal(data, &message); err != nil {
|
||||||
|
scopedLogger.Warn().Err(err).Msg("failed to unmarshal HID RPC message")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
scopedLogger = scopedLogger.With().Str("descr", message.String()).Logger()
|
||||||
|
|
||||||
|
t := time.Now()
|
||||||
|
|
||||||
|
r := make(chan interface{})
|
||||||
|
go func() {
|
||||||
|
handleHidRPCMessage(message, session)
|
||||||
|
r <- nil
|
||||||
|
}()
|
||||||
|
select {
|
||||||
|
case <-time.After(1 * time.Second):
|
||||||
|
scopedLogger.Warn().Msg("HID RPC message timed out")
|
||||||
|
case <-r:
|
||||||
|
scopedLogger.Debug().Dur("duration", time.Since(t)).Msg("HID RPC message handled")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleHidRPCKeyboardInput(message hidrpc.Message) (*usbgadget.KeysDownState, error) {
|
||||||
|
switch message.Type() {
|
||||||
|
case hidrpc.TypeKeypressReport:
|
||||||
|
keypressReport, err := message.KeypressReport()
|
||||||
|
if err != nil {
|
||||||
|
logger.Warn().Err(err).Msg("failed to get keypress report")
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
keysDownState, rpcError := rpcKeypressReport(keypressReport.Key, keypressReport.Press)
|
||||||
|
return &keysDownState, rpcError
|
||||||
|
case hidrpc.TypeKeyboardReport:
|
||||||
|
keyboardReport, err := message.KeyboardReport()
|
||||||
|
if err != nil {
|
||||||
|
logger.Warn().Err(err).Msg("failed to get keyboard report")
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
keysDownState, rpcError := rpcKeyboardReport(keyboardReport.Modifier, keyboardReport.Keys)
|
||||||
|
return &keysDownState, rpcError
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("unknown HID RPC message type: %d", message.Type())
|
||||||
|
}
|
||||||
|
|
||||||
|
func reportHidRPC(params any, session *Session) {
|
||||||
|
if session == nil {
|
||||||
|
logger.Warn().Msg("session is nil, skipping reportHidRPC")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !session.hidRPCAvailable || session.HidChannel == nil {
|
||||||
|
logger.Warn().Msg("HID RPC is not available, skipping reportHidRPC")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
message []byte
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
switch params := params.(type) {
|
||||||
|
case usbgadget.KeyboardState:
|
||||||
|
message, err = hidrpc.NewKeyboardLedMessage(params).Marshal()
|
||||||
|
case usbgadget.KeysDownState:
|
||||||
|
message, err = hidrpc.NewKeydownStateMessage(params).Marshal()
|
||||||
|
default:
|
||||||
|
err = fmt.Errorf("unknown HID RPC message type: %T", params)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
logger.Warn().Err(err).Msg("failed to marshal HID RPC message")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if message == nil {
|
||||||
|
logger.Warn().Msg("failed to marshal HID RPC message")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := session.HidChannel.Send(message); err != nil {
|
||||||
|
logger.Warn().Err(err).Msg("failed to send HID RPC message")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Session) reportHidRPCKeyboardLedState(state usbgadget.KeyboardState) {
|
||||||
|
if !s.hidRPCAvailable {
|
||||||
|
writeJSONRPCEvent("keyboardLedState", state, s)
|
||||||
|
}
|
||||||
|
reportHidRPC(state, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Session) reportHidRPCKeysDownState(state usbgadget.KeysDownState) {
|
||||||
|
if !s.hidRPCAvailable {
|
||||||
|
writeJSONRPCEvent("keysDownState", state, s)
|
||||||
|
}
|
||||||
|
reportHidRPC(state, s)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,100 @@
|
||||||
|
package hidrpc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/jetkvm/kvm/internal/usbgadget"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MessageType is the type of the HID RPC message
|
||||||
|
type MessageType byte
|
||||||
|
|
||||||
|
const (
|
||||||
|
TypeHandshake MessageType = 0x01
|
||||||
|
TypeKeyboardReport MessageType = 0x02
|
||||||
|
TypePointerReport MessageType = 0x03
|
||||||
|
TypeWheelReport MessageType = 0x04
|
||||||
|
TypeKeypressReport MessageType = 0x05
|
||||||
|
TypeMouseReport MessageType = 0x06
|
||||||
|
TypeKeyboardLedState MessageType = 0x32
|
||||||
|
TypeKeydownState MessageType = 0x33
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
Version byte = 0x01 // Version of the HID RPC protocol
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetQueueIndex returns the index of the queue to which the message should be enqueued.
|
||||||
|
func GetQueueIndex(messageType MessageType) int {
|
||||||
|
switch messageType {
|
||||||
|
case TypeHandshake:
|
||||||
|
return 0
|
||||||
|
case TypeKeyboardReport, TypeKeypressReport, TypeKeyboardLedState, TypeKeydownState:
|
||||||
|
return 1
|
||||||
|
case TypePointerReport, TypeMouseReport, TypeWheelReport:
|
||||||
|
return 2
|
||||||
|
default:
|
||||||
|
return 3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unmarshal unmarshals the HID RPC message from the data.
|
||||||
|
func Unmarshal(data []byte, message *Message) error {
|
||||||
|
l := len(data)
|
||||||
|
if l < 1 {
|
||||||
|
return fmt.Errorf("invalid data length: %d", l)
|
||||||
|
}
|
||||||
|
|
||||||
|
message.t = MessageType(data[0])
|
||||||
|
message.d = data[1:]
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marshal marshals the HID RPC message to the data.
|
||||||
|
func Marshal(message *Message) ([]byte, error) {
|
||||||
|
if message.t == 0 {
|
||||||
|
return nil, fmt.Errorf("invalid message type: %d", message.t)
|
||||||
|
}
|
||||||
|
|
||||||
|
data := make([]byte, len(message.d)+1)
|
||||||
|
data[0] = byte(message.t)
|
||||||
|
copy(data[1:], message.d)
|
||||||
|
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewHandshakeMessage creates a new handshake message.
|
||||||
|
func NewHandshakeMessage() *Message {
|
||||||
|
return &Message{
|
||||||
|
t: TypeHandshake,
|
||||||
|
d: []byte{Version},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewKeyboardReportMessage creates a new keyboard report message.
|
||||||
|
func NewKeyboardReportMessage(keys []byte, modifier uint8) *Message {
|
||||||
|
return &Message{
|
||||||
|
t: TypeKeyboardReport,
|
||||||
|
d: append([]byte{modifier}, keys...),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewKeyboardLedMessage creates a new keyboard LED message.
|
||||||
|
func NewKeyboardLedMessage(state usbgadget.KeyboardState) *Message {
|
||||||
|
return &Message{
|
||||||
|
t: TypeKeyboardLedState,
|
||||||
|
d: []byte{state.Byte()},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewKeydownStateMessage creates a new keydown state message.
|
||||||
|
func NewKeydownStateMessage(state usbgadget.KeysDownState) *Message {
|
||||||
|
data := make([]byte, len(state.Keys)+1)
|
||||||
|
data[0] = state.Modifier
|
||||||
|
copy(data[1:], state.Keys)
|
||||||
|
|
||||||
|
return &Message{
|
||||||
|
t: TypeKeydownState,
|
||||||
|
d: data,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,133 @@
|
||||||
|
package hidrpc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Message ..
|
||||||
|
type Message struct {
|
||||||
|
t MessageType
|
||||||
|
d []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marshal marshals the message to a byte array.
|
||||||
|
func (m *Message) Marshal() ([]byte, error) {
|
||||||
|
return Marshal(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Message) Type() MessageType {
|
||||||
|
return m.t
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Message) String() string {
|
||||||
|
switch m.t {
|
||||||
|
case TypeHandshake:
|
||||||
|
return "Handshake"
|
||||||
|
case TypeKeypressReport:
|
||||||
|
if len(m.d) < 2 {
|
||||||
|
return fmt.Sprintf("KeypressReport{Malformed: %v}", m.d)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("KeypressReport{Key: %d, Press: %v}", m.d[0], m.d[1] == uint8(1))
|
||||||
|
case TypeKeyboardReport:
|
||||||
|
if len(m.d) < 2 {
|
||||||
|
return fmt.Sprintf("KeyboardReport{Malformed: %v}", m.d)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("KeyboardReport{Modifier: %d, Keys: %v}", m.d[0], m.d[1:])
|
||||||
|
case TypePointerReport:
|
||||||
|
if len(m.d) < 9 {
|
||||||
|
return fmt.Sprintf("PointerReport{Malformed: %v}", m.d)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("PointerReport{X: %d, Y: %d, Button: %d}", m.d[0:4], m.d[4:8], m.d[8])
|
||||||
|
case TypeMouseReport:
|
||||||
|
if len(m.d) < 3 {
|
||||||
|
return fmt.Sprintf("MouseReport{Malformed: %v}", m.d)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("MouseReport{DX: %d, DY: %d, Button: %d}", m.d[0], m.d[1], m.d[2])
|
||||||
|
default:
|
||||||
|
return fmt.Sprintf("Unknown{Type: %d, Data: %v}", m.t, m.d)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// KeypressReport ..
|
||||||
|
type KeypressReport struct {
|
||||||
|
Key byte
|
||||||
|
Press bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// KeypressReport returns the keypress report from the message.
|
||||||
|
func (m *Message) KeypressReport() (KeypressReport, error) {
|
||||||
|
if m.t != TypeKeypressReport {
|
||||||
|
return KeypressReport{}, fmt.Errorf("invalid message type: %d", m.t)
|
||||||
|
}
|
||||||
|
|
||||||
|
return KeypressReport{
|
||||||
|
Key: m.d[0],
|
||||||
|
Press: m.d[1] == uint8(1),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// KeyboardReport ..
|
||||||
|
type KeyboardReport struct {
|
||||||
|
Modifier byte
|
||||||
|
Keys []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// KeyboardReport returns the keyboard report from the message.
|
||||||
|
func (m *Message) KeyboardReport() (KeyboardReport, error) {
|
||||||
|
if m.t != TypeKeyboardReport {
|
||||||
|
return KeyboardReport{}, fmt.Errorf("invalid message type: %d", m.t)
|
||||||
|
}
|
||||||
|
|
||||||
|
return KeyboardReport{
|
||||||
|
Modifier: m.d[0],
|
||||||
|
Keys: m.d[1:],
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PointerReport ..
|
||||||
|
type PointerReport struct {
|
||||||
|
X int
|
||||||
|
Y int
|
||||||
|
Button uint8
|
||||||
|
}
|
||||||
|
|
||||||
|
func toInt(b []byte) int {
|
||||||
|
return int(b[0])<<24 + int(b[1])<<16 + int(b[2])<<8 + int(b[3])<<0
|
||||||
|
}
|
||||||
|
|
||||||
|
// PointerReport returns the point report from the message.
|
||||||
|
func (m *Message) PointerReport() (PointerReport, error) {
|
||||||
|
if m.t != TypePointerReport {
|
||||||
|
return PointerReport{}, fmt.Errorf("invalid message type: %d", m.t)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(m.d) != 9 {
|
||||||
|
return PointerReport{}, fmt.Errorf("invalid message length: %d", len(m.d))
|
||||||
|
}
|
||||||
|
|
||||||
|
return PointerReport{
|
||||||
|
X: toInt(m.d[0:4]),
|
||||||
|
Y: toInt(m.d[4:8]),
|
||||||
|
Button: uint8(m.d[8]),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MouseReport ..
|
||||||
|
type MouseReport struct {
|
||||||
|
DX int8
|
||||||
|
DY int8
|
||||||
|
Button uint8
|
||||||
|
}
|
||||||
|
|
||||||
|
// MouseReport returns the mouse report from the message.
|
||||||
|
func (m *Message) MouseReport() (MouseReport, error) {
|
||||||
|
if m.t != TypeMouseReport {
|
||||||
|
return MouseReport{}, fmt.Errorf("invalid message type: %d", m.t)
|
||||||
|
}
|
||||||
|
|
||||||
|
return MouseReport{
|
||||||
|
DX: int8(m.d[0]),
|
||||||
|
DY: int8(m.d[1]),
|
||||||
|
Button: uint8(m.d[2]),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,7 @@
|
||||||
package usbgadget
|
package usbgadget
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
const dwc3Path = "/sys/bus/platform/drivers/dwc3"
|
const dwc3Path = "/sys/bus/platform/drivers/dwc3"
|
||||||
|
|
||||||
|
const hidWriteTimeout = 10 * time.Millisecond
|
||||||
|
|
|
||||||
|
|
@ -86,6 +86,12 @@ type KeyboardState struct {
|
||||||
Compose bool `json:"compose"`
|
Compose bool `json:"compose"`
|
||||||
Kana bool `json:"kana"`
|
Kana bool `json:"kana"`
|
||||||
Shift bool `json:"shift"` // This is not part of the main USB HID spec
|
Shift bool `json:"shift"` // This is not part of the main USB HID spec
|
||||||
|
raw byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// Byte returns the raw byte representation of the keyboard state.
|
||||||
|
func (k *KeyboardState) Byte() byte {
|
||||||
|
return k.raw
|
||||||
}
|
}
|
||||||
|
|
||||||
func getKeyboardState(b byte) KeyboardState {
|
func getKeyboardState(b byte) KeyboardState {
|
||||||
|
|
@ -97,6 +103,7 @@ func getKeyboardState(b byte) KeyboardState {
|
||||||
Compose: b&KeyboardLedMaskCompose != 0,
|
Compose: b&KeyboardLedMaskCompose != 0,
|
||||||
Kana: b&KeyboardLedMaskKana != 0,
|
Kana: b&KeyboardLedMaskKana != 0,
|
||||||
Shift: b&KeyboardLedMaskShift != 0,
|
Shift: b&KeyboardLedMaskShift != 0,
|
||||||
|
raw: b,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -139,19 +146,26 @@ func (u *UsbGadget) GetKeysDownState() KeysDownState {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *UsbGadget) updateKeyDownState(state KeysDownState) {
|
func (u *UsbGadget) updateKeyDownState(state KeysDownState) {
|
||||||
u.keyboardStateLock.Lock()
|
u.log.Trace().Interface("old", u.keysDownState).Interface("new", state).Msg("acquiring keyboardStateLock for updateKeyDownState")
|
||||||
defer u.keyboardStateLock.Unlock()
|
|
||||||
|
|
||||||
if u.keysDownState.Modifier == state.Modifier &&
|
// this is intentional to unlock keyboard state lock before onKeysDownChange callback
|
||||||
bytes.Equal(u.keysDownState.Keys, state.Keys) {
|
{
|
||||||
return // No change in key down state
|
u.keyboardStateLock.Lock()
|
||||||
|
defer u.keyboardStateLock.Unlock()
|
||||||
|
|
||||||
|
if u.keysDownState.Modifier == state.Modifier &&
|
||||||
|
bytes.Equal(u.keysDownState.Keys, state.Keys) {
|
||||||
|
return // No change in key down state
|
||||||
|
}
|
||||||
|
|
||||||
|
u.log.Trace().Interface("old", u.keysDownState).Interface("new", state).Msg("keysDownState updated")
|
||||||
|
u.keysDownState = state
|
||||||
}
|
}
|
||||||
|
|
||||||
u.log.Trace().Interface("old", u.keysDownState).Interface("new", state).Msg("keysDownState updated")
|
|
||||||
u.keysDownState = state
|
|
||||||
|
|
||||||
if u.onKeysDownChange != nil {
|
if u.onKeysDownChange != nil {
|
||||||
|
u.log.Trace().Interface("state", state).Msg("calling onKeysDownChange")
|
||||||
(*u.onKeysDownChange)(state)
|
(*u.onKeysDownChange)(state)
|
||||||
|
u.log.Trace().Interface("state", state).Msg("onKeysDownChange called")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -233,7 +247,7 @@ func (u *UsbGadget) keyboardWriteHidFile(modifier byte, keys []byte) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := u.keyboardHidFile.Write(append([]byte{modifier, 0x00}, keys[:hidKeyBufferSize]...))
|
_, err := u.writeWithTimeout(u.keyboardHidFile, append([]byte{modifier, 0x00}, keys[:hidKeyBufferSize]...))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
u.logWithSuppression("keyboardWriteHidFile", 100, u.log, err, "failed to write to hidg0")
|
u.logWithSuppression("keyboardWriteHidFile", 100, u.log, err, "failed to write to hidg0")
|
||||||
u.keyboardHidFile.Close()
|
u.keyboardHidFile.Close()
|
||||||
|
|
|
||||||
|
|
@ -74,7 +74,7 @@ func (u *UsbGadget) absMouseWriteHidFile(data []byte) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := u.absMouseHidFile.Write(data)
|
_, err := u.writeWithTimeout(u.absMouseHidFile, data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
u.logWithSuppression("absMouseWriteHidFile", 100, u.log, err, "failed to write to hidg1")
|
u.logWithSuppression("absMouseWriteHidFile", 100, u.log, err, "failed to write to hidg1")
|
||||||
u.absMouseHidFile.Close()
|
u.absMouseHidFile.Close()
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,7 @@ func (u *UsbGadget) relMouseWriteHidFile(data []byte) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := u.relMouseHidFile.Write(data)
|
_, err := u.writeWithTimeout(u.relMouseHidFile, data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
u.logWithSuppression("relMouseWriteHidFile", 100, u.log, err, "failed to write to hidg2")
|
u.logWithSuppression("relMouseWriteHidFile", 100, u.log, err, "failed to write to hidg2")
|
||||||
u.relMouseHidFile.Close()
|
u.relMouseHidFile.Close()
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,13 @@ package usbgadget
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
)
|
)
|
||||||
|
|
@ -107,6 +110,31 @@ func compareFileContent(oldContent []byte, newContent []byte, looserMatch bool)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *UsbGadget) writeWithTimeout(file *os.File, data []byte) (n int, err error) {
|
||||||
|
if err := file.SetWriteDeadline(time.Now().Add(hidWriteTimeout)); err != nil {
|
||||||
|
return -1, err
|
||||||
|
}
|
||||||
|
|
||||||
|
n, err = file.Write(data)
|
||||||
|
if err == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if errors.Is(err, os.ErrDeadlineExceeded) {
|
||||||
|
u.logWithSuppression(
|
||||||
|
fmt.Sprintf("writeWithTimeout_%s", file.Name()),
|
||||||
|
1000,
|
||||||
|
u.log,
|
||||||
|
err,
|
||||||
|
"write timed out: %s",
|
||||||
|
file.Name(),
|
||||||
|
)
|
||||||
|
err = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
func (u *UsbGadget) logWithSuppression(counterName string, every int, logger *zerolog.Logger, err error, msg string, args ...any) {
|
func (u *UsbGadget) logWithSuppression(counterName string, every int, logger *zerolog.Logger, err error, msg string, args ...any) {
|
||||||
u.logSuppressionLock.Lock()
|
u.logSuppressionLock.Lock()
|
||||||
defer u.logSuppressionLock.Unlock()
|
defer u.logSuppressionLock.Unlock()
|
||||||
|
|
|
||||||
|
|
@ -83,7 +83,7 @@ func writeJSONRPCEvent(event string, params any, session *Session) {
|
||||||
Str("data", requestString).
|
Str("data", requestString).
|
||||||
Logger()
|
Logger()
|
||||||
|
|
||||||
scopedLogger.Info().Msg("sending JSONRPC event")
|
scopedLogger.Trace().Msg("sending JSONRPC event")
|
||||||
|
|
||||||
err = session.RPCChannel.SendText(requestString)
|
err = session.RPCChannel.SendText(requestString)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
1
log.go
1
log.go
|
|
@ -19,6 +19,7 @@ var (
|
||||||
nbdLogger = logging.GetSubsystemLogger("nbd")
|
nbdLogger = logging.GetSubsystemLogger("nbd")
|
||||||
timesyncLogger = logging.GetSubsystemLogger("timesync")
|
timesyncLogger = logging.GetSubsystemLogger("timesync")
|
||||||
jsonRpcLogger = logging.GetSubsystemLogger("jsonrpc")
|
jsonRpcLogger = logging.GetSubsystemLogger("jsonrpc")
|
||||||
|
hidRPCLogger = logging.GetSubsystemLogger("hidrpc")
|
||||||
watchdogLogger = logging.GetSubsystemLogger("watchdog")
|
watchdogLogger = logging.GetSubsystemLogger("watchdog")
|
||||||
websecureLogger = logging.GetSubsystemLogger("websecure")
|
websecureLogger = logging.GetSubsystemLogger("websecure")
|
||||||
otaLogger = logging.GetSubsystemLogger("ota")
|
otaLogger = logging.GetSubsystemLogger("ota")
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "kvm-ui",
|
"name": "kvm-ui",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "2025.08.25.2300",
|
"version": "2025.09.03.2100",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "22.15.0"
|
"node": "22.15.0"
|
||||||
|
|
@ -30,7 +30,7 @@
|
||||||
"@xterm/addon-webgl": "^0.18.0",
|
"@xterm/addon-webgl": "^0.18.0",
|
||||||
"@xterm/xterm": "^5.5.0",
|
"@xterm/xterm": "^5.5.0",
|
||||||
"cva": "^1.0.0-beta.4",
|
"cva": "^1.0.0-beta.4",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.18",
|
||||||
"eslint-import-resolver-alias": "^1.1.2",
|
"eslint-import-resolver-alias": "^1.1.2",
|
||||||
"focus-trap-react": "^11.0.4",
|
"focus-trap-react": "^11.0.4",
|
||||||
"framer-motion": "^12.23.12",
|
"framer-motion": "^12.23.12",
|
||||||
|
|
@ -41,11 +41,11 @@
|
||||||
"react-dom": "^19.1.1",
|
"react-dom": "^19.1.1",
|
||||||
"react-hot-toast": "^2.6.0",
|
"react-hot-toast": "^2.6.0",
|
||||||
"react-icons": "^5.5.0",
|
"react-icons": "^5.5.0",
|
||||||
"react-router-dom": "^6.22.3",
|
"react-router": "^7.8.2",
|
||||||
"react-simple-keyboard": "^3.8.115",
|
"react-simple-keyboard": "^3.8.119",
|
||||||
"react-use-websocket": "^4.13.0",
|
"react-use-websocket": "^4.13.0",
|
||||||
"react-xtermjs": "^1.0.10",
|
"react-xtermjs": "^1.0.10",
|
||||||
"recharts": "^2.15.3",
|
"recharts": "^3.1.2",
|
||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
"usehooks-ts": "^3.1.1",
|
"usehooks-ts": "^3.1.1",
|
||||||
"validator": "^13.15.15",
|
"validator": "^13.15.15",
|
||||||
|
|
@ -59,13 +59,13 @@
|
||||||
"@tailwindcss/postcss": "^4.1.12",
|
"@tailwindcss/postcss": "^4.1.12",
|
||||||
"@tailwindcss/typography": "^0.5.16",
|
"@tailwindcss/typography": "^0.5.16",
|
||||||
"@tailwindcss/vite": "^4.1.12",
|
"@tailwindcss/vite": "^4.1.12",
|
||||||
"@types/react": "^19.1.11",
|
"@types/react": "^19.1.12",
|
||||||
"@types/react-dom": "^19.1.8",
|
"@types/react-dom": "^19.1.9",
|
||||||
"@types/semver": "^7.7.0",
|
"@types/semver": "^7.7.1",
|
||||||
"@types/validator": "^13.15.2",
|
"@types/validator": "^13.15.3",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.41.0",
|
"@typescript-eslint/eslint-plugin": "^8.42.0",
|
||||||
"@typescript-eslint/parser": "^8.41.0",
|
"@typescript-eslint/parser": "^8.42.0",
|
||||||
"@vitejs/plugin-react-swc": "^3.10.2",
|
"@vitejs/plugin-react-swc": "^4.0.1",
|
||||||
"autoprefixer": "^10.4.21",
|
"autoprefixer": "^10.4.21",
|
||||||
"eslint": "^9.34.0",
|
"eslint": "^9.34.0",
|
||||||
"eslint-config-prettier": "^10.1.8",
|
"eslint-config-prettier": "^10.1.8",
|
||||||
|
|
@ -79,7 +79,7 @@
|
||||||
"prettier-plugin-tailwindcss": "^0.6.14",
|
"prettier-plugin-tailwindcss": "^0.6.14",
|
||||||
"tailwindcss": "^4.1.12",
|
"tailwindcss": "^4.1.12",
|
||||||
"typescript": "^5.9.2",
|
"typescript": "^5.9.2",
|
||||||
"vite": "^6.3.5",
|
"vite": "^7.1.4",
|
||||||
"vite-tsconfig-paths": "^5.1.4"
|
"vite-tsconfig-paths": "^5.1.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useLocation, useNavigation, useSearchParams } from "react-router-dom";
|
import { useLocation, useNavigation, useSearchParams } from "react-router";
|
||||||
|
|
||||||
import { Button, LinkButton } from "@components/Button";
|
import { Button, LinkButton } from "@components/Button";
|
||||||
import { GoogleIcon } from "@components/Icons";
|
import { GoogleIcon } from "@components/Icons";
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import React, { JSX } from "react";
|
import React, { JSX } from "react";
|
||||||
import { FetcherWithComponents, Link, LinkProps, useNavigation } from "react-router-dom";
|
import { Link, useNavigation } from "react-router";
|
||||||
|
import type { FetcherWithComponents, LinkProps } from "react-router";
|
||||||
|
|
||||||
import ExtLink from "@/components/ExtLink";
|
import ExtLink from "@/components/ExtLink";
|
||||||
import LoadingSpinner from "@/components/LoadingSpinner";
|
import LoadingSpinner from "@/components/LoadingSpinner";
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { FetcherWithComponents, useNavigation } from "react-router-dom";
|
import { useNavigation } from "react-router";
|
||||||
|
import type { FetcherWithComponents } from "react-router";
|
||||||
|
|
||||||
export default function Fieldset({
|
export default function Fieldset({
|
||||||
children,
|
children,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router";
|
||||||
import { ArrowLeftEndOnRectangleIcon, ChevronDownIcon } from "@heroicons/react/16/solid";
|
import { ArrowLeftEndOnRectangleIcon, ChevronDownIcon } from "@heroicons/react/16/solid";
|
||||||
import { Button, Menu, MenuButton, MenuItem, MenuItems } from "@headlessui/react";
|
import { Button, Menu, MenuButton, MenuItem, MenuItems } from "@headlessui/react";
|
||||||
import { LuMonitorSmartphone } from "react-icons/lu";
|
import { LuMonitorSmartphone } from "react-icons/lu";
|
||||||
|
|
|
||||||
|
|
@ -10,10 +10,12 @@ import {
|
||||||
VideoState
|
VideoState
|
||||||
} from "@/hooks/stores";
|
} from "@/hooks/stores";
|
||||||
import { keys, modifiers } from "@/keyboardMappings";
|
import { keys, modifiers } from "@/keyboardMappings";
|
||||||
|
import { useHidRpc } from "@/hooks/useHidRpc";
|
||||||
|
|
||||||
export default function InfoBar() {
|
export default function InfoBar() {
|
||||||
const { keysDownState } = useHidStore();
|
const { keysDownState } = useHidStore();
|
||||||
const { mouseX, mouseY, mouseMove } = useMouseStore();
|
const { mouseX, mouseY, mouseMove } = useMouseStore();
|
||||||
|
const { rpcHidStatus } = useHidRpc();
|
||||||
|
|
||||||
const videoClientSize = useVideoStore(
|
const videoClientSize = useVideoStore(
|
||||||
(state: VideoState) => `${Math.round(state.clientWidth)}x${Math.round(state.clientHeight)}`,
|
(state: VideoState) => `${Math.round(state.clientWidth)}x${Math.round(state.clientHeight)}`,
|
||||||
|
|
@ -46,7 +48,7 @@ export default function InfoBar() {
|
||||||
const modifierNames = Object.entries(modifiers).filter(([_, mask]) => (activeModifierMask & mask) !== 0).map(([name, _]) => name);
|
const modifierNames = Object.entries(modifiers).filter(([_, mask]) => (activeModifierMask & mask) !== 0).map(([name, _]) => name);
|
||||||
const keyNames = Object.entries(keys).filter(([_, value]) => keysDown.includes(value)).map(([name, _]) => name);
|
const keyNames = Object.entries(keys).filter(([_, value]) => keysDown.includes(value)).map(([name, _]) => name);
|
||||||
|
|
||||||
return [...modifierNames,...keyNames].join(", ");
|
return [...modifierNames, ...keyNames].join(", ");
|
||||||
}, [keysDownState, showPressedKeys]);
|
}, [keysDownState, showPressedKeys]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -100,6 +102,12 @@ export default function InfoBar() {
|
||||||
<span className="text-xs">{hdmiState}</span>
|
<span className="text-xs">{hdmiState}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{debugMode && (
|
||||||
|
<div className="flex w-[156px] items-center gap-x-1">
|
||||||
|
<span className="text-xs font-semibold">HidRPC State:</span>
|
||||||
|
<span className="text-xs">{rpcHidStatus}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{showPressedKeys && (
|
{showPressedKeys && (
|
||||||
<div className="flex items-center gap-x-1">
|
<div className="flex items-center gap-x-1">
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { MdConnectWithoutContact } from "react-icons/md";
|
import { MdConnectWithoutContact } from "react-icons/md";
|
||||||
import { Menu, MenuButton, MenuItem, MenuItems } from "@headlessui/react";
|
import { Menu, MenuButton, MenuItem, MenuItems } from "@headlessui/react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router";
|
||||||
import { LuEllipsisVertical } from "react-icons/lu";
|
import { LuEllipsisVertical } from "react-icons/lu";
|
||||||
|
|
||||||
import Card from "@components/Card";
|
import Card from "@components/Card";
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import Container from "@/components/Container";
|
import Container from "@/components/Container";
|
||||||
|
|
|
||||||
|
|
@ -7,15 +7,14 @@ import MacroBar from "@/components/MacroBar";
|
||||||
import InfoBar from "@components/InfoBar";
|
import InfoBar from "@components/InfoBar";
|
||||||
import notifications from "@/notifications";
|
import notifications from "@/notifications";
|
||||||
import useKeyboard from "@/hooks/useKeyboard";
|
import useKeyboard from "@/hooks/useKeyboard";
|
||||||
import { useJsonRpc } from "@/hooks/useJsonRpc";
|
|
||||||
import { cx } from "@/cva.config";
|
import { cx } from "@/cva.config";
|
||||||
import { keys } from "@/keyboardMappings";
|
import { keys } from "@/keyboardMappings";
|
||||||
import {
|
import {
|
||||||
useMouseStore,
|
|
||||||
useRTCStore,
|
useRTCStore,
|
||||||
useSettingsStore,
|
useSettingsStore,
|
||||||
useVideoStore,
|
useVideoStore,
|
||||||
} from "@/hooks/stores";
|
} from "@/hooks/stores";
|
||||||
|
import useMouse from "@/hooks/useMouse";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
HDMIErrorOverlay,
|
HDMIErrorOverlay,
|
||||||
|
|
@ -31,10 +30,18 @@ export default function WebRTCVideo() {
|
||||||
const [isPlaying, setIsPlaying] = useState(false);
|
const [isPlaying, setIsPlaying] = useState(false);
|
||||||
const [isPointerLockActive, setIsPointerLockActive] = useState(false);
|
const [isPointerLockActive, setIsPointerLockActive] = useState(false);
|
||||||
const [isKeyboardLockActive, setIsKeyboardLockActive] = useState(false);
|
const [isKeyboardLockActive, setIsKeyboardLockActive] = useState(false);
|
||||||
|
|
||||||
|
const isPointerLockPossible = window.location.protocol === "https:" || window.location.hostname === "localhost";
|
||||||
|
|
||||||
// Store hooks
|
// Store hooks
|
||||||
const settings = useSettingsStore();
|
const settings = useSettingsStore();
|
||||||
const { handleKeyPress, resetKeyboardState } = useKeyboard();
|
const { handleKeyPress, resetKeyboardState } = useKeyboard();
|
||||||
const { setMousePosition, setMouseMove } = useMouseStore();
|
const {
|
||||||
|
getRelMouseMoveHandler,
|
||||||
|
getAbsMouseMoveHandler,
|
||||||
|
getMouseWheelHandler,
|
||||||
|
resetMousePosition,
|
||||||
|
} = useMouse();
|
||||||
const {
|
const {
|
||||||
setClientSize: setVideoClientSize,
|
setClientSize: setVideoClientSize,
|
||||||
setSize: setVideoSize,
|
setSize: setVideoSize,
|
||||||
|
|
@ -55,15 +62,9 @@ export default function WebRTCVideo() {
|
||||||
const hdmiError = ["no_lock", "no_signal", "out_of_range"].includes(hdmiState);
|
const hdmiError = ["no_lock", "no_signal", "out_of_range"].includes(hdmiState);
|
||||||
const isVideoLoading = !isPlaying;
|
const isVideoLoading = !isPlaying;
|
||||||
|
|
||||||
// Mouse wheel states
|
|
||||||
const [blockWheelEvent, setBlockWheelEvent] = useState(false);
|
|
||||||
|
|
||||||
// Misc states and hooks
|
|
||||||
const { send } = useJsonRpc();
|
|
||||||
|
|
||||||
// Video-related
|
// Video-related
|
||||||
const handleResize = useCallback(
|
const handleResize = useCallback(
|
||||||
( { width, height }: { width: number | undefined; height: number | undefined }) => {
|
({ width, height }: { width: number | undefined; height: number | undefined }) => {
|
||||||
if (!videoElm.current) return;
|
if (!videoElm.current) return;
|
||||||
// Do something with width and height, e.g.:
|
// Do something with width and height, e.g.:
|
||||||
setVideoClientSize(width || 0, height || 0);
|
setVideoClientSize(width || 0, height || 0);
|
||||||
|
|
@ -99,7 +100,6 @@ export default function WebRTCVideo() {
|
||||||
);
|
);
|
||||||
|
|
||||||
// Pointer lock and keyboard lock related
|
// Pointer lock and keyboard lock related
|
||||||
const isPointerLockPossible = window.location.protocol === "https:" || window.location.hostname === "localhost";
|
|
||||||
const isFullscreenEnabled = document.fullscreenEnabled;
|
const isFullscreenEnabled = document.fullscreenEnabled;
|
||||||
|
|
||||||
const checkNavigatorPermissions = useCallback(async (permissionName: string) => {
|
const checkNavigatorPermissions = useCallback(async (permissionName: string) => {
|
||||||
|
|
@ -211,129 +211,32 @@ export default function WebRTCVideo() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
document.addEventListener("fullscreenchange ", handleFullscreenChange);
|
document.addEventListener("fullscreenchange", handleFullscreenChange);
|
||||||
}, [releaseKeyboardLock]);
|
}, [releaseKeyboardLock]);
|
||||||
|
|
||||||
// Mouse-related
|
const absMouseMoveHandler = useMemo(
|
||||||
const calcDelta = (pos: number) => (Math.abs(pos) < 10 ? pos * 2 : pos);
|
() => getAbsMouseMoveHandler({
|
||||||
|
videoClientWidth,
|
||||||
const sendRelMouseMovement = useCallback(
|
videoClientHeight,
|
||||||
(x: number, y: number, buttons: number) => {
|
videoWidth,
|
||||||
if (settings.mouseMode !== "relative") return;
|
videoHeight,
|
||||||
// if we ignore the event, double-click will not work
|
}),
|
||||||
// if (x === 0 && y === 0 && buttons === 0) return;
|
[getAbsMouseMoveHandler, videoClientWidth, videoClientHeight, videoWidth, videoHeight],
|
||||||
send("relMouseReport", { dx: calcDelta(x), dy: calcDelta(y), buttons });
|
|
||||||
setMouseMove({ x, y, buttons });
|
|
||||||
},
|
|
||||||
[send, setMouseMove, settings.mouseMode],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const relMouseMoveHandler = useCallback(
|
const relMouseMoveHandler = useMemo(
|
||||||
(e: MouseEvent) => {
|
() => getRelMouseMoveHandler({
|
||||||
if (settings.mouseMode !== "relative") return;
|
isPointerLockActive,
|
||||||
if (isPointerLockActive === false && isPointerLockPossible) return;
|
isPointerLockPossible,
|
||||||
|
}),
|
||||||
// Send mouse movement
|
[getRelMouseMoveHandler, isPointerLockActive, isPointerLockPossible],
|
||||||
const { buttons } = e;
|
|
||||||
sendRelMouseMovement(e.movementX, e.movementY, buttons);
|
|
||||||
},
|
|
||||||
[isPointerLockActive, isPointerLockPossible, sendRelMouseMovement, settings.mouseMode],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const sendAbsMouseMovement = useCallback(
|
const mouseWheelHandler = useMemo(
|
||||||
(x: number, y: number, buttons: number) => {
|
() => getMouseWheelHandler(),
|
||||||
if (settings.mouseMode !== "absolute") return;
|
[getMouseWheelHandler],
|
||||||
send("absMouseReport", { x, y, buttons });
|
|
||||||
// We set that for the debug info bar
|
|
||||||
setMousePosition(x, y);
|
|
||||||
},
|
|
||||||
[send, setMousePosition, settings.mouseMode],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const absMouseMoveHandler = useCallback(
|
|
||||||
(e: MouseEvent) => {
|
|
||||||
if (!videoClientWidth || !videoClientHeight) return;
|
|
||||||
if (settings.mouseMode !== "absolute") return;
|
|
||||||
|
|
||||||
// Get the aspect ratios of the video element and the video stream
|
|
||||||
const videoElementAspectRatio = videoClientWidth / videoClientHeight;
|
|
||||||
const videoStreamAspectRatio = videoWidth / videoHeight;
|
|
||||||
|
|
||||||
// Calculate the effective video display area
|
|
||||||
let effectiveWidth = videoClientWidth;
|
|
||||||
let effectiveHeight = videoClientHeight;
|
|
||||||
let offsetX = 0;
|
|
||||||
let offsetY = 0;
|
|
||||||
|
|
||||||
if (videoElementAspectRatio > videoStreamAspectRatio) {
|
|
||||||
// Pillarboxing: black bars on the left and right
|
|
||||||
effectiveWidth = videoClientHeight * videoStreamAspectRatio;
|
|
||||||
offsetX = (videoClientWidth - effectiveWidth) / 2;
|
|
||||||
} else if (videoElementAspectRatio < videoStreamAspectRatio) {
|
|
||||||
// Letterboxing: black bars on the top and bottom
|
|
||||||
effectiveHeight = videoClientWidth / videoStreamAspectRatio;
|
|
||||||
offsetY = (videoClientHeight - effectiveHeight) / 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clamp mouse position within the effective video boundaries
|
|
||||||
const clampedX = Math.min(Math.max(offsetX, e.offsetX), offsetX + effectiveWidth);
|
|
||||||
const clampedY = Math.min(Math.max(offsetY, e.offsetY), offsetY + effectiveHeight);
|
|
||||||
|
|
||||||
// Map clamped mouse position to the video stream's coordinate system
|
|
||||||
const relativeX = (clampedX - offsetX) / effectiveWidth;
|
|
||||||
const relativeY = (clampedY - offsetY) / effectiveHeight;
|
|
||||||
|
|
||||||
// Convert to HID absolute coordinate system (0-32767 range)
|
|
||||||
const x = Math.round(relativeX * 32767);
|
|
||||||
const y = Math.round(relativeY * 32767);
|
|
||||||
|
|
||||||
// Send mouse movement
|
|
||||||
const { buttons } = e;
|
|
||||||
sendAbsMouseMovement(x, y, buttons);
|
|
||||||
},
|
|
||||||
[settings.mouseMode, videoClientWidth, videoClientHeight, videoWidth, videoHeight, sendAbsMouseMovement],
|
|
||||||
);
|
|
||||||
|
|
||||||
const mouseWheelHandler = useCallback(
|
|
||||||
(e: WheelEvent) => {
|
|
||||||
|
|
||||||
if (settings.scrollThrottling && blockWheelEvent) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine if the wheel event is an accel scroll value
|
|
||||||
const isAccel = Math.abs(e.deltaY) >= 100;
|
|
||||||
|
|
||||||
// Calculate the accel scroll value
|
|
||||||
const accelScrollValue = e.deltaY / 100;
|
|
||||||
|
|
||||||
// Calculate the no accel scroll value
|
|
||||||
const noAccelScrollValue = Math.sign(e.deltaY);
|
|
||||||
|
|
||||||
// Get scroll value
|
|
||||||
const scrollValue = isAccel ? accelScrollValue : noAccelScrollValue;
|
|
||||||
|
|
||||||
// Apply clamping (i.e. min and max mouse wheel hardware value)
|
|
||||||
const clampedScrollValue = Math.max(-127, Math.min(127, scrollValue));
|
|
||||||
|
|
||||||
// Invert the clamped scroll value to match expected behavior
|
|
||||||
const invertedScrollValue = -clampedScrollValue;
|
|
||||||
|
|
||||||
send("wheelReport", { wheelY: invertedScrollValue });
|
|
||||||
|
|
||||||
// Apply blocking delay based of throttling settings
|
|
||||||
if (settings.scrollThrottling && !blockWheelEvent) {
|
|
||||||
setBlockWheelEvent(true);
|
|
||||||
setTimeout(() => setBlockWheelEvent(false), settings.scrollThrottling);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[send, blockWheelEvent, settings],
|
|
||||||
);
|
|
||||||
|
|
||||||
const resetMousePosition = useCallback(() => {
|
|
||||||
sendAbsMouseMovement(0, 0, 0);
|
|
||||||
}, [sendAbsMouseMovement]);
|
|
||||||
|
|
||||||
const keyDownHandler = useCallback(
|
const keyDownHandler = useCallback(
|
||||||
(e: KeyboardEvent) => {
|
(e: KeyboardEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
@ -488,14 +391,16 @@ export default function WebRTCVideo() {
|
||||||
function setMouseModeEventListeners() {
|
function setMouseModeEventListeners() {
|
||||||
const videoElmRefValue = videoElm.current;
|
const videoElmRefValue = videoElm.current;
|
||||||
if (!videoElmRefValue) return;
|
if (!videoElmRefValue) return;
|
||||||
|
|
||||||
const isRelativeMouseMode = (settings.mouseMode === "relative");
|
const isRelativeMouseMode = (settings.mouseMode === "relative");
|
||||||
|
const mouseHandler = isRelativeMouseMode ? relMouseMoveHandler : absMouseMoveHandler;
|
||||||
|
|
||||||
const abortController = new AbortController();
|
const abortController = new AbortController();
|
||||||
const signal = abortController.signal;
|
const signal = abortController.signal;
|
||||||
|
|
||||||
videoElmRefValue.addEventListener("mousemove", isRelativeMouseMode ? relMouseMoveHandler : absMouseMoveHandler, { signal });
|
videoElmRefValue.addEventListener("mousemove", mouseHandler, { signal });
|
||||||
videoElmRefValue.addEventListener("pointerdown", isRelativeMouseMode ? relMouseMoveHandler : absMouseMoveHandler, { signal });
|
videoElmRefValue.addEventListener("pointerdown", mouseHandler, { signal });
|
||||||
videoElmRefValue.addEventListener("pointerup", isRelativeMouseMode ? relMouseMoveHandler : absMouseMoveHandler, { signal });
|
videoElmRefValue.addEventListener("pointerup", mouseHandler, { signal });
|
||||||
videoElmRefValue.addEventListener("wheel", mouseWheelHandler, {
|
videoElmRefValue.addEventListener("wheel", mouseWheelHandler, {
|
||||||
signal,
|
signal,
|
||||||
passive: true,
|
passive: true,
|
||||||
|
|
@ -523,7 +428,16 @@ export default function WebRTCVideo() {
|
||||||
abortController.abort();
|
abortController.abort();
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
[absMouseMoveHandler, isPointerLockActive, isPointerLockPossible, mouseWheelHandler, relMouseMoveHandler, requestPointerLock, resetMousePosition, settings.mouseMode],
|
[
|
||||||
|
isPointerLockActive,
|
||||||
|
isPointerLockPossible,
|
||||||
|
requestPointerLock,
|
||||||
|
absMouseMoveHandler,
|
||||||
|
relMouseMoveHandler,
|
||||||
|
mouseWheelHandler,
|
||||||
|
resetMousePosition,
|
||||||
|
settings.mouseMode,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import {
|
||||||
LuRadioReceiver,
|
LuRadioReceiver,
|
||||||
} from "react-icons/lu";
|
} from "react-icons/lu";
|
||||||
import { useClose } from "@headlessui/react";
|
import { useClose } from "@headlessui/react";
|
||||||
import { useLocation } from "react-router-dom";
|
import { useLocation } from "react-router";
|
||||||
|
|
||||||
import { Button } from "@components/Button";
|
import { Button } from "@components/Button";
|
||||||
import Card, { GridCard } from "@components/Card";
|
import Card, { GridCard } from "@components/Card";
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,302 @@
|
||||||
|
import { KeyboardLedState, KeysDownState } from "./stores";
|
||||||
|
|
||||||
|
export const HID_RPC_MESSAGE_TYPES = {
|
||||||
|
Handshake: 0x01,
|
||||||
|
KeyboardReport: 0x02,
|
||||||
|
PointerReport: 0x03,
|
||||||
|
WheelReport: 0x04,
|
||||||
|
KeypressReport: 0x05,
|
||||||
|
MouseReport: 0x06,
|
||||||
|
KeyboardLedState: 0x32,
|
||||||
|
KeysDownState: 0x33,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type HidRpcMessageType = typeof HID_RPC_MESSAGE_TYPES[keyof typeof HID_RPC_MESSAGE_TYPES];
|
||||||
|
|
||||||
|
export const HID_RPC_VERSION = 0x01;
|
||||||
|
|
||||||
|
const withinUint8Range = (value: number) => {
|
||||||
|
return value >= 0 && value <= 255;
|
||||||
|
};
|
||||||
|
|
||||||
|
const fromInt32toUint8 = (n: number) => {
|
||||||
|
if (n !== n >> 0) {
|
||||||
|
throw new Error(`Number ${n} is not within the int32 range`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Uint8Array([
|
||||||
|
(n >> 24) & 0xFF,
|
||||||
|
(n >> 16) & 0xFF,
|
||||||
|
(n >> 8) & 0xFF,
|
||||||
|
(n >> 0) & 0xFF,
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const fromInt8ToUint8 = (n: number) => {
|
||||||
|
if (n < -128 || n > 127) {
|
||||||
|
throw new Error(`Number ${n} is not within the int8 range`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (n >> 0) & 0xFF;
|
||||||
|
};
|
||||||
|
|
||||||
|
const keyboardLedStateMasks = {
|
||||||
|
num_lock: 1 << 0,
|
||||||
|
caps_lock: 1 << 1,
|
||||||
|
scroll_lock: 1 << 2,
|
||||||
|
compose: 1 << 3,
|
||||||
|
kana: 1 << 4,
|
||||||
|
shift: 1 << 6,
|
||||||
|
}
|
||||||
|
|
||||||
|
export class RpcMessage {
|
||||||
|
messageType: HidRpcMessageType;
|
||||||
|
|
||||||
|
constructor(messageType: HidRpcMessageType) {
|
||||||
|
this.messageType = messageType;
|
||||||
|
}
|
||||||
|
|
||||||
|
marshal(): Uint8Array {
|
||||||
|
throw new Error("Not implemented");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static unmarshal(_data: Uint8Array): RpcMessage | undefined {
|
||||||
|
throw new Error("Not implemented");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class HandshakeMessage extends RpcMessage {
|
||||||
|
version: number;
|
||||||
|
|
||||||
|
constructor(version: number) {
|
||||||
|
super(HID_RPC_MESSAGE_TYPES.Handshake);
|
||||||
|
this.version = version;
|
||||||
|
}
|
||||||
|
|
||||||
|
marshal(): Uint8Array {
|
||||||
|
return new Uint8Array([this.messageType, this.version]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static unmarshal(data: Uint8Array): HandshakeMessage | undefined {
|
||||||
|
if (data.length < 1) {
|
||||||
|
throw new Error(`Invalid handshake message length: ${data.length}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new HandshakeMessage(data[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class KeypressReportMessage extends RpcMessage {
|
||||||
|
private _key = 0;
|
||||||
|
private _press = false;
|
||||||
|
|
||||||
|
get key(): number {
|
||||||
|
return this._key;
|
||||||
|
}
|
||||||
|
|
||||||
|
set key(value: number) {
|
||||||
|
if (!withinUint8Range(value)) {
|
||||||
|
throw new Error(`Key ${value} is not within the uint8 range`);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._key = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
get press(): boolean {
|
||||||
|
return this._press;
|
||||||
|
}
|
||||||
|
|
||||||
|
set press(value: boolean) {
|
||||||
|
this._press = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(key: number, press: boolean) {
|
||||||
|
super(HID_RPC_MESSAGE_TYPES.KeypressReport);
|
||||||
|
this.key = key;
|
||||||
|
this.press = press;
|
||||||
|
}
|
||||||
|
|
||||||
|
marshal(): Uint8Array {
|
||||||
|
return new Uint8Array([
|
||||||
|
this.messageType,
|
||||||
|
this.key,
|
||||||
|
this.press ? 1 : 0,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static unmarshal(data: Uint8Array): KeypressReportMessage | undefined {
|
||||||
|
if (data.length < 1) {
|
||||||
|
throw new Error(`Invalid keypress report message length: ${data.length}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new KeypressReportMessage(data[0], data[1] === 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class KeyboardReportMessage extends RpcMessage {
|
||||||
|
private _keys: number[] = [];
|
||||||
|
private _modifier = 0;
|
||||||
|
|
||||||
|
get keys(): number[] {
|
||||||
|
return this._keys;
|
||||||
|
}
|
||||||
|
|
||||||
|
set keys(value: number[]) {
|
||||||
|
value.forEach((k) => {
|
||||||
|
if (!withinUint8Range(k)) {
|
||||||
|
throw new Error(`Key ${k} is not within the uint8 range`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this._keys = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
get modifier(): number {
|
||||||
|
return this._modifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
set modifier(value: number) {
|
||||||
|
if (!withinUint8Range(value)) {
|
||||||
|
throw new Error(`Modifier ${value} is not within the uint8 range`);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._modifier = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(keys: number[], modifier: number) {
|
||||||
|
super(HID_RPC_MESSAGE_TYPES.KeyboardReport);
|
||||||
|
this.keys = keys;
|
||||||
|
this.modifier = modifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
marshal(): Uint8Array {
|
||||||
|
return new Uint8Array([
|
||||||
|
this.messageType,
|
||||||
|
this.modifier,
|
||||||
|
...this.keys,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static unmarshal(data: Uint8Array): KeyboardReportMessage | undefined {
|
||||||
|
if (data.length < 1) {
|
||||||
|
throw new Error(`Invalid keyboard report message length: ${data.length}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new KeyboardReportMessage(Array.from(data.slice(1)), data[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class KeyboardLedStateMessage extends RpcMessage {
|
||||||
|
keyboardLedState: KeyboardLedState;
|
||||||
|
|
||||||
|
constructor(keyboardLedState: KeyboardLedState) {
|
||||||
|
super(HID_RPC_MESSAGE_TYPES.KeyboardLedState);
|
||||||
|
this.keyboardLedState = keyboardLedState;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static unmarshal(data: Uint8Array): KeyboardLedStateMessage | undefined {
|
||||||
|
if (data.length < 1) {
|
||||||
|
throw new Error(`Invalid keyboard led state message length: ${data.length}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const s = data[0];
|
||||||
|
|
||||||
|
const state = {
|
||||||
|
num_lock: (s & keyboardLedStateMasks.num_lock) !== 0,
|
||||||
|
caps_lock: (s & keyboardLedStateMasks.caps_lock) !== 0,
|
||||||
|
scroll_lock: (s & keyboardLedStateMasks.scroll_lock) !== 0,
|
||||||
|
compose: (s & keyboardLedStateMasks.compose) !== 0,
|
||||||
|
kana: (s & keyboardLedStateMasks.kana) !== 0,
|
||||||
|
shift: (s & keyboardLedStateMasks.shift) !== 0,
|
||||||
|
} as KeyboardLedState;
|
||||||
|
|
||||||
|
return new KeyboardLedStateMessage(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class KeysDownStateMessage extends RpcMessage {
|
||||||
|
keysDownState: KeysDownState;
|
||||||
|
|
||||||
|
constructor(keysDownState: KeysDownState) {
|
||||||
|
super(HID_RPC_MESSAGE_TYPES.KeysDownState);
|
||||||
|
this.keysDownState = keysDownState;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static unmarshal(data: Uint8Array): KeysDownStateMessage | undefined {
|
||||||
|
if (data.length < 1) {
|
||||||
|
throw new Error(`Invalid keys down state message length: ${data.length}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new KeysDownStateMessage({
|
||||||
|
modifier: data[0],
|
||||||
|
keys: Array.from(data.slice(1))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PointerReportMessage extends RpcMessage {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
buttons: number;
|
||||||
|
|
||||||
|
constructor(x: number, y: number, buttons: number) {
|
||||||
|
super(HID_RPC_MESSAGE_TYPES.PointerReport);
|
||||||
|
this.x = x;
|
||||||
|
this.y = y;
|
||||||
|
this.buttons = buttons;
|
||||||
|
}
|
||||||
|
|
||||||
|
marshal(): Uint8Array {
|
||||||
|
return new Uint8Array([
|
||||||
|
this.messageType,
|
||||||
|
...fromInt32toUint8(this.x),
|
||||||
|
...fromInt32toUint8(this.y),
|
||||||
|
this.buttons,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MouseReportMessage extends RpcMessage {
|
||||||
|
dx: number;
|
||||||
|
dy: number;
|
||||||
|
buttons: number;
|
||||||
|
|
||||||
|
constructor(dx: number, dy: number, buttons: number) {
|
||||||
|
super(HID_RPC_MESSAGE_TYPES.MouseReport);
|
||||||
|
this.dx = dx;
|
||||||
|
this.dy = dy;
|
||||||
|
this.buttons = buttons;
|
||||||
|
}
|
||||||
|
|
||||||
|
marshal(): Uint8Array {
|
||||||
|
return new Uint8Array([
|
||||||
|
this.messageType,
|
||||||
|
fromInt8ToUint8(this.dx),
|
||||||
|
fromInt8ToUint8(this.dy),
|
||||||
|
this.buttons,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const messageRegistry = {
|
||||||
|
[HID_RPC_MESSAGE_TYPES.Handshake]: HandshakeMessage,
|
||||||
|
[HID_RPC_MESSAGE_TYPES.KeysDownState]: KeysDownStateMessage,
|
||||||
|
[HID_RPC_MESSAGE_TYPES.KeyboardLedState]: KeyboardLedStateMessage,
|
||||||
|
[HID_RPC_MESSAGE_TYPES.KeyboardReport]: KeyboardReportMessage,
|
||||||
|
[HID_RPC_MESSAGE_TYPES.KeypressReport]: KeypressReportMessage,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const unmarshalHidRpcMessage = (data: Uint8Array): RpcMessage | undefined => {
|
||||||
|
if (data.length < 1) {
|
||||||
|
throw new Error(`Invalid HID RPC message length: ${data.length}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = data.slice(1);
|
||||||
|
|
||||||
|
const messageType = data[0];
|
||||||
|
if (!(messageType in messageRegistry)) {
|
||||||
|
throw new Error(`Unknown HID RPC message type: ${messageType}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return messageRegistry[messageType].unmarshal(payload);
|
||||||
|
};
|
||||||
|
|
@ -105,6 +105,12 @@ export interface RTCState {
|
||||||
setRpcDataChannel: (channel: RTCDataChannel) => void;
|
setRpcDataChannel: (channel: RTCDataChannel) => void;
|
||||||
rpcDataChannel: RTCDataChannel | null;
|
rpcDataChannel: RTCDataChannel | null;
|
||||||
|
|
||||||
|
rpcHidProtocolVersion: number | null;
|
||||||
|
setRpcHidProtocolVersion: (version: number) => void;
|
||||||
|
|
||||||
|
rpcHidChannel: RTCDataChannel | null;
|
||||||
|
setRpcHidChannel: (channel: RTCDataChannel) => void;
|
||||||
|
|
||||||
peerConnectionState: RTCPeerConnectionState | null;
|
peerConnectionState: RTCPeerConnectionState | null;
|
||||||
setPeerConnectionState: (state: RTCPeerConnectionState) => void;
|
setPeerConnectionState: (state: RTCPeerConnectionState) => void;
|
||||||
|
|
||||||
|
|
@ -151,6 +157,12 @@ export const useRTCStore = create<RTCState>(set => ({
|
||||||
rpcDataChannel: null,
|
rpcDataChannel: null,
|
||||||
setRpcDataChannel: (channel: RTCDataChannel) => set({ rpcDataChannel: channel }),
|
setRpcDataChannel: (channel: RTCDataChannel) => set({ rpcDataChannel: channel }),
|
||||||
|
|
||||||
|
rpcHidProtocolVersion: null,
|
||||||
|
setRpcHidProtocolVersion: (version: number) => set({ rpcHidProtocolVersion: version }),
|
||||||
|
|
||||||
|
rpcHidChannel: null,
|
||||||
|
setRpcHidChannel: (channel: RTCDataChannel) => set({ rpcHidChannel: channel }),
|
||||||
|
|
||||||
transceiver: null,
|
transceiver: null,
|
||||||
setTransceiver: (transceiver: RTCRtpTransceiver) => set({ transceiver }),
|
setTransceiver: (transceiver: RTCRtpTransceiver) => set({ transceiver }),
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { useNavigate, useParams, NavigateOptions } from "react-router-dom";
|
import { useNavigate, useParams } from "react-router";
|
||||||
|
import type { NavigateOptions } from "react-router";
|
||||||
import { useCallback, useMemo } from "react";
|
import { useCallback, useMemo } from "react";
|
||||||
|
|
||||||
import { isOnDevice } from "../main";
|
import { isOnDevice } from "../main";
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,147 @@
|
||||||
|
import { useCallback, useEffect, useMemo } from "react";
|
||||||
|
|
||||||
|
import { useRTCStore } from "@/hooks/stores";
|
||||||
|
|
||||||
|
import {
|
||||||
|
HID_RPC_VERSION,
|
||||||
|
HandshakeMessage,
|
||||||
|
KeyboardReportMessage,
|
||||||
|
KeypressReportMessage,
|
||||||
|
MouseReportMessage,
|
||||||
|
PointerReportMessage,
|
||||||
|
RpcMessage,
|
||||||
|
unmarshalHidRpcMessage,
|
||||||
|
} from "./hidRpc";
|
||||||
|
|
||||||
|
export function useHidRpc(onHidRpcMessage?: (payload: RpcMessage) => void) {
|
||||||
|
const { rpcHidChannel, setRpcHidProtocolVersion, rpcHidProtocolVersion } = useRTCStore();
|
||||||
|
const rpcHidReady = useMemo(() => {
|
||||||
|
return rpcHidChannel?.readyState === "open" && rpcHidProtocolVersion !== null;
|
||||||
|
}, [rpcHidChannel, rpcHidProtocolVersion]);
|
||||||
|
|
||||||
|
const rpcHidStatus = useMemo(() => {
|
||||||
|
if (!rpcHidChannel) return "N/A";
|
||||||
|
if (rpcHidChannel.readyState !== "open") return rpcHidChannel.readyState;
|
||||||
|
if (!rpcHidProtocolVersion) return "handshaking";
|
||||||
|
return `ready (v${rpcHidProtocolVersion})`;
|
||||||
|
}, [rpcHidChannel, rpcHidProtocolVersion]);
|
||||||
|
|
||||||
|
const sendMessage = useCallback((message: RpcMessage, ignoreHandshakeState = false) => {
|
||||||
|
if (rpcHidChannel?.readyState !== "open") return;
|
||||||
|
if (!rpcHidReady && !ignoreHandshakeState) return;
|
||||||
|
|
||||||
|
let data: Uint8Array | undefined;
|
||||||
|
try {
|
||||||
|
data = message.marshal();
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to send HID RPC message", e);
|
||||||
|
}
|
||||||
|
if (!data) return;
|
||||||
|
|
||||||
|
rpcHidChannel?.send(data as unknown as ArrayBuffer);
|
||||||
|
}, [rpcHidChannel, rpcHidReady]);
|
||||||
|
|
||||||
|
const reportKeyboardEvent = useCallback(
|
||||||
|
(keys: number[], modifier: number) => {
|
||||||
|
sendMessage(new KeyboardReportMessage(keys, modifier));
|
||||||
|
}, [sendMessage],
|
||||||
|
);
|
||||||
|
|
||||||
|
const reportKeypressEvent = useCallback(
|
||||||
|
(key: number, press: boolean) => {
|
||||||
|
sendMessage(new KeypressReportMessage(key, press));
|
||||||
|
},
|
||||||
|
[sendMessage],
|
||||||
|
);
|
||||||
|
|
||||||
|
const reportAbsMouseEvent = useCallback(
|
||||||
|
(x: number, y: number, buttons: number) => {
|
||||||
|
sendMessage(new PointerReportMessage(x, y, buttons));
|
||||||
|
},
|
||||||
|
[sendMessage],
|
||||||
|
);
|
||||||
|
|
||||||
|
const reportRelMouseEvent = useCallback(
|
||||||
|
(dx: number, dy: number, buttons: number) => {
|
||||||
|
sendMessage(new MouseReportMessage(dx, dy, buttons));
|
||||||
|
},
|
||||||
|
[sendMessage],
|
||||||
|
);
|
||||||
|
|
||||||
|
const sendHandshake = useCallback(() => {
|
||||||
|
if (rpcHidProtocolVersion) return;
|
||||||
|
if (!rpcHidChannel) return;
|
||||||
|
|
||||||
|
sendMessage(new HandshakeMessage(HID_RPC_VERSION), true);
|
||||||
|
}, [rpcHidChannel, rpcHidProtocolVersion, sendMessage]);
|
||||||
|
|
||||||
|
const handleHandshake = useCallback((message: HandshakeMessage) => {
|
||||||
|
if (!message.version) {
|
||||||
|
console.error("Received handshake message without version", message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.version < HID_RPC_VERSION) {
|
||||||
|
console.error("Server is using an older HID RPC version than the client", message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setRpcHidProtocolVersion(message.version);
|
||||||
|
}, [setRpcHidProtocolVersion]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!rpcHidChannel) return;
|
||||||
|
|
||||||
|
// send handshake message
|
||||||
|
sendHandshake();
|
||||||
|
|
||||||
|
const messageHandler = (e: MessageEvent) => {
|
||||||
|
if (typeof e.data === "string") {
|
||||||
|
console.warn("Received string data in HID RPC message handler", e.data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = unmarshalHidRpcMessage(new Uint8Array(e.data));
|
||||||
|
if (!message) {
|
||||||
|
console.warn("Received invalid HID RPC message", e.data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.debug("Received HID RPC message", message);
|
||||||
|
switch (message.constructor) {
|
||||||
|
case HandshakeMessage:
|
||||||
|
handleHandshake(message as HandshakeMessage);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// not all events are handled here, the rest are handled by the onHidRpcMessage callback
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
onHidRpcMessage?.(message);
|
||||||
|
};
|
||||||
|
|
||||||
|
rpcHidChannel.addEventListener("message", messageHandler);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
rpcHidChannel.removeEventListener("message", messageHandler);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
[
|
||||||
|
rpcHidChannel,
|
||||||
|
onHidRpcMessage,
|
||||||
|
setRpcHidProtocolVersion,
|
||||||
|
sendHandshake,
|
||||||
|
handleHandshake,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
reportKeyboardEvent,
|
||||||
|
reportKeypressEvent,
|
||||||
|
reportAbsMouseEvent,
|
||||||
|
reportRelMouseEvent,
|
||||||
|
rpcHidProtocolVersion,
|
||||||
|
rpcHidReady,
|
||||||
|
rpcHidStatus,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -1,13 +1,15 @@
|
||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
|
|
||||||
import { KeysDownState, useHidStore, useRTCStore, hidKeyBufferSize, hidErrorRollOver } from "@/hooks/stores";
|
import { hidErrorRollOver, hidKeyBufferSize, KeysDownState, useHidStore, useRTCStore } from "@/hooks/stores";
|
||||||
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
|
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
|
||||||
|
import { useHidRpc } from "@/hooks/useHidRpc";
|
||||||
|
import { KeyboardLedStateMessage, KeysDownStateMessage } from "@/hooks/hidRpc";
|
||||||
import { hidKeyToModifierMask, keys, modifiers } from "@/keyboardMappings";
|
import { hidKeyToModifierMask, keys, modifiers } from "@/keyboardMappings";
|
||||||
|
|
||||||
export default function useKeyboard() {
|
export default function useKeyboard() {
|
||||||
const { send } = useJsonRpc();
|
const { send } = useJsonRpc();
|
||||||
const { rpcDataChannel } = useRTCStore();
|
const { rpcDataChannel } = useRTCStore();
|
||||||
const { keysDownState, setKeysDownState } = useHidStore();
|
const { keysDownState, setKeysDownState, setKeyboardLedState } = useHidStore();
|
||||||
|
|
||||||
// INTRODUCTION: The earlier version of the JetKVM device shipped with all keyboard state
|
// INTRODUCTION: The earlier version of the JetKVM device shipped with all keyboard state
|
||||||
// being tracked on the browser/client-side. When adding the keyPressReport API to the
|
// being tracked on the browser/client-side. When adding the keyPressReport API to the
|
||||||
|
|
@ -19,7 +21,28 @@ export default function useKeyboard() {
|
||||||
// dynamically set when the device responds to the first key press event or reports its
|
// dynamically set when the device responds to the first key press event or reports its
|
||||||
// keysDownState when queried since the keyPressReport was introduced together with the
|
// keysDownState when queried since the keyPressReport was introduced together with the
|
||||||
// getKeysDownState API.
|
// getKeysDownState API.
|
||||||
const { keyPressReportApiAvailable, setkeyPressReportApiAvailable} = useHidStore();
|
const { keyPressReportApiAvailable, setkeyPressReportApiAvailable } = useHidStore();
|
||||||
|
const enableKeyPressReport = useCallback((reason: string) => {
|
||||||
|
if (keyPressReportApiAvailable) return;
|
||||||
|
console.debug(`Enable keyPressReport API because ${reason}`);
|
||||||
|
setkeyPressReportApiAvailable(true);
|
||||||
|
}, [setkeyPressReportApiAvailable, keyPressReportApiAvailable]);
|
||||||
|
|
||||||
|
// HidRPC is a binary format for exchanging keyboard and mouse events
|
||||||
|
const { reportKeyboardEvent, reportKeypressEvent, rpcHidReady } = useHidRpc((message) => {
|
||||||
|
switch (message.constructor) {
|
||||||
|
case KeysDownStateMessage:
|
||||||
|
setKeysDownState((message as KeysDownStateMessage).keysDownState);
|
||||||
|
enableKeyPressReport("HidRPC:KeysDownStateMessage received");
|
||||||
|
break;
|
||||||
|
case KeyboardLedStateMessage:
|
||||||
|
setKeyboardLedState((message as KeyboardLedStateMessage).keyboardLedState);
|
||||||
|
enableKeyPressReport("HidRPC:KeyboardLedStateMessage received");
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// sendKeyboardEvent is used to send the full keyboard state to the device for macro handling
|
// sendKeyboardEvent is used to send the full keyboard state to the device for macro handling
|
||||||
// and resetting keyboard state. It sends the keys currently pressed and the modifier state.
|
// and resetting keyboard state. It sends the keys currently pressed and the modifier state.
|
||||||
|
|
@ -27,30 +50,30 @@ export default function useKeyboard() {
|
||||||
// or just accept the state if it does not support (returning no result)
|
// or just accept the state if it does not support (returning no result)
|
||||||
const sendKeyboardEvent = useCallback(
|
const sendKeyboardEvent = useCallback(
|
||||||
async (state: KeysDownState) => {
|
async (state: KeysDownState) => {
|
||||||
if (rpcDataChannel?.readyState !== "open") return;
|
if (rpcDataChannel?.readyState !== "open" && !rpcHidReady) return;
|
||||||
|
|
||||||
console.debug(`Send keyboardReport keys: ${state.keys}, modifier: ${state.modifier}`);
|
console.debug(`Send keyboardReport keys: ${state.keys}, modifier: ${state.modifier}`);
|
||||||
|
|
||||||
|
if (rpcHidReady) {
|
||||||
|
console.debug("Sending keyboard report via HidRPC");
|
||||||
|
reportKeyboardEvent(state.keys, state.modifier);
|
||||||
|
enableKeyPressReport("HidRPC:KeyboardReport received");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
send("keyboardReport", { keys: state.keys, modifier: state.modifier }, (resp: JsonRpcResponse) => {
|
send("keyboardReport", { keys: state.keys, modifier: state.modifier }, (resp: JsonRpcResponse) => {
|
||||||
if ("error" in resp) {
|
if ("error" in resp) {
|
||||||
console.error(`Failed to send keyboard report ${state}`, resp.error);
|
console.error(`Failed to send keyboard report ${state}`, resp.error);
|
||||||
} else {
|
|
||||||
// If the device supports keyPressReport API, it will (also) return the keysDownState when we send
|
|
||||||
// the keyboardReport
|
|
||||||
const keysDownState = resp.result as KeysDownState;
|
|
||||||
|
|
||||||
if (keysDownState) {
|
|
||||||
setKeysDownState(keysDownState); // treat the response as the canonical state
|
|
||||||
setkeyPressReportApiAvailable(true); // if they returned a keysDownState, we ALSO know they also support keyPressReport
|
|
||||||
} else {
|
|
||||||
// older devices versions do not return the keyDownState
|
|
||||||
// so we just pretend they accepted what we sent
|
|
||||||
setKeysDownState(state);
|
|
||||||
setkeyPressReportApiAvailable(false); // we ALSO know they do not support keyPressReport
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[rpcDataChannel?.readyState, send, setKeysDownState, setkeyPressReportApiAvailable],
|
[
|
||||||
|
rpcDataChannel?.readyState,
|
||||||
|
rpcHidReady,
|
||||||
|
send,
|
||||||
|
reportKeyboardEvent,
|
||||||
|
enableKeyPressReport,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
// sendKeypressEvent is used to send a single key press/release event to the device.
|
// sendKeypressEvent is used to send a single key press/release event to the device.
|
||||||
|
|
@ -61,29 +84,16 @@ export default function useKeyboard() {
|
||||||
// in client/browser-side code using simulateDeviceSideKeyHandlingForLegacyDevices.
|
// in client/browser-side code using simulateDeviceSideKeyHandlingForLegacyDevices.
|
||||||
const sendKeypressEvent = useCallback(
|
const sendKeypressEvent = useCallback(
|
||||||
async (key: number, press: boolean) => {
|
async (key: number, press: boolean) => {
|
||||||
if (rpcDataChannel?.readyState !== "open") return;
|
|
||||||
|
|
||||||
console.debug(`Send keypressEvent key: ${key}, press: ${press}`);
|
console.debug(`Send keypressEvent key: ${key}, press: ${press}`);
|
||||||
send("keypressReport", { key, press }, (resp: JsonRpcResponse) => {
|
|
||||||
if ("error" in resp) {
|
|
||||||
// -32601 means the method is not supported because the device is running an older version
|
|
||||||
if (resp.error.code === -32601) {
|
|
||||||
console.error("Legacy device does not support keypressReport API, switching to local key down state handling", resp.error);
|
|
||||||
setkeyPressReportApiAvailable(false);
|
|
||||||
} else {
|
|
||||||
console.error(`Failed to send key ${key} press: ${press}`, resp.error);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const keysDownState = resp.result as KeysDownState;
|
|
||||||
|
|
||||||
if (keysDownState) {
|
if (!rpcHidReady) return;
|
||||||
setKeysDownState(keysDownState);
|
|
||||||
// we don't need to set keyPressReportApiAvailable here, because it's already true or we never landed here
|
reportKeypressEvent(key, press);
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
[rpcDataChannel?.readyState, send, setkeyPressReportApiAvailable, setKeysDownState],
|
[
|
||||||
|
rpcHidReady,
|
||||||
|
reportKeypressEvent,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
// resetKeyboardState is used to reset the keyboard state to no keys pressed and no modifiers.
|
// resetKeyboardState is used to reset the keyboard state to no keys pressed and no modifiers.
|
||||||
|
|
@ -135,9 +145,15 @@ export default function useKeyboard() {
|
||||||
// It then sends the full keyboard state to the device.
|
// It then sends the full keyboard state to the device.
|
||||||
const handleKeyPress = useCallback(
|
const handleKeyPress = useCallback(
|
||||||
async (key: number, press: boolean) => {
|
async (key: number, press: boolean) => {
|
||||||
if (rpcDataChannel?.readyState !== "open") return;
|
if (rpcDataChannel?.readyState !== "open" && !rpcHidReady) return;
|
||||||
if ((key || 0) === 0) return; // ignore zero key presses (they are bad mappings)
|
if ((key || 0) === 0) return; // ignore zero key presses (they are bad mappings)
|
||||||
|
|
||||||
|
if (rpcHidReady) {
|
||||||
|
console.debug("Sending keypress event via HidRPC");
|
||||||
|
reportKeypressEvent(key, press);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (keyPressReportApiAvailable) {
|
if (keyPressReportApiAvailable) {
|
||||||
// if the keyPress api is available, we can just send the key press event
|
// if the keyPress api is available, we can just send the key press event
|
||||||
sendKeypressEvent(key, press);
|
sendKeypressEvent(key, press);
|
||||||
|
|
@ -152,7 +168,16 @@ export default function useKeyboard() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[keyPressReportApiAvailable, keysDownState, resetKeyboardState, rpcDataChannel?.readyState, sendKeyboardEvent, sendKeypressEvent],
|
[
|
||||||
|
keyPressReportApiAvailable,
|
||||||
|
keysDownState,
|
||||||
|
resetKeyboardState,
|
||||||
|
rpcDataChannel?.readyState,
|
||||||
|
rpcHidReady,
|
||||||
|
sendKeyboardEvent,
|
||||||
|
sendKeypressEvent,
|
||||||
|
reportKeypressEvent,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
// IMPORTANT: See the keyPressReportApiAvailable comment above for the reason this exists
|
// IMPORTANT: See the keyPressReportApiAvailable comment above for the reason this exists
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,178 @@
|
||||||
|
import { useCallback, useState } from "react";
|
||||||
|
|
||||||
|
import { useJsonRpc } from "./useJsonRpc";
|
||||||
|
import { useHidRpc } from "./useHidRpc";
|
||||||
|
import { useMouseStore, useSettingsStore } from "./stores";
|
||||||
|
|
||||||
|
const calcDelta = (pos: number) => (Math.abs(pos) < 10 ? pos * 2 : pos);
|
||||||
|
|
||||||
|
export interface AbsMouseMoveHandlerProps {
|
||||||
|
videoClientWidth: number;
|
||||||
|
videoClientHeight: number;
|
||||||
|
videoWidth: number;
|
||||||
|
videoHeight: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RelMouseMoveHandlerProps {
|
||||||
|
isPointerLockActive: boolean;
|
||||||
|
isPointerLockPossible: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function useMouse() {
|
||||||
|
// states
|
||||||
|
const { setMousePosition, setMouseMove } = useMouseStore();
|
||||||
|
const [blockWheelEvent, setBlockWheelEvent] = useState(false);
|
||||||
|
|
||||||
|
const { mouseMode, scrollThrottling } = useSettingsStore();
|
||||||
|
|
||||||
|
// RPC hooks
|
||||||
|
const { send } = useJsonRpc();
|
||||||
|
const { reportAbsMouseEvent, reportRelMouseEvent, rpcHidReady } = useHidRpc();
|
||||||
|
// Mouse-related
|
||||||
|
|
||||||
|
const sendRelMouseMovement = useCallback(
|
||||||
|
(x: number, y: number, buttons: number) => {
|
||||||
|
if (mouseMode !== "relative") return;
|
||||||
|
// if we ignore the event, double-click will not work
|
||||||
|
// if (x === 0 && y === 0 && buttons === 0) return;
|
||||||
|
const dx = calcDelta(x);
|
||||||
|
const dy = calcDelta(y);
|
||||||
|
if (rpcHidReady) {
|
||||||
|
reportRelMouseEvent(dx, dy, buttons);
|
||||||
|
} else {
|
||||||
|
// kept for backward compatibility
|
||||||
|
send("relMouseReport", { dx, dy, buttons });
|
||||||
|
}
|
||||||
|
setMouseMove({ x, y, buttons });
|
||||||
|
},
|
||||||
|
[
|
||||||
|
send,
|
||||||
|
reportRelMouseEvent,
|
||||||
|
setMouseMove,
|
||||||
|
mouseMode,
|
||||||
|
rpcHidReady,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
const getRelMouseMoveHandler = useCallback(
|
||||||
|
({ isPointerLockActive, isPointerLockPossible }: RelMouseMoveHandlerProps) => (e: MouseEvent) => {
|
||||||
|
if (mouseMode !== "relative") return;
|
||||||
|
if (isPointerLockActive === false && isPointerLockPossible) return;
|
||||||
|
|
||||||
|
// Send mouse movement
|
||||||
|
const { buttons } = e;
|
||||||
|
sendRelMouseMovement(e.movementX, e.movementY, buttons);
|
||||||
|
},
|
||||||
|
[sendRelMouseMovement, mouseMode],
|
||||||
|
);
|
||||||
|
|
||||||
|
const sendAbsMouseMovement = useCallback(
|
||||||
|
(x: number, y: number, buttons: number) => {
|
||||||
|
if (mouseMode !== "absolute") return;
|
||||||
|
if (rpcHidReady) {
|
||||||
|
reportAbsMouseEvent(x, y, buttons);
|
||||||
|
} else {
|
||||||
|
// kept for backward compatibility
|
||||||
|
send("absMouseReport", { x, y, buttons });
|
||||||
|
}
|
||||||
|
// We set that for the debug info bar
|
||||||
|
setMousePosition(x, y);
|
||||||
|
},
|
||||||
|
[
|
||||||
|
send,
|
||||||
|
reportAbsMouseEvent,
|
||||||
|
setMousePosition,
|
||||||
|
mouseMode,
|
||||||
|
rpcHidReady,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
const getAbsMouseMoveHandler = useCallback(
|
||||||
|
({ videoClientWidth, videoClientHeight, videoWidth, videoHeight }: AbsMouseMoveHandlerProps) => (e: MouseEvent) => {
|
||||||
|
if (!videoClientWidth || !videoClientHeight) return;
|
||||||
|
if (mouseMode !== "absolute") return;
|
||||||
|
|
||||||
|
// Get the aspect ratios of the video element and the video stream
|
||||||
|
const videoElementAspectRatio = videoClientWidth / videoClientHeight;
|
||||||
|
const videoStreamAspectRatio = videoWidth / videoHeight;
|
||||||
|
|
||||||
|
// Calculate the effective video display area
|
||||||
|
let effectiveWidth = videoClientWidth;
|
||||||
|
let effectiveHeight = videoClientHeight;
|
||||||
|
let offsetX = 0;
|
||||||
|
let offsetY = 0;
|
||||||
|
|
||||||
|
if (videoElementAspectRatio > videoStreamAspectRatio) {
|
||||||
|
// Pillarboxing: black bars on the left and right
|
||||||
|
effectiveWidth = videoClientHeight * videoStreamAspectRatio;
|
||||||
|
offsetX = (videoClientWidth - effectiveWidth) / 2;
|
||||||
|
} else if (videoElementAspectRatio < videoStreamAspectRatio) {
|
||||||
|
// Letterboxing: black bars on the top and bottom
|
||||||
|
effectiveHeight = videoClientWidth / videoStreamAspectRatio;
|
||||||
|
offsetY = (videoClientHeight - effectiveHeight) / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clamp mouse position within the effective video boundaries
|
||||||
|
const clampedX = Math.min(Math.max(offsetX, e.offsetX), offsetX + effectiveWidth);
|
||||||
|
const clampedY = Math.min(Math.max(offsetY, e.offsetY), offsetY + effectiveHeight);
|
||||||
|
|
||||||
|
// Map clamped mouse position to the video stream's coordinate system
|
||||||
|
const relativeX = (clampedX - offsetX) / effectiveWidth;
|
||||||
|
const relativeY = (clampedY - offsetY) / effectiveHeight;
|
||||||
|
|
||||||
|
// Convert to HID absolute coordinate system (0-32767 range)
|
||||||
|
const x = Math.round(relativeX * 32767);
|
||||||
|
const y = Math.round(relativeY * 32767);
|
||||||
|
|
||||||
|
// Send mouse movement
|
||||||
|
const { buttons } = e;
|
||||||
|
sendAbsMouseMovement(x, y, buttons);
|
||||||
|
}, [mouseMode, sendAbsMouseMovement],
|
||||||
|
);
|
||||||
|
|
||||||
|
const getMouseWheelHandler = useCallback(
|
||||||
|
() => (e: WheelEvent) => {
|
||||||
|
if (scrollThrottling && blockWheelEvent) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine if the wheel event is an accel scroll value
|
||||||
|
const isAccel = Math.abs(e.deltaY) >= 100;
|
||||||
|
|
||||||
|
// Calculate the accel scroll value
|
||||||
|
const accelScrollValue = e.deltaY / 100;
|
||||||
|
|
||||||
|
// Calculate the no accel scroll value
|
||||||
|
const noAccelScrollValue = Math.sign(e.deltaY);
|
||||||
|
|
||||||
|
// Get scroll value
|
||||||
|
const scrollValue = isAccel ? accelScrollValue : noAccelScrollValue;
|
||||||
|
|
||||||
|
// Apply clamping (i.e. min and max mouse wheel hardware value)
|
||||||
|
const clampedScrollValue = Math.max(-127, Math.min(127, scrollValue));
|
||||||
|
|
||||||
|
// Invert the clamped scroll value to match expected behavior
|
||||||
|
const invertedScrollValue = -clampedScrollValue;
|
||||||
|
|
||||||
|
send("wheelReport", { wheelY: invertedScrollValue });
|
||||||
|
|
||||||
|
// Apply blocking delay based of throttling settings
|
||||||
|
if (scrollThrottling && !blockWheelEvent) {
|
||||||
|
setBlockWheelEvent(true);
|
||||||
|
setTimeout(() => setBlockWheelEvent(false), scrollThrottling);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[send, blockWheelEvent, scrollThrottling],
|
||||||
|
);
|
||||||
|
|
||||||
|
const resetMousePosition = useCallback(() => {
|
||||||
|
sendAbsMouseMovement(0, 0, 0);
|
||||||
|
}, [sendAbsMouseMovement]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
getRelMouseMoveHandler,
|
||||||
|
getAbsMouseMoveHandler,
|
||||||
|
getMouseWheelHandler,
|
||||||
|
resetMousePosition,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -7,7 +7,7 @@ import {
|
||||||
redirect,
|
redirect,
|
||||||
RouterProvider,
|
RouterProvider,
|
||||||
useRouteError,
|
useRouteError,
|
||||||
} from "react-router-dom";
|
} from "react-router";
|
||||||
import { ExclamationTriangleIcon } from "@heroicons/react/16/solid";
|
import { ExclamationTriangleIcon } from "@heroicons/react/16/solid";
|
||||||
|
|
||||||
import { CLOUD_API, DEVICE_API } from "@/ui.config";
|
import { CLOUD_API, DEVICE_API } from "@/ui.config";
|
||||||
|
|
@ -28,7 +28,7 @@ import DeviceIdRename from "@routes/devices.$id.rename";
|
||||||
import DevicesRoute from "@routes/devices";
|
import DevicesRoute from "@routes/devices";
|
||||||
import SettingsIndexRoute from "@routes/devices.$id.settings._index";
|
import SettingsIndexRoute from "@routes/devices.$id.settings._index";
|
||||||
import SettingsAccessIndexRoute from "@routes/devices.$id.settings.access._index";
|
import SettingsAccessIndexRoute from "@routes/devices.$id.settings.access._index";
|
||||||
const Notifications = lazy(() => import("@/notifications"));
|
import Notifications from "@/notifications";
|
||||||
const SignupRoute = lazy(() => import("@routes/signup"));
|
const SignupRoute = lazy(() => import("@routes/signup"));
|
||||||
const LoginRoute = lazy(() => import("@routes/login"));
|
const LoginRoute = lazy(() => import("@routes/login"));
|
||||||
const DevicesAlreadyAdopted = lazy(() => import("@routes/devices.already-adopted"));
|
const DevicesAlreadyAdopted = lazy(() => import("@routes/devices.already-adopted"));
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Outlet } from "react-router-dom";
|
import { Outlet } from "react-router";
|
||||||
|
|
||||||
function Root() {
|
function Root() {
|
||||||
return <Outlet />;
|
return <Outlet />;
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { LoaderFunctionArgs, redirect } from "react-router-dom";
|
import { redirect } from "react-router";
|
||||||
|
import type { LoaderFunction, LoaderFunctionArgs } from "react-router";
|
||||||
|
|
||||||
import { DEVICE_API } from "@/ui.config";
|
import { DEVICE_API } from "@/ui.config";
|
||||||
|
import api from "@/api";
|
||||||
import api from "../api";
|
|
||||||
|
|
||||||
export interface CloudState {
|
export interface CloudState {
|
||||||
connected: boolean;
|
connected: boolean;
|
||||||
|
|
@ -10,7 +10,7 @@ export interface CloudState {
|
||||||
appUrl: string;
|
appUrl: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const loader = async ({ request }: LoaderFunctionArgs) => {
|
const loader: LoaderFunction = async ({ request }: LoaderFunctionArgs) => {
|
||||||
const url = new URL(request.url);
|
const url = new URL(request.url);
|
||||||
const searchParams = url.searchParams;
|
const searchParams = url.searchParams;
|
||||||
|
|
||||||
|
|
@ -37,7 +37,7 @@ const loader = async ({ request }: LoaderFunctionArgs) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function AdoptRoute() {
|
export default function AdoptRoute() {
|
||||||
return <></>;
|
return (<></>);
|
||||||
}
|
}
|
||||||
|
|
||||||
AdoptRoute.loader = loader;
|
AdoptRoute.loader = loader;
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,5 @@
|
||||||
import {
|
import { Form, redirect, useActionData, useLoaderData } from "react-router";
|
||||||
ActionFunctionArgs,
|
import type { ActionFunction, ActionFunctionArgs, LoaderFunction, LoaderFunctionArgs } from "react-router";
|
||||||
Form,
|
|
||||||
LoaderFunctionArgs,
|
|
||||||
redirect,
|
|
||||||
useActionData,
|
|
||||||
useLoaderData,
|
|
||||||
} from "react-router-dom";
|
|
||||||
import { ChevronLeftIcon } from "@heroicons/react/16/solid";
|
import { ChevronLeftIcon } from "@heroicons/react/16/solid";
|
||||||
|
|
||||||
import { Button, LinkButton } from "@components/Button";
|
import { Button, LinkButton } from "@components/Button";
|
||||||
|
|
@ -22,7 +16,7 @@ interface LoaderData {
|
||||||
user: User;
|
user: User;
|
||||||
}
|
}
|
||||||
|
|
||||||
const action = async ({ request }: ActionFunctionArgs) => {
|
const action: ActionFunction = async ({ request }: ActionFunctionArgs) => {
|
||||||
const { deviceId } = Object.fromEntries(await request.formData());
|
const { deviceId } = Object.fromEntries(await request.formData());
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
@ -34,17 +28,17 @@ const action = async ({ request }: ActionFunctionArgs) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
return { message: "There was an error renaming your device. Please try again." };
|
return { message: "There was an error deregistering your device. Please try again." };
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
return { message: "There was an error renaming your device. Please try again." };
|
return { message: "There was an error deregistering your device. Please try again." };
|
||||||
}
|
}
|
||||||
|
|
||||||
return redirect("/devices");
|
return redirect("/devices");
|
||||||
};
|
};
|
||||||
|
|
||||||
const loader = async ({ params }: LoaderFunctionArgs) => {
|
const loader: LoaderFunction = async ({ params }: LoaderFunctionArgs) => {
|
||||||
const user = await checkAuth();
|
const user = await checkAuth();
|
||||||
const { id } = params;
|
const { id } = params;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import {
|
||||||
} from "react-icons/lu";
|
} from "react-icons/lu";
|
||||||
import { PlusCircleIcon, ExclamationTriangleIcon } from "@heroicons/react/20/solid";
|
import { PlusCircleIcon, ExclamationTriangleIcon } from "@heroicons/react/20/solid";
|
||||||
import { TrashIcon } from "@heroicons/react/16/solid";
|
import { TrashIcon } from "@heroicons/react/16/solid";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router";
|
||||||
|
|
||||||
import Card, { GridCard } from "@/components/Card";
|
import Card, { GridCard } from "@/components/Card";
|
||||||
import { Button } from "@components/Button";
|
import { Button } from "@components/Button";
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useNavigate, useOutletContext } from "react-router-dom";
|
import { useNavigate, useOutletContext } from "react-router";
|
||||||
|
|
||||||
import { GridCard } from "@/components/Card";
|
import { GridCard } from "@/components/Card";
|
||||||
import { Button } from "@components/Button";
|
import { Button } from "@components/Button";
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,5 @@
|
||||||
import {
|
import { Form, redirect, useActionData, useLoaderData } from "react-router";
|
||||||
ActionFunctionArgs,
|
import type { ActionFunction, ActionFunctionArgs, LoaderFunction, LoaderFunctionArgs } from "react-router";
|
||||||
Form,
|
|
||||||
LoaderFunctionArgs,
|
|
||||||
redirect,
|
|
||||||
useActionData,
|
|
||||||
useLoaderData,
|
|
||||||
} from "react-router-dom";
|
|
||||||
import { ChevronLeftIcon } from "@heroicons/react/16/solid";
|
import { ChevronLeftIcon } from "@heroicons/react/16/solid";
|
||||||
|
|
||||||
import { Button, LinkButton } from "@components/Button";
|
import { Button, LinkButton } from "@components/Button";
|
||||||
|
|
@ -25,7 +19,7 @@ interface LoaderData {
|
||||||
user: User;
|
user: User;
|
||||||
}
|
}
|
||||||
|
|
||||||
const action = async ({ params, request }: ActionFunctionArgs) => {
|
const action: ActionFunction = async ({ params, request }: ActionFunctionArgs) => {
|
||||||
const { id } = params;
|
const { id } = params;
|
||||||
const { name } = Object.fromEntries(await request.formData());
|
const { name } = Object.fromEntries(await request.formData());
|
||||||
|
|
||||||
|
|
@ -48,7 +42,7 @@ const action = async ({ params, request }: ActionFunctionArgs) => {
|
||||||
return redirect("/devices");
|
return redirect("/devices");
|
||||||
};
|
};
|
||||||
|
|
||||||
const loader = async ({ params }: LoaderFunctionArgs) => {
|
const loader: LoaderFunction = async ({ params }: LoaderFunctionArgs) => {
|
||||||
const user = await checkAuth();
|
const user = await checkAuth();
|
||||||
const { id } = params;
|
const { id } = params;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
import { LoaderFunctionArgs, redirect } from "react-router-dom";
|
import { redirect } from "react-router";
|
||||||
|
import type { LoaderFunction, LoaderFunctionArgs } from "react-router";
|
||||||
|
|
||||||
import { getDeviceUiPath } from "../hooks/useAppNavigation";
|
import { getDeviceUiPath } from "../hooks/useAppNavigation";
|
||||||
|
|
||||||
const loader = ({ params }: LoaderFunctionArgs) => {
|
const loader: LoaderFunction = ({ params }: LoaderFunctionArgs) => {
|
||||||
return redirect(getDeviceUiPath("/settings/general", params.id));
|
return redirect(getDeviceUiPath("/settings/general", params.id));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { useLoaderData, useNavigate } from "react-router-dom";
|
import { useLoaderData, useNavigate } from "react-router";
|
||||||
|
import type { LoaderFunction } from "react-router";
|
||||||
import { ShieldCheckIcon } from "@heroicons/react/24/outline";
|
import { ShieldCheckIcon } from "@heroicons/react/24/outline";
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
|
||||||
|
|
@ -26,7 +27,7 @@ export interface TLSState {
|
||||||
privateKey?: string;
|
privateKey?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const loader = async () => {
|
const loader: LoaderFunction = async () => {
|
||||||
if (isOnDevice) {
|
if (isOnDevice) {
|
||||||
const status = await api
|
const status = await api
|
||||||
.GET(`${DEVICE_API}/device`)
|
.GET(`${DEVICE_API}/device`)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { useLocation, useRevalidator } from "react-router-dom";
|
import { useLocation, useRevalidator } from "react-router";
|
||||||
|
|
||||||
import { Button } from "@components/Button";
|
import { Button } from "@components/Button";
|
||||||
import { InputFieldWithLabel } from "@/components/InputField";
|
import { InputFieldWithLabel } from "@/components/InputField";
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router";
|
||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
|
|
||||||
import { useJsonRpc } from "@/hooks/useJsonRpc";
|
import { useJsonRpc } from "@/hooks/useJsonRpc";
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useLocation, useNavigate } from "react-router-dom";
|
import { useLocation, useNavigate } from "react-router";
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { CheckCircleIcon } from "@heroicons/react/20/solid";
|
import { CheckCircleIcon } from "@heroicons/react/20/solid";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
import { KeySequence, useMacrosStore, generateMacroId } from "@/hooks/stores";
|
import { KeySequence, useMacrosStore, generateMacroId } from "@/hooks/stores";
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useNavigate, useParams } from "react-router-dom";
|
import { useNavigate, useParams } from "react-router";
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { LuTrash2 } from "react-icons/lu";
|
import { LuTrash2 } from "react-icons/lu";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { useEffect, Fragment, useMemo, useState, useCallback } from "react";
|
import { useEffect, Fragment, useMemo, useState, useCallback } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router";
|
||||||
import {
|
import {
|
||||||
LuPenLine,
|
LuPenLine,
|
||||||
LuCopy,
|
LuCopy,
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { NavLink, Outlet, useLocation } from "react-router-dom";
|
import { NavLink, Outlet, useLocation } from "react-router";
|
||||||
import {
|
import {
|
||||||
LuSettings,
|
LuSettings,
|
||||||
LuMouse,
|
LuMouse,
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,5 @@
|
||||||
import {
|
import { Form, redirect, useActionData, useParams, useSearchParams } from "react-router";
|
||||||
ActionFunctionArgs,
|
import type { ActionFunction, ActionFunctionArgs, LoaderFunction, LoaderFunctionArgs } from "react-router";
|
||||||
Form,
|
|
||||||
LoaderFunctionArgs,
|
|
||||||
redirect,
|
|
||||||
useActionData,
|
|
||||||
useParams,
|
|
||||||
useSearchParams,
|
|
||||||
} from "react-router-dom";
|
|
||||||
|
|
||||||
import SimpleNavbar from "@components/SimpleNavbar";
|
import SimpleNavbar from "@components/SimpleNavbar";
|
||||||
import GridBackground from "@components/GridBackground";
|
import GridBackground from "@components/GridBackground";
|
||||||
|
|
@ -20,7 +13,7 @@ import { CLOUD_API } from "@/ui.config";
|
||||||
|
|
||||||
import api from "../api";
|
import api from "../api";
|
||||||
|
|
||||||
const loader = async ({ params }: LoaderFunctionArgs) => {
|
const loader: LoaderFunction = async ({ params }: LoaderFunctionArgs) => {
|
||||||
await checkAuth();
|
await checkAuth();
|
||||||
const res = await fetch(`${CLOUD_API}/devices/${params.id}`, {
|
const res = await fetch(`${CLOUD_API}/devices/${params.id}`, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
|
|
@ -35,7 +28,7 @@ const loader = async ({ params }: LoaderFunctionArgs) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const action = async ({ request }: ActionFunctionArgs) => {
|
const action: ActionFunction = async ({ request }: ActionFunctionArgs) => {
|
||||||
// Handle form submission
|
// Handle form submission
|
||||||
const { name, id, returnTo } = Object.fromEntries(await request.formData());
|
const { name, id, returnTo } = Object.fromEntries(await request.formData());
|
||||||
const res = await api.PUT(`${CLOUD_API}/devices/${id}`, { name });
|
const res = await api.PUT(`${CLOUD_API}/devices/${id}`, { name });
|
||||||
|
|
@ -43,7 +36,7 @@ const action = async ({ request }: ActionFunctionArgs) => {
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
return redirect(returnTo?.toString() ?? `/devices/${id}`);
|
return redirect(returnTo?.toString() ?? `/devices/${id}`);
|
||||||
} else {
|
} else {
|
||||||
return { error: "There was an error creating your device" };
|
return { error: "There was an error registering your device" };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
import { lazy, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { lazy, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import {
|
import {
|
||||||
LoaderFunctionArgs,
|
|
||||||
Outlet,
|
Outlet,
|
||||||
Params,
|
|
||||||
redirect,
|
redirect,
|
||||||
useLoaderData,
|
useLoaderData,
|
||||||
useLocation,
|
useLocation,
|
||||||
|
|
@ -10,7 +8,8 @@ import {
|
||||||
useOutlet,
|
useOutlet,
|
||||||
useParams,
|
useParams,
|
||||||
useSearchParams,
|
useSearchParams,
|
||||||
} from "react-router-dom";
|
} from "react-router";
|
||||||
|
import type { LoaderFunction, LoaderFunctionArgs, Params } from "react-router";
|
||||||
import { useInterval } from "usehooks-ts";
|
import { useInterval } from "usehooks-ts";
|
||||||
import { FocusTrap } from "focus-trap-react";
|
import { FocusTrap } from "focus-trap-react";
|
||||||
import { motion, AnimatePresence } from "framer-motion";
|
import { motion, AnimatePresence } from "framer-motion";
|
||||||
|
|
@ -112,7 +111,7 @@ const cloudLoader = async (params: Params<string>): Promise<CloudLoaderResp> =>
|
||||||
return { user, iceConfig, deviceName: device.name || device.id };
|
return { user, iceConfig, deviceName: device.name || device.id };
|
||||||
};
|
};
|
||||||
|
|
||||||
const loader = ({ params }: LoaderFunctionArgs) => {
|
const loader: LoaderFunction = ({ params }: LoaderFunctionArgs) => {
|
||||||
return import.meta.env.MODE === "device" ? deviceLoader() : cloudLoader(params);
|
return import.meta.env.MODE === "device" ? deviceLoader() : cloudLoader(params);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -135,7 +134,8 @@ export default function KvmIdRoute() {
|
||||||
setRpcDataChannel,
|
setRpcDataChannel,
|
||||||
isTurnServerInUse, setTurnServerInUse,
|
isTurnServerInUse, setTurnServerInUse,
|
||||||
rpcDataChannel,
|
rpcDataChannel,
|
||||||
setTransceiver
|
setTransceiver,
|
||||||
|
setRpcHidChannel,
|
||||||
} = useRTCStore();
|
} = useRTCStore();
|
||||||
|
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
@ -482,6 +482,12 @@ export default function KvmIdRoute() {
|
||||||
setRpcDataChannel(rpcDataChannel);
|
setRpcDataChannel(rpcDataChannel);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const rpcHidChannel = pc.createDataChannel("hidrpc");
|
||||||
|
rpcHidChannel.binaryType = "arraybuffer";
|
||||||
|
rpcHidChannel.onopen = () => {
|
||||||
|
setRpcHidChannel(rpcHidChannel);
|
||||||
|
};
|
||||||
|
|
||||||
setPeerConnection(pc);
|
setPeerConnection(pc);
|
||||||
}, [
|
}, [
|
||||||
cleanupAndStopReconnecting,
|
cleanupAndStopReconnecting,
|
||||||
|
|
@ -492,6 +498,7 @@ export default function KvmIdRoute() {
|
||||||
setPeerConnection,
|
setPeerConnection,
|
||||||
setPeerConnectionState,
|
setPeerConnectionState,
|
||||||
setRpcDataChannel,
|
setRpcDataChannel,
|
||||||
|
setRpcHidChannel,
|
||||||
setTransceiver,
|
setTransceiver,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { useLoaderData, useRevalidator } from "react-router-dom";
|
import { useLoaderData, useRevalidator } from "react-router";
|
||||||
|
import type { LoaderFunction } from "react-router";
|
||||||
import { LuMonitorSmartphone } from "react-icons/lu";
|
import { LuMonitorSmartphone } from "react-icons/lu";
|
||||||
import { ArrowRightIcon } from "@heroicons/react/16/solid";
|
import { ArrowRightIcon } from "@heroicons/react/16/solid";
|
||||||
import { useInterval } from "usehooks-ts";
|
import { useInterval } from "usehooks-ts";
|
||||||
|
|
@ -16,7 +17,7 @@ interface LoaderData {
|
||||||
user: User;
|
user: User;
|
||||||
}
|
}
|
||||||
|
|
||||||
const loader = async () => {
|
const loader: LoaderFunction = async () => {
|
||||||
const user = await checkAuth();
|
const user = await checkAuth();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { ActionFunctionArgs, Form, redirect, useActionData } from "react-router-dom";
|
import { Form, redirect, useActionData } from "react-router";
|
||||||
|
import type { ActionFunction, ActionFunctionArgs, LoaderFunction } from "react-router";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { LuEye, LuEyeOff } from "react-icons/lu";
|
import { LuEye, LuEyeOff } from "react-icons/lu";
|
||||||
|
|
||||||
|
|
@ -17,7 +18,7 @@ import ExtLink from "../components/ExtLink";
|
||||||
|
|
||||||
import { DeviceStatus } from "./welcome-local";
|
import { DeviceStatus } from "./welcome-local";
|
||||||
|
|
||||||
const loader = async () => {
|
const loader: LoaderFunction = async () => {
|
||||||
const res = await api
|
const res = await api
|
||||||
.GET(`${DEVICE_API}/device/status`)
|
.GET(`${DEVICE_API}/device/status`)
|
||||||
.then(res => res.json() as Promise<DeviceStatus>);
|
.then(res => res.json() as Promise<DeviceStatus>);
|
||||||
|
|
@ -29,7 +30,7 @@ const loader = async () => {
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const action = async ({ request }: ActionFunctionArgs) => {
|
const action: ActionFunction = async ({ request }: ActionFunctionArgs) => {
|
||||||
const formData = await request.formData();
|
const formData = await request.formData();
|
||||||
const password = formData.get("password");
|
const password = formData.get("password");
|
||||||
|
|
||||||
|
|
@ -86,6 +87,7 @@ export default function LoginLocalRoute() {
|
||||||
label="Password"
|
label="Password"
|
||||||
type={showPassword ? "text" : "password"}
|
type={showPassword ? "text" : "password"}
|
||||||
name="password"
|
name="password"
|
||||||
|
autoComplete="current-password"
|
||||||
placeholder="Enter your password"
|
placeholder="Enter your password"
|
||||||
autoFocus
|
autoFocus
|
||||||
error={actionData?.error}
|
error={actionData?.error}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useLocation, useSearchParams } from "react-router-dom";
|
import { useLocation, useSearchParams } from "react-router";
|
||||||
|
|
||||||
import AuthLayout from "@components/AuthLayout";
|
import AuthLayout from "@components/AuthLayout";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useLocation, useSearchParams } from "react-router-dom";
|
import { useLocation, useSearchParams } from "react-router";
|
||||||
|
|
||||||
import AuthLayout from "@components/AuthLayout";
|
import AuthLayout from "@components/AuthLayout";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { ActionFunctionArgs, Form, redirect, useActionData } from "react-router-dom";
|
import { Form, redirect, useActionData } from "react-router";
|
||||||
|
import type { ActionFunction, ActionFunctionArgs, LoaderFunction } from "react-router";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
import GridBackground from "@components/GridBackground";
|
import GridBackground from "@components/GridBackground";
|
||||||
|
|
@ -14,7 +15,7 @@ import api from "../api";
|
||||||
|
|
||||||
import { DeviceStatus } from "./welcome-local";
|
import { DeviceStatus } from "./welcome-local";
|
||||||
|
|
||||||
const loader = async () => {
|
const loader: LoaderFunction = async () => {
|
||||||
const res = await api
|
const res = await api
|
||||||
.GET(`${DEVICE_API}/device/status`)
|
.GET(`${DEVICE_API}/device/status`)
|
||||||
.then(res => res.json() as Promise<DeviceStatus>);
|
.then(res => res.json() as Promise<DeviceStatus>);
|
||||||
|
|
@ -23,7 +24,7 @@ const loader = async () => {
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const action = async ({ request }: ActionFunctionArgs) => {
|
const action: ActionFunction = async ({ request }: ActionFunctionArgs) => {
|
||||||
const formData = await request.formData();
|
const formData = await request.formData();
|
||||||
const localAuthMode = formData.get("localAuthMode");
|
const localAuthMode = formData.get("localAuthMode");
|
||||||
if (!localAuthMode) return { error: "Please select an authentication mode" };
|
if (!localAuthMode) return { error: "Please select an authentication mode" };
|
||||||
|
|
@ -162,5 +163,5 @@ export default function WelcomeLocalModeRoute() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
WelcomeLocalModeRoute.action = action;
|
|
||||||
WelcomeLocalModeRoute.loader = loader;
|
WelcomeLocalModeRoute.loader = loader;
|
||||||
|
WelcomeLocalModeRoute.action = action;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { ActionFunctionArgs, Form, redirect, useActionData } from "react-router-dom";
|
import { Form, redirect, useActionData } from "react-router";
|
||||||
|
import type { ActionFunction, ActionFunctionArgs, LoaderFunction } from "react-router";
|
||||||
import { useState, useRef, useEffect } from "react";
|
import { useState, useRef, useEffect } from "react";
|
||||||
import { LuEye, LuEyeOff } from "react-icons/lu";
|
import { LuEye, LuEyeOff } from "react-icons/lu";
|
||||||
|
|
||||||
|
|
@ -15,7 +16,7 @@ import api from "../api";
|
||||||
|
|
||||||
import { DeviceStatus } from "./welcome-local";
|
import { DeviceStatus } from "./welcome-local";
|
||||||
|
|
||||||
const loader = async () => {
|
const loader: LoaderFunction = async () => {
|
||||||
const res = await api
|
const res = await api
|
||||||
.GET(`${DEVICE_API}/device/status`)
|
.GET(`${DEVICE_API}/device/status`)
|
||||||
.then(res => res.json() as Promise<DeviceStatus>);
|
.then(res => res.json() as Promise<DeviceStatus>);
|
||||||
|
|
@ -24,7 +25,7 @@ const loader = async () => {
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const action = async ({ request }: ActionFunctionArgs) => {
|
const action: ActionFunction = async ({ request }: ActionFunctionArgs) => {
|
||||||
const formData = await request.formData();
|
const formData = await request.formData();
|
||||||
const password = formData.get("password");
|
const password = formData.get("password");
|
||||||
const confirmPassword = formData.get("confirmPassword");
|
const confirmPassword = formData.get("confirmPassword");
|
||||||
|
|
@ -174,5 +175,5 @@ export default function WelcomeLocalPasswordRoute() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
WelcomeLocalPasswordRoute.action = action;
|
|
||||||
WelcomeLocalPasswordRoute.loader = loader;
|
WelcomeLocalPasswordRoute.loader = loader;
|
||||||
|
WelcomeLocalPasswordRoute.action = action;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { cx } from "cva";
|
import { cx } from "cva";
|
||||||
import { redirect } from "react-router-dom";
|
import { redirect } from "react-router";
|
||||||
|
import type { LoaderFunction } from "react-router";
|
||||||
|
|
||||||
import GridBackground from "@components/GridBackground";
|
import GridBackground from "@components/GridBackground";
|
||||||
import Container from "@components/Container";
|
import Container from "@components/Container";
|
||||||
|
|
@ -17,7 +18,7 @@ export interface DeviceStatus {
|
||||||
isSetup: boolean;
|
isSetup: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const loader = async () => {
|
const loader: LoaderFunction = async () => {
|
||||||
const res = await api
|
const res = await api
|
||||||
.GET(`${DEVICE_API}/device/status`)
|
.GET(`${DEVICE_API}/device/status`)
|
||||||
.then(res => res.json() as Promise<DeviceStatus>);
|
.then(res => res.json() as Promise<DeviceStatus>);
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,9 @@ export default defineConfig(({ mode, command }) => {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
plugins,
|
plugins,
|
||||||
|
esbuild: {
|
||||||
|
pure: ["console.debug"],
|
||||||
|
},
|
||||||
build: { outDir: isCloud ? "dist" : "../static" },
|
build: { outDir: isCloud ? "dist" : "../static" },
|
||||||
server: {
|
server: {
|
||||||
host: "0.0.0.0",
|
host: "0.0.0.0",
|
||||||
|
|
|
||||||
4
usb.go
4
usb.go
|
|
@ -27,13 +27,13 @@ func initUsbGadget() {
|
||||||
|
|
||||||
gadget.SetOnKeyboardStateChange(func(state usbgadget.KeyboardState) {
|
gadget.SetOnKeyboardStateChange(func(state usbgadget.KeyboardState) {
|
||||||
if currentSession != nil {
|
if currentSession != nil {
|
||||||
writeJSONRPCEvent("keyboardLedState", state, currentSession)
|
currentSession.reportHidRPCKeyboardLedState(state)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
gadget.SetOnKeysDownChange(func(state usbgadget.KeysDownState) {
|
gadget.SetOnKeysDownChange(func(state usbgadget.KeysDownState) {
|
||||||
if currentSession != nil {
|
if currentSession != nil {
|
||||||
writeJSONRPCEvent("keysDownState", state, currentSession)
|
currentSession.reportHidRPCKeysDownState(state)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
86
webrtc.go
86
webrtc.go
|
|
@ -6,10 +6,12 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net"
|
"net"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"github.com/coder/websocket"
|
"github.com/coder/websocket"
|
||||||
"github.com/coder/websocket/wsjson"
|
"github.com/coder/websocket/wsjson"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/jetkvm/kvm/internal/hidrpc"
|
||||||
"github.com/jetkvm/kvm/internal/logging"
|
"github.com/jetkvm/kvm/internal/logging"
|
||||||
"github.com/pion/webrtc/v4"
|
"github.com/pion/webrtc/v4"
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
|
|
@ -22,7 +24,12 @@ type Session struct {
|
||||||
RPCChannel *webrtc.DataChannel
|
RPCChannel *webrtc.DataChannel
|
||||||
HidChannel *webrtc.DataChannel
|
HidChannel *webrtc.DataChannel
|
||||||
shouldUmountVirtualMedia bool
|
shouldUmountVirtualMedia bool
|
||||||
rpcQueue chan webrtc.DataChannelMessage
|
|
||||||
|
rpcQueue chan webrtc.DataChannelMessage
|
||||||
|
|
||||||
|
hidRPCAvailable bool
|
||||||
|
hidQueueLock sync.Mutex
|
||||||
|
hidQueue []chan webrtc.DataChannelMessage
|
||||||
}
|
}
|
||||||
|
|
||||||
type SessionConfig struct {
|
type SessionConfig struct {
|
||||||
|
|
@ -67,6 +74,23 @@ func (s *Session) ExchangeOffer(offerStr string) (string, error) {
|
||||||
return base64.StdEncoding.EncodeToString(localDescription), nil
|
return base64.StdEncoding.EncodeToString(localDescription), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Session) initQueues() {
|
||||||
|
s.hidQueueLock.Lock()
|
||||||
|
defer s.hidQueueLock.Unlock()
|
||||||
|
|
||||||
|
s.hidQueue = make([]chan webrtc.DataChannelMessage, 0)
|
||||||
|
for i := 0; i < 4; i++ {
|
||||||
|
q := make(chan webrtc.DataChannelMessage, 256)
|
||||||
|
s.hidQueue = append(s.hidQueue, q)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Session) handleQueues(index int) {
|
||||||
|
for msg := range s.hidQueue[index] {
|
||||||
|
onHidMessage(msg.Data, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func newSession(config SessionConfig) (*Session, error) {
|
func newSession(config SessionConfig) (*Session, error) {
|
||||||
webrtcSettingEngine := webrtc.SettingEngine{
|
webrtcSettingEngine := webrtc.SettingEngine{
|
||||||
LoggerFactory: logging.GetPionDefaultLoggerFactory(),
|
LoggerFactory: logging.GetPionDefaultLoggerFactory(),
|
||||||
|
|
@ -105,17 +129,68 @@ func newSession(config SessionConfig) (*Session, error) {
|
||||||
scopedLogger.Warn().Err(err).Msg("Failed to create PeerConnection")
|
scopedLogger.Warn().Err(err).Msg("Failed to create PeerConnection")
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
session := &Session{peerConnection: peerConnection}
|
session := &Session{peerConnection: peerConnection}
|
||||||
session.rpcQueue = make(chan webrtc.DataChannelMessage, 256)
|
session.rpcQueue = make(chan webrtc.DataChannelMessage, 256)
|
||||||
|
session.initQueues()
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
for msg := range session.rpcQueue {
|
for msg := range session.rpcQueue {
|
||||||
onRPCMessage(msg, session)
|
// TODO: only use goroutine if the task is asynchronous
|
||||||
|
go onRPCMessage(msg, session)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
for i := 0; i < len(session.hidQueue); i++ {
|
||||||
|
go session.handleQueues(i)
|
||||||
|
}
|
||||||
|
|
||||||
peerConnection.OnDataChannel(func(d *webrtc.DataChannel) {
|
peerConnection.OnDataChannel(func(d *webrtc.DataChannel) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
scopedLogger.Error().Interface("error", r).Msg("Recovered from panic in DataChannel handler")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
scopedLogger.Info().Str("label", d.Label()).Uint16("id", *d.ID()).Msg("New DataChannel")
|
scopedLogger.Info().Str("label", d.Label()).Uint16("id", *d.ID()).Msg("New DataChannel")
|
||||||
|
|
||||||
switch d.Label() {
|
switch d.Label() {
|
||||||
|
case "hidrpc":
|
||||||
|
session.HidChannel = d
|
||||||
|
d.OnMessage(func(msg webrtc.DataChannelMessage) {
|
||||||
|
l := scopedLogger.With().Int("length", len(msg.Data)).Logger()
|
||||||
|
// only log data if the log level is debug or lower
|
||||||
|
if scopedLogger.GetLevel() > zerolog.DebugLevel {
|
||||||
|
l = l.With().Str("data", string(msg.Data)).Logger()
|
||||||
|
}
|
||||||
|
|
||||||
|
if msg.IsString {
|
||||||
|
l.Warn().Msg("received string data in HID RPC message handler")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(msg.Data) < 1 {
|
||||||
|
l.Warn().Msg("received empty data in HID RPC message handler")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
l.Trace().Msg("received data in HID RPC message handler")
|
||||||
|
|
||||||
|
// Enqueue to ensure ordered processing
|
||||||
|
queueIndex := hidrpc.GetQueueIndex(hidrpc.MessageType(msg.Data[0]))
|
||||||
|
if queueIndex >= len(session.hidQueue) || queueIndex < 0 {
|
||||||
|
l.Warn().Int("queueIndex", queueIndex).Msg("received data in HID RPC message handler, but queue index not found")
|
||||||
|
queueIndex = 3
|
||||||
|
}
|
||||||
|
|
||||||
|
queue := session.hidQueue[queueIndex]
|
||||||
|
if queue != nil {
|
||||||
|
queue <- msg
|
||||||
|
} else {
|
||||||
|
l.Warn().Int("queueIndex", queueIndex).Msg("received data in HID RPC message handler, but queue is nil")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
case "rpc":
|
case "rpc":
|
||||||
session.RPCChannel = d
|
session.RPCChannel = d
|
||||||
d.OnMessage(func(msg webrtc.DataChannelMessage) {
|
d.OnMessage(func(msg webrtc.DataChannelMessage) {
|
||||||
|
|
@ -198,6 +273,13 @@ func newSession(config SessionConfig) (*Session, error) {
|
||||||
close(session.rpcQueue)
|
close(session.rpcQueue)
|
||||||
session.rpcQueue = nil
|
session.rpcQueue = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Stop HID RPC processor
|
||||||
|
for i := 0; i < len(session.hidQueue); i++ {
|
||||||
|
close(session.hidQueue[i])
|
||||||
|
session.hidQueue[i] = nil
|
||||||
|
}
|
||||||
|
|
||||||
if session.shouldUmountVirtualMedia {
|
if session.shouldUmountVirtualMedia {
|
||||||
if err := rpcUnmountImage(); err != nil {
|
if err := rpcUnmountImage(); err != nil {
|
||||||
scopedLogger.Warn().Err(err).Msg("unmount image failed on connection close")
|
scopedLogger.Warn().Err(err).Msg("unmount image failed on connection close")
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue