Compare commits

...

23 Commits

Author SHA1 Message Date
Marc Brooks a5b07b4862
refactor(ui): Refactor the keyboardLayouts
Add missing keyboard mappings for most layouts
Change  pasteModel.tsx to use the new structure and vastly clarified the way that keys are emitted.
Make each layout export just the KeyboardLayout object (which is a package of isoCode, name, and chars)
Made keyboardLayouts.ts export a function to select keyboard by `isoCode`, export the keyboards as label . value pairs (for a select list) and the list of keyboards.
Changed devices.$id.settings.keyboard.tsx use the exported keyboard option list.
2025-06-12 13:29:13 -05:00
Aveline b822b73a03
chore: use pure Go resolver and remove CGO_ENABLED=0 (#603) 2025-06-12 14:04:51 +02:00
Ben Kochie 58ade3b551
fix: Update metric naming (#602)
Fix up metric names to follow best practice naming conventions[0].

[0]: https://prometheus.io/docs/practices/naming/

Signed-off-by: SuperQ <superq@gmail.com>
2025-06-12 13:41:43 +02:00
Siyuan Miao 3cc119c646 chore: bump version to 0.4.3 2025-06-12 09:35:34 +02:00
Aveline c494cf26ef
chore: disable cgo (#601) 2025-06-12 09:29:31 +02:00
Aveline 4bfbc66ea7
chore: upgrade go from 1.24.3 to 1.24.4 (#600) 2025-06-12 08:53:58 +02:00
dependabot[bot] 0636cc9aff
build(deps): bump github.com/hanwen/go-fuse/v2 from 2.5.1 to 2.8.0 (#590)
Bumps [github.com/hanwen/go-fuse/v2](https://github.com/hanwen/go-fuse) from 2.5.1 to 2.8.0.
- [Commits](https://github.com/hanwen/go-fuse/compare/v2.5.1...v2.8.0)

---
updated-dependencies:
- dependency-name: github.com/hanwen/go-fuse/v2
  dependency-version: 2.8.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-12 08:50:14 +02:00
dependabot[bot] 4f6026e182
build(deps): bump github.com/Masterminds/semver/v3 from 3.3.0 to 3.3.1 (#593)
Bumps [github.com/Masterminds/semver/v3](https://github.com/Masterminds/semver) from 3.3.0 to 3.3.1.
- [Release notes](https://github.com/Masterminds/semver/releases)
- [Changelog](https://github.com/Masterminds/semver/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Masterminds/semver/compare/v3.3.0...v3.3.1)

---
updated-dependencies:
- dependency-name: github.com/Masterminds/semver/v3
  dependency-version: 3.3.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-12 08:49:56 +02:00
dependabot[bot] 89f3bc8c40
build(deps): bump github.com/go-jose/go-jose/v4 in the go_modules group (#596)
Bumps the go_modules group with 1 update: [github.com/go-jose/go-jose/v4](https://github.com/go-jose/go-jose).


Updates `github.com/go-jose/go-jose/v4` from 4.0.2 to 4.0.5
- [Release notes](https://github.com/go-jose/go-jose/releases)
- [Changelog](https://github.com/go-jose/go-jose/blob/main/CHANGELOG.md)
- [Commits](https://github.com/go-jose/go-jose/compare/v4.0.2...v4.0.5)

---
updated-dependencies:
- dependency-name: github.com/go-jose/go-jose/v4
  dependency-version: 4.0.5
  dependency-type: indirect
  dependency-group: go_modules
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-12 08:46:11 +02:00
dependabot[bot] 91171d9bf7
build(deps): bump golang.org/x/net from 0.40.0 to 0.41.0 (#580)
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.40.0 to 0.41.0.
- [Commits](https://github.com/golang/net/compare/v0.40.0...v0.41.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-version: 0.41.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-12 08:45:05 +02:00
dependabot[bot] 0d955a8d95
build(deps): bump github.com/beevik/ntp from 1.3.1 to 1.4.3 (#585)
Bumps [github.com/beevik/ntp](https://github.com/beevik/ntp) from 1.3.1 to 1.4.3.
- [Release notes](https://github.com/beevik/ntp/releases)
- [Changelog](https://github.com/beevik/ntp/blob/main/RELEASE_NOTES.md)
- [Commits](https://github.com/beevik/ntp/compare/v1.3.1...v1.4.3)

---
updated-dependencies:
- dependency-name: github.com/beevik/ntp
  dependency-version: 1.4.3
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-12 08:42:59 +02:00
dependabot[bot] a40d26ab9b
build(deps): bump github.com/prometheus/procfs from 0.15.1 to 0.16.1 (#592)
Bumps [github.com/prometheus/procfs](https://github.com/prometheus/procfs) from 0.15.1 to 0.16.1.
- [Release notes](https://github.com/prometheus/procfs/releases)
- [Commits](https://github.com/prometheus/procfs/compare/v0.15.1...v0.16.1)

---
updated-dependencies:
- dependency-name: github.com/prometheus/procfs
  dependency-version: 0.16.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-12 08:42:25 +02:00
dependabot[bot] 9bd587b52e
build(deps): bump github.com/prometheus/client_golang (#588)
Bumps [github.com/prometheus/client_golang](https://github.com/prometheus/client_golang) from 1.21.0 to 1.22.0.
- [Release notes](https://github.com/prometheus/client_golang/releases)
- [Changelog](https://github.com/prometheus/client_golang/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prometheus/client_golang/compare/v1.21.0...v1.22.0)

---
updated-dependencies:
- dependency-name: github.com/prometheus/client_golang
  dependency-version: 1.22.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-12 08:42:14 +02:00
dependabot[bot] 7ef9a7ba93
build(deps): bump github.com/gin-contrib/logger from 1.2.5 to 1.2.6 (#589)
Bumps [github.com/gin-contrib/logger](https://github.com/gin-contrib/logger) from 1.2.5 to 1.2.6.
- [Release notes](https://github.com/gin-contrib/logger/releases)
- [Changelog](https://github.com/gin-contrib/logger/blob/master/.goreleaser.yaml)
- [Commits](https://github.com/gin-contrib/logger/compare/v1.2.5...v1.2.6)

---
updated-dependencies:
- dependency-name: github.com/gin-contrib/logger
  dependency-version: 1.2.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-12 08:42:08 +02:00
dependabot[bot] bfbc1a5a57
build(deps): bump actions/setup-go from 4 to 5 (#577)
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 4 to 5.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-12 08:40:53 +02:00
Ben Kochie abb4350316
chore: enable dependabot (#256)
Enable dependabot to auto-update dependencies.
* Update montly to avoid too much PR noise.
* Enable updates for Go modules.
* Enable updates for GitHub Actions.
* Enable updates for NPM in /ui.

Signed-off-by: SuperQ <superq@gmail.com>
2025-06-12 08:36:31 +02:00
Marc Brooks 52825da68d
Upgrade pion modules to fix CVE (#572)
Fixes #570

## Required

|  Package | From  | To  |
|---|---|---|
| github.com/pion/logging | v0.2.2 | v0.2.3 |
| github.com/pion/wrbrtc/v4 | v4.0.0 | v4.0.16 |

## Indirect

|  Package | From  | To  |
|---|---|---|
| github.com/pion/datachannel | v1.5.9 | v1.5.10 |
| github.com/pion/dtls | v3.0.3 | v3.0.6 |
| github.com/pion/ice/v4 | v4.0.2 | v4.0.10 |
| github.com/pion/interceptor | v0.1.37 | v0.1.40 |
| github.com/pion/rtcp | v1.2.14 | v1.2.15 |
| github.com/pion/rtp | v1.8.9 | v1.8.18 |
| github.com/pion/sctp | v1.8.33 | v1.8.39 |
| github.com/pion/sdp | v3.0.9| v3.0.13 |
| github.com/pion/srtp | v3.0.4 | v3.0.5 |
| github.com/pion/turn | v4.0.0 | v4.0.2 |
2025-06-12 08:34:19 +02:00
ariedel87 9d2abd9fb0
feat(video): add video enhancement settings for saturation, brightness, and contrast (#557) 2025-06-04 18:44:37 +02:00
Siyuan Miao 52dd675e52 chore: fix eslint errors 2025-06-03 02:19:57 +02:00
John e95e30e48c
Re-add scroll blocking/throttling (#544) 2025-06-03 01:51:38 +02:00
John eaa58492ab
fix: Log spam when using a web terminal #547 (#550) 2025-06-03 01:51:30 +02:00
Marc Brooks f4bb47c544
fix(ui): Fix full-screen keyboard lock issues. (#535)
When the keyboard lock is supposed to be active (in full-screen mode), hitting the escape key (NOT long-pressing) should NOT dismiss the full-screen mode, and should send the Escape key through to the remote.

- Added awaits to the browser calls that need to complete in order.
- Cleaned up (mostly) duplicate code in the Absolute/Relative mouse handling
- Ensure we don't overrun any existing keyboard lock or pointer lock
- Release the keyboard lock when leaving full-screen
- Per standards, we need to acquire the keyboard and pointer locks before entering full-screen or the user may get multiple messages about exiting.
- Fixed all the missing/excess React dependencies.
- Moved the pointer lock bar up so it is visible.
- Somewhere along the way, the prompt to click the video when in relative-mouse-mode stopped being visible, restored it's visibility
- Fixed all the "should be readonly" warnings.
2025-06-03 01:28:35 +02:00
Alex Ballas a7693df92c
fix: the screen doesn't dim or turn off
* Fix for #531

Fix for https://github.com/jetkvm/kvm/issues/531

* typo

* Skip processing if lease hasn't changed to avoid unnecessary wake-ups

* Add comment to clarify the need to stop the tickers
2025-06-03 01:28:14 +02:00
39 changed files with 673 additions and 448 deletions

17
.github/dependabot.yml vendored Normal file
View File

@ -0,0 +1,17 @@
version: 2
updates:
- package-ecosystem: gomod
directory: /
schedule:
interval: monthly
open-pull-requests-limit: 10
- package-ecosystem: github-actions
directory: /
schedule:
interval: monthly
open-pull-requests-limit: 10
- package-ecosystem: npm
directory: /ui
open-pull-requests-limit: 10
schedule:
interval: monthly

View File

@ -23,9 +23,9 @@ jobs:
cache: "npm"
cache-dependency-path: "**/package-lock.json"
- name: Set up Golang
uses: actions/setup-go@v4
uses: actions/setup-go@v5
with:
go-version: "1.24.3"
go-version: "1.24.4"
- name: Build frontend
run: |
make frontend

View File

@ -24,9 +24,9 @@ jobs:
- name: Checkout repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Install Go
uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0
uses: actions/setup-go@19bb51245e9c80abacb2e91cc42b33fa478b8639 # v4.2.1
with:
go-version: 1.24.3
go-version: 1.24.4
- name: Create empty resource directory
run: |
mkdir -p static && touch static/.gitkeep

View File

@ -104,9 +104,9 @@ jobs:
EOF
ssh jkci "cat /tmp/device-tests.json" > device-tests.json
- name: Set up Golang
uses: actions/setup-go@v4
uses: actions/setup-go@v5
with:
go-version: "1.24.3"
go-version: "1.24.4"
- name: Golang Test Report
uses: becheran/go-testreport@v0.3.2
with:

View File

@ -2,12 +2,14 @@ BRANCH ?= $(shell git rev-parse --abbrev-ref HEAD)
BUILDDATE ?= $(shell date -u +%FT%T%z)
BUILDTS ?= $(shell date -u +%s)
REVISION ?= $(shell git rev-parse HEAD)
VERSION_DEV := 0.4.2-dev$(shell date +%Y%m%d%H%M)
VERSION := 0.4.1
VERSION_DEV := 0.4.4-dev$(shell date +%Y%m%d%H%M)
VERSION := 0.4.3
PROMETHEUS_TAG := github.com/prometheus/common/version
KVM_PKG_NAME := github.com/jetkvm/kvm
GO_BUILD_ARGS := -tags netgo
GO_RELEASE_BUILD_ARGS := -trimpath $(GO_BUILD_ARGS)
GO_LDFLAGS := \
-s -w \
-X $(PROMETHEUS_TAG).Branch=$(BRANCH) \
@ -27,7 +29,7 @@ build_dev: hash_resource
@echo "Building..."
$(GO_CMD) build \
-ldflags="$(GO_LDFLAGS) -X $(KVM_PKG_NAME).builtAppVersion=$(VERSION_DEV)" \
-trimpath \
$(GO_RELEASE_BUILD_ARGS) \
-o $(BIN_DIR)/jetkvm_app cmd/main.go
build_test2json:
@ -50,6 +52,7 @@ build_dev_test: build_test2json build_gotestsum
test_filename=$$(echo $$test_pkg_name | sed 's/\//__/g')_test; \
$(GO_CMD) test -v \
-ldflags="$(GO_LDFLAGS) -X $(KVM_PKG_NAME).builtAppVersion=$(VERSION_DEV)" \
$(GO_BUILD_ARGS) \
-c -o $(BIN_DIR)/tests/$$test_filename $$test; \
echo "runTest ./$$test_filename $$test_pkg_full_name" >> $(BIN_DIR)/tests/run_all_tests; \
done; \
@ -71,7 +74,7 @@ build_release: frontend hash_resource
@echo "Building release..."
$(GO_CMD) build \
-ldflags="$(GO_LDFLAGS) -X $(KVM_PKG_NAME).builtAppVersion=$(VERSION)" \
-trimpath \
$(GO_RELEASE_BUILD_ARGS) \
-o bin/jetkvm_app cmd/main.go
release:

View File

@ -51,34 +51,34 @@ var (
)
metricCloudConnectionEstablishedTimestamp = promauto.NewGauge(
prometheus.GaugeOpts{
Name: "jetkvm_cloud_connection_established_timestamp",
Name: "jetkvm_cloud_connection_established_timestamp_seconds",
Help: "The timestamp when the cloud connection was established",
},
)
metricConnectionLastPingTimestamp = promauto.NewGaugeVec(
prometheus.GaugeOpts{
Name: "jetkvm_connection_last_ping_timestamp",
Name: "jetkvm_connection_last_ping_timestamp_seconds",
Help: "The timestamp when the last ping response was received",
},
[]string{"type", "source"},
)
metricConnectionLastPingReceivedTimestamp = promauto.NewGaugeVec(
prometheus.GaugeOpts{
Name: "jetkvm_connection_last_ping_received_timestamp",
Name: "jetkvm_connection_last_ping_received_timestamp_seconds",
Help: "The timestamp when the last ping request was received",
},
[]string{"type", "source"},
)
metricConnectionLastPingDuration = promauto.NewGaugeVec(
prometheus.GaugeOpts{
Name: "jetkvm_connection_last_ping_duration",
Name: "jetkvm_connection_last_ping_duration_seconds",
Help: "The duration of the last ping response",
},
[]string{"type", "source"},
)
metricConnectionPingDuration = promauto.NewHistogramVec(
prometheus.HistogramOpts{
Name: "jetkvm_connection_ping_duration",
Name: "jetkvm_connection_ping_duration_seconds",
Help: "The duration of the ping response",
Buckets: []float64{
0.1, 0.5, 1, 10,
@ -88,28 +88,28 @@ var (
)
metricConnectionTotalPingSentCount = promauto.NewCounterVec(
prometheus.CounterOpts{
Name: "jetkvm_connection_total_ping_sent",
Name: "jetkvm_connection_ping_sent_total",
Help: "The total number of pings sent to the connection",
},
[]string{"type", "source"},
)
metricConnectionTotalPingReceivedCount = promauto.NewCounterVec(
prometheus.CounterOpts{
Name: "jetkvm_connection_total_ping_received",
Name: "jetkvm_connection_ping_received_total",
Help: "The total number of pings received from the connection",
},
[]string{"type", "source"},
)
metricConnectionSessionRequestCount = promauto.NewCounterVec(
prometheus.CounterOpts{
Name: "jetkvm_connection_session_total_requests",
Name: "jetkvm_connection_session_requests_total",
Help: "The total number of session requests received",
},
[]string{"type", "source"},
)
metricConnectionSessionRequestDuration = promauto.NewHistogramVec(
prometheus.HistogramOpts{
Name: "jetkvm_connection_session_request_duration",
Name: "jetkvm_connection_session_request_duration_seconds",
Help: "The duration of session requests",
Buckets: []float64{
0.1, 0.5, 1, 10,
@ -119,7 +119,7 @@ var (
)
metricConnectionLastSessionRequestTimestamp = promauto.NewGaugeVec(
prometheus.GaugeOpts{
Name: "jetkvm_connection_last_session_request_timestamp",
Name: "jetkvm_connection_last_session_request_timestamp_seconds",
Help: "The timestamp of the last session request",
},
[]string{"type", "source"},
@ -133,7 +133,7 @@ var (
)
metricCloudConnectionFailureCount = promauto.NewCounter(
prometheus.CounterOpts{
Name: "jetkvm_cloud_connection_failure_count",
Name: "jetkvm_cloud_connection_failure_total",
Help: "The number of times the cloud connection has failed",
},
)

View File

@ -174,7 +174,7 @@ cd "${REMOTE_PATH}"
chmod +x jetkvm_app_debug
# Run the application in the background
PION_LOG_TRACE=${LOG_TRACE_SCOPES} ./jetkvm_app_debug
PION_LOG_TRACE=${LOG_TRACE_SCOPES} GODEBUG=netdns=1 ./jetkvm_app_debug
EOF
echo "Deployment complete."

View File

@ -339,10 +339,18 @@ func startBacklightTickers() {
return
}
if dimTicker == nil && config.DisplayDimAfterSec != 0 {
// Stop existing tickers to prevent multiple active instances on repeated calls
if dimTicker != nil {
dimTicker.Stop()
}
if offTicker != nil {
offTicker.Stop()
}
if config.DisplayDimAfterSec != 0 {
displayLogger.Info().Msg("dim_ticker has started")
dimTicker = time.NewTicker(time.Duration(config.DisplayDimAfterSec) * time.Second)
defer dimTicker.Stop()
go func() {
for { //nolint:staticcheck
@ -354,10 +362,9 @@ func startBacklightTickers() {
}()
}
if offTicker == nil && config.DisplayOffAfterSec != 0 {
if config.DisplayOffAfterSec != 0 {
displayLogger.Info().Msg("off_ticker has started")
offTicker = time.NewTicker(time.Duration(config.DisplayOffAfterSec) * time.Second)
defer offTicker.Stop()
go func() {
for { //nolint:staticcheck

56
go.mod
View File

@ -5,33 +5,33 @@ go 1.23.4
toolchain go1.24.3
require (
github.com/Masterminds/semver/v3 v3.3.0
github.com/beevik/ntp v1.3.1
github.com/Masterminds/semver/v3 v3.3.1
github.com/beevik/ntp v1.4.3
github.com/coder/websocket v1.8.13
github.com/coreos/go-oidc/v3 v3.11.0
github.com/creack/pty v1.1.23
github.com/fsnotify/fsnotify v1.9.0
github.com/gin-contrib/logger v1.2.5
github.com/gin-gonic/gin v1.10.0
github.com/gin-contrib/logger v1.2.6
github.com/gin-gonic/gin v1.10.1
github.com/google/uuid v1.6.0
github.com/guregu/null/v6 v6.0.0
github.com/gwatts/rootcerts v0.0.0-20240401182218-3ab9db955caf
github.com/hanwen/go-fuse/v2 v2.5.1
github.com/pion/logging v0.2.2
github.com/hanwen/go-fuse/v2 v2.8.0
github.com/pion/logging v0.2.3
github.com/pion/mdns/v2 v2.0.7
github.com/pion/webrtc/v4 v4.0.0
github.com/pion/webrtc/v4 v4.0.16
github.com/pojntfx/go-nbd v0.3.2
github.com/prometheus/client_golang v1.21.0
github.com/prometheus/client_golang v1.22.0
github.com/prometheus/common v0.62.0
github.com/prometheus/procfs v0.15.1
github.com/prometheus/procfs v0.16.1
github.com/psanford/httpreadat v0.1.0
github.com/rs/zerolog v1.34.0
github.com/sourcegraph/tf-dag v0.2.2-0.20250131204052-3e8ff1477b4f
github.com/stretchr/testify v1.10.0
github.com/vishvananda/netlink v1.3.0
go.bug.st/serial v1.6.2
golang.org/x/crypto v0.38.0
golang.org/x/net v0.40.0
golang.org/x/crypto v0.39.0
golang.org/x/net v0.41.0
golang.org/x/sys v0.33.0
)
@ -45,16 +45,14 @@ require (
github.com/cloudwego/base64x v0.1.5 // indirect
github.com/creack/goselect v0.1.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
github.com/gin-contrib/sse v1.0.0 // indirect
github.com/go-jose/go-jose/v4 v4.0.2 // indirect
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
github.com/go-jose/go-jose/v4 v4.0.5 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.26.0 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.17.11 // indirect
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
@ -62,21 +60,21 @@ require (
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/pilebones/go-udev v0.9.0 // indirect
github.com/pion/datachannel v1.5.9 // indirect
github.com/pion/dtls/v3 v3.0.3 // indirect
github.com/pion/ice/v4 v4.0.2 // indirect
github.com/pion/interceptor v0.1.37 // indirect
github.com/pion/datachannel v1.5.10 // indirect
github.com/pion/dtls/v3 v3.0.6 // indirect
github.com/pion/ice/v4 v4.0.10 // indirect
github.com/pion/interceptor v0.1.40 // indirect
github.com/pion/randutil v0.1.0 // indirect
github.com/pion/rtcp v1.2.14 // indirect
github.com/pion/rtp v1.8.9 // indirect
github.com/pion/sctp v1.8.33 // indirect
github.com/pion/sdp/v3 v3.0.9 // indirect
github.com/pion/srtp/v3 v3.0.4 // indirect
github.com/pion/rtcp v1.2.15 // indirect
github.com/pion/rtp v1.8.18 // indirect
github.com/pion/sctp v1.8.39 // indirect
github.com/pion/sdp/v3 v3.0.13 // indirect
github.com/pion/srtp/v3 v3.0.5 // indirect
github.com/pion/stun/v3 v3.0.0 // indirect
github.com/pion/transport/v3 v3.0.7 // indirect
github.com/pion/turn/v4 v4.0.0 // indirect
github.com/pion/turn/v4 v4.0.2 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/rogpeppe/go-internal v1.11.0 // indirect
@ -84,9 +82,9 @@ require (
github.com/ugorji/go/codec v1.2.12 // indirect
github.com/vishvananda/netns v0.0.4 // indirect
github.com/wlynxg/anet v0.0.5 // indirect
golang.org/x/arch v0.15.0 // indirect
golang.org/x/arch v0.17.0 // indirect
golang.org/x/oauth2 v0.24.0 // indirect
golang.org/x/text v0.25.0 // indirect
golang.org/x/text v0.26.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

122
go.sum
View File

@ -1,7 +1,7 @@
github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0=
github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/beevik/ntp v1.3.1 h1:Y/srlT8L1yQr58kyPWFPZIxRL8ttx2SRIpVYJqZIlAM=
github.com/beevik/ntp v1.3.1/go.mod h1:fT6PylBq86Tsq23ZMEe47b7QQrZfYBFPnpzt0a9kJxw=
github.com/Masterminds/semver/v3 v3.3.1 h1:QtNSWtVZ3nBfk8mAOu/B6v7FMJ+NHTIgUPi7rj+4nv4=
github.com/Masterminds/semver/v3 v3.3.1/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/beevik/ntp v1.4.3 h1:PlbTvE5NNy4QHmA4Mg57n7mcFTmr1W1j3gcK7L1lqho=
github.com/beevik/ntp v1.4.3/go.mod h1:Unr8Zg+2dRn7d8bHFuehIMSvvUYssHMxW3Q5Nx4RW5Q=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ=
@ -30,16 +30,16 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
github.com/gin-contrib/logger v1.2.5 h1:qVQI4omayQecuN4zX9ZZnsOq7w9J/ZLds3J/FMn8ypM=
github.com/gin-contrib/logger v1.2.5/go.mod h1:/bj+vNMuA2xOEQ1aRHoJ1m9+uyaaXIAxQTvM2llsc6I=
github.com/gin-contrib/sse v1.0.0 h1:y3bT1mUWUxDpW4JLQg/HnTqV4rozuW4tC9eFKTxYI9E=
github.com/gin-contrib/sse v1.0.0/go.mod h1:zNuFdwarAygJBht0NTKiSi3jRf6RbqeILZ9Sp6Slhe0=
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
github.com/go-jose/go-jose/v4 v4.0.2 h1:R3l3kkBds16bO7ZFAEEcofK0MkrAJt3jlJznWZG0nvk=
github.com/go-jose/go-jose/v4 v4.0.2/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY=
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
github.com/gin-contrib/logger v1.2.6 h1:EPolruKUTzNXMVBD9LuAFQmRjTs7AH7yKGuXgYqrKWc=
github.com/gin-contrib/logger v1.2.6/go.mod h1:7niPrd7F0Nscw/zvgz8RiGJxSdbKM2yfQNy8xCHcm64=
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-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE=
github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
@ -60,12 +60,12 @@ 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/gwatts/rootcerts v0.0.0-20240401182218-3ab9db955caf h1:JO6ISZIvEUitto5zjQ3/VEnDM5rPbqIFuOhS0U0ByeA=
github.com/gwatts/rootcerts v0.0.0-20240401182218-3ab9db955caf/go.mod h1:5Kt9XkWvkGi2OHOq0QsGxebHmhCcqJ8KCbNg/a6+n+g=
github.com/hanwen/go-fuse/v2 v2.5.1 h1:OQBE8zVemSocRxA4OaFJbjJ5hlpCmIWbGr7r0M4uoQQ=
github.com/hanwen/go-fuse/v2 v2.5.1/go.mod h1:xKwi1cF7nXAOBCXujD5ie0ZKsxc8GGSA1rlMJc+8IJs=
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/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
@ -77,7 +77,6 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
@ -89,8 +88,8 @@ 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.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/moby/sys/mountinfo v0.6.2 h1:BzJjoreD5BMFNmD9Rus6gdd1pLuecOFPt8wC+Vygl78=
github.com/moby/sys/mountinfo v0.6.2/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI=
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-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@ -98,53 +97,53 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pilebones/go-udev v0.9.0 h1:N1uEO/SxUwtIctc0WLU0t69JeBxIYEYnj8lT/Nabl9Q=
github.com/pilebones/go-udev v0.9.0/go.mod h1:T2eI2tUSK0hA2WS5QLjXJUfQkluZQu+18Cqvem3CaXI=
github.com/pion/datachannel v1.5.9 h1:LpIWAOYPyDrXtU+BW7X0Yt/vGtYxtXQ8ql7dFfYUVZA=
github.com/pion/datachannel v1.5.9/go.mod h1:kDUuk4CU4Uxp82NH4LQZbISULkX/HtzKa4P7ldf9izE=
github.com/pion/dtls/v3 v3.0.3 h1:j5ajZbQwff7Z8k3pE3S+rQ4STvKvXUdKsi/07ka+OWM=
github.com/pion/dtls/v3 v3.0.3/go.mod h1:weOTUyIV4z0bQaVzKe8kpaP17+us3yAuiQsEAG1STMU=
github.com/pion/ice/v4 v4.0.2 h1:1JhBRX8iQLi0+TfcavTjPjI6GO41MFn4CeTBX+Y9h5s=
github.com/pion/ice/v4 v4.0.2/go.mod h1:DCdqyzgtsDNYN6/3U8044j3U7qsJ9KFJC92VnOWHvXg=
github.com/pion/interceptor v0.1.37 h1:aRA8Zpab/wE7/c0O3fh1PqY0AJI3fCSEM5lRWJVorwI=
github.com/pion/interceptor v0.1.37/go.mod h1:JzxbJ4umVTlZAf+/utHzNesY8tmRkM2lVmkS82TTj8Y=
github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
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/dtls/v3 v3.0.6 h1:7Hkd8WhAJNbRgq9RgdNh1aaWlZlGpYTzdqjy9x9sK2E=
github.com/pion/dtls/v3 v3.0.6/go.mod h1:iJxNQ3Uhn1NZWOMWlLxEEHAN5yX7GyPvvKw04v9bzYU=
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/interceptor v0.1.40 h1:e0BjnPcGpr2CFQgKhrQisBU7V3GXK6wrfYrGYaU6Jq4=
github.com/pion/interceptor v0.1.40/go.mod h1:Z6kqH7M/FYirg3frjGJ21VLSRJGBXB/KqaTIrdqnOic=
github.com/pion/logging v0.2.3 h1:gHuf0zpoh1GW67Nr6Gj4cv5Z9ZscU7g/EaoC/Ke/igI=
github.com/pion/logging v0.2.3/go.mod h1:z8YfknkquMe1csOrxK5kc+5/ZPAzMxbKLX5aXpbpC90=
github.com/pion/mdns/v2 v2.0.7 h1:c9kM8ewCgjslaAmicYMFQIde2H9/lrZpjBkN8VwoVtM=
github.com/pion/mdns/v2 v2.0.7/go.mod h1:vAdSYNAT0Jy3Ru0zl2YiW3Rm/fJCwIeM0nToenfOJKA=
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/rtcp v1.2.14 h1:KCkGV3vJ+4DAJmvP0vaQShsb0xkRfWkO540Gy102KyE=
github.com/pion/rtcp v1.2.14/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4=
github.com/pion/rtp v1.8.9 h1:E2HX740TZKaqdcPmf4pw6ZZuG8u5RlMMt+l3dxeu6Wk=
github.com/pion/rtp v1.8.9/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU=
github.com/pion/sctp v1.8.33 h1:dSE4wX6uTJBcNm8+YlMg7lw1wqyKHggsP5uKbdj+NZw=
github.com/pion/sctp v1.8.33/go.mod h1:beTnqSzewI53KWoG3nqB282oDMGrhNxBdb+JZnkCwRM=
github.com/pion/sdp/v3 v3.0.9 h1:pX++dCHoHUwq43kuwf3PyJfHlwIj4hXA7Vrifiq0IJY=
github.com/pion/sdp/v3 v3.0.9/go.mod h1:B5xmvENq5IXJimIO4zfp6LAe1fD9N+kFv+V/1lOdz8M=
github.com/pion/srtp/v3 v3.0.4 h1:2Z6vDVxzrX3UHEgrUyIGM4rRouoC7v+NiF1IHtp9B5M=
github.com/pion/srtp/v3 v3.0.4/go.mod h1:1Jx3FwDoxpRaTh1oRV8A/6G1BnFL+QI82eK4ms8EEJQ=
github.com/pion/rtcp v1.2.15 h1:LZQi2JbdipLOj4eBjK4wlVoQWfrZbh3Q6eHtWtJBZBo=
github.com/pion/rtcp v1.2.15/go.mod h1:jlGuAjHMEXwMUHK78RgX0UmEJFV4zUKOFHR7OP+D3D0=
github.com/pion/rtp v1.8.18 h1:yEAb4+4a8nkPCecWzQB6V/uEU18X1lQCGAQCjP+pyvU=
github.com/pion/rtp v1.8.18/go.mod h1:bAu2UFKScgzyFqvUKmbvzSdPr+NGbZtv6UB2hesqXBk=
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/sdp/v3 v3.0.13 h1:uN3SS2b+QDZnWXgdr69SM8KB4EbcnPnPf2Laxhty/l4=
github.com/pion/sdp/v3 v3.0.13/go.mod h1:88GMahN5xnScv1hIMTqLdu/cOcUkj6a9ytbncwMCq2E=
github.com/pion/srtp/v3 v3.0.5 h1:8XLB6Dt3QXkMkRFpoqC3314BemkpMQK2mZeJc4pUKqo=
github.com/pion/srtp/v3 v3.0.5/go.mod h1:r1G7y5r1scZRLe2QJI/is+/O83W2d+JoEsuIexpw+uM=
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/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0=
github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo=
github.com/pion/turn/v4 v4.0.0 h1:qxplo3Rxa9Yg1xXDxxH8xaqcyGUtbHYw4QSCvmFWvhM=
github.com/pion/turn/v4 v4.0.0/go.mod h1:MuPDkm15nYSklKpN8vWJ9W2M0PlyQZqYt1McGuxG7mA=
github.com/pion/webrtc/v4 v4.0.0 h1:x8ec7uJQPP3D1iI8ojPAiTOylPI7Fa7QgqZrhpLyqZ8=
github.com/pion/webrtc/v4 v4.0.0/go.mod h1:SfNn8CcFxR6OUVjLXVslAQ3a3994JhyE3Hw1jAuqEto=
github.com/pion/turn/v4 v4.0.2 h1:ZqgQ3+MjP32ug30xAbD6Mn+/K4Sxi3SdNOTFf+7mpps=
github.com/pion/turn/v4 v4.0.2/go.mod h1:pMMKP/ieNAG/fN5cZiN4SDuyKsXtNTr0ccN7IToA1zs=
github.com/pion/webrtc/v4 v4.0.16 h1:5f8QMVIbNvJr2mPRGi2QamkPa/LVUB6NWolOCwphKHA=
github.com/pion/webrtc/v4 v4.0.16/go.mod h1:C3uTCPzVafUA0eUzru9f47OgNt3nEO7ZJ6zNY6VSJno=
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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.21.0 h1:DIsaGmiaBkSangBgMtWdNfxbMNdku5IK6iNhrEqWvdA=
github.com/prometheus/client_golang v1.21.0/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg=
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
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/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
@ -157,14 +156,11 @@ github.com/sourcegraph/tf-dag v0.2.2-0.20250131204052-3e8ff1477b4f/go.mod h1:pzr
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/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.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
@ -179,16 +175,14 @@ github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
go.bug.st/serial v1.6.2 h1:kn9LRX3sdm+WxWKufMlIRndwGfPWsH1/9lCWXQCasq8=
go.bug.st/serial v1.6.2/go.mod h1:UABfsluHAiaNI+La2iESysd9Vetq7VRdpxvjx7CmmOE=
golang.org/x/arch v0.15.0 h1:QtOrQd0bTUnhNVNndMpLHNWrDmYzZ2KDqSrEymqInZw=
golang.org/x/arch v0.15.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE=
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
golang.org/x/arch v0.17.0 h1:4O3dfLzd+lQewptAHqjewQZQDyEdejz3VwgeYwkZneU=
golang.org/x/arch v0.17.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE=
golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@ -196,8 +190,8 @@ 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.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View File

@ -14,44 +14,44 @@ var (
)
metricTimeSyncCount = promauto.NewCounter(
prometheus.CounterOpts{
Name: "jetkvm_timesync_count",
Name: "jetkvm_timesync_total",
Help: "The number of times the timesync has been run",
},
)
metricTimeSyncSuccessCount = promauto.NewCounter(
prometheus.CounterOpts{
Name: "jetkvm_timesync_success_count",
Name: "jetkvm_timesync_success_total",
Help: "The number of times the timesync has been successful",
},
)
metricRTCUpdateCount = promauto.NewCounter( //nolint:unused
prometheus.CounterOpts{
Name: "jetkvm_timesync_rtc_update_count",
Name: "jetkvm_timesync_rtc_update_total",
Help: "The number of times the RTC has been updated",
},
)
metricNtpTotalSuccessCount = promauto.NewCounter(
prometheus.CounterOpts{
Name: "jetkvm_timesync_ntp_total_success_count",
Name: "jetkvm_timesync_ntp_total_success_total",
Help: "The total number of successful NTP requests",
},
)
metricNtpTotalRequestCount = promauto.NewCounter(
prometheus.CounterOpts{
Name: "jetkvm_timesync_ntp_total_request_count",
Name: "jetkvm_timesync_ntp_total_request_total",
Help: "The total number of NTP requests sent",
},
)
metricNtpSuccessCount = promauto.NewCounterVec(
prometheus.CounterOpts{
Name: "jetkvm_timesync_ntp_success_count",
Name: "jetkvm_timesync_ntp_success_total",
Help: "The number of successful NTP requests",
},
[]string{"url"},
)
metricNtpRequestCount = promauto.NewCounterVec(
prometheus.CounterOpts{
Name: "jetkvm_timesync_ntp_request_count",
Name: "jetkvm_timesync_ntp_request_total",
Help: "The number of NTP requests sent to the server",
},
[]string{"url"},
@ -83,39 +83,39 @@ var (
metricHttpTotalSuccessCount = promauto.NewCounter(
prometheus.CounterOpts{
Name: "jetkvm_timesync_http_total_success_count",
Name: "jetkvm_timesync_http_total_success_total",
Help: "The total number of successful HTTP requests",
},
)
metricHttpTotalRequestCount = promauto.NewCounter(
prometheus.CounterOpts{
Name: "jetkvm_timesync_http_total_request_count",
Name: "jetkvm_timesync_http_total_request_total",
Help: "The total number of HTTP requests sent",
},
)
metricHttpTotalCancelCount = promauto.NewCounter(
prometheus.CounterOpts{
Name: "jetkvm_timesync_http_total_cancel_count",
Name: "jetkvm_timesync_http_total_cancel_total",
Help: "The total number of HTTP requests cancelled",
},
)
metricHttpSuccessCount = promauto.NewCounterVec(
prometheus.CounterOpts{
Name: "jetkvm_timesync_http_success_count",
Name: "jetkvm_timesync_http_success_total",
Help: "The number of successful HTTP requests",
},
[]string{"url"},
)
metricHttpRequestCount = promauto.NewCounterVec(
prometheus.CounterOpts{
Name: "jetkvm_timesync_http_request_count",
Name: "jetkvm_timesync_http_request_total",
Help: "The number of HTTP requests sent to the server",
},
[]string{"url"},
)
metricHttpCancelCount = promauto.NewCounterVec(
prometheus.CounterOpts{
Name: "jetkvm_timesync_http_cancel_count",
Name: "jetkvm_timesync_http_cancel_total",
Help: "The number of HTTP requests cancelled",
},
[]string{"url"},

View File

@ -5,6 +5,7 @@ import (
"fmt"
"os"
"path/filepath"
"reflect"
"time"
"github.com/fsnotify/fsnotify"
@ -149,6 +150,12 @@ func (c *DHCPClient) loadLeaseFile() error {
}
isFirstLoad := c.lease == nil
// Skip processing if lease hasn't changed to avoid unnecessary wake-ups.
if reflect.DeepEqual(c.lease, lease) {
return nil
}
c.lease = lease
if lease.IPAddress == nil {

View File

@ -1,6 +1,7 @@
package kvm
import (
"bytes"
"encoding/json"
"io"
"os"
@ -55,18 +56,23 @@ func handleTerminalChannel(d *webrtc.DataChannel) {
return
}
if msg.IsString {
var size TerminalSize
err := json.Unmarshal([]byte(msg.Data), &size)
if err == nil {
err = pty.Setsize(ptmx, &pty.Winsize{
Rows: uint16(size.Rows),
Cols: uint16(size.Cols),
})
maybeJson := bytes.TrimSpace(msg.Data)
// Cheap check to see if this resembles JSON
if len(maybeJson) > 1 && maybeJson[0] == '{' && maybeJson[len(maybeJson)-1] == '}' {
var size TerminalSize
err := json.Unmarshal(maybeJson, &size)
if err == nil {
return
err = pty.Setsize(ptmx, &pty.Winsize{
Rows: uint16(size.Rows),
Cols: uint16(size.Cols),
})
if err == nil {
scopedLogger.Info().Int("rows", size.Rows).Int("cols", size.Cols).Msg("Set terminal size")
return
}
}
scopedLogger.Warn().Err(err).Msg("Failed to parse terminal size")
}
scopedLogger.Warn().Err(err).Msg("Failed to parse terminal size")
}
_, err := ptmx.Write(msg.Data)
if err != nil {

View File

@ -67,19 +67,19 @@ function Terminal({
}) {
const enableTerminal = useUiStore(state => state.terminalType == type);
const setTerminalType = useUiStore(state => state.setTerminalType);
const setDisableKeyboardFocusTrap = useUiStore(state => state.setDisableVideoFocusTrap);
const setDisableVideoFocusTrap = useUiStore(state => state.setDisableVideoFocusTrap);
const { instance, ref } = useXTerm({ options: TERMINAL_CONFIG });
useEffect(() => {
setTimeout(() => {
setDisableKeyboardFocusTrap(enableTerminal);
setDisableVideoFocusTrap(enableTerminal);
}, 500);
return () => {
setDisableKeyboardFocusTrap(false);
setDisableVideoFocusTrap(false);
};
}, [ref, instance, enableTerminal, setDisableKeyboardFocusTrap, type]);
}, [enableTerminal, setDisableVideoFocusTrap]);
const readyState = dataChannel.readyState;
useEffect(() => {
@ -116,7 +116,7 @@ function Terminal({
const { domEvent } = e;
if (domEvent.key === "Escape") {
setTerminalType("none");
setDisableKeyboardFocusTrap(false);
setDisableVideoFocusTrap(false);
domEvent.preventDefault();
}
});
@ -131,7 +131,7 @@ function Terminal({
onDataHandler.dispose();
onKeyHandler.dispose();
};
}, [instance, dataChannel, readyState, setDisableKeyboardFocusTrap, setTerminalType]);
}, [dataChannel, instance, readyState, setDisableVideoFocusTrap, setTerminalType]);
useEffect(() => {
if (!instance) return;
@ -158,7 +158,7 @@ function Terminal({
return () => {
window.removeEventListener("resize", handleResize);
};
}, [ref, instance]);
}, [instance]);
return (
<div

View File

@ -10,7 +10,7 @@ import LoadingSpinner from "@components/LoadingSpinner";
import Card, { GridCard } from "@components/Card";
interface OverlayContentProps {
children: React.ReactNode;
readonly children: React.ReactNode;
}
function OverlayContent({ children }: OverlayContentProps) {
return (
@ -23,7 +23,7 @@ function OverlayContent({ children }: OverlayContentProps) {
}
interface LoadingOverlayProps {
show: boolean;
readonly show: boolean;
}
export function LoadingVideoOverlay({ show }: LoadingOverlayProps) {
@ -57,8 +57,8 @@ export function LoadingVideoOverlay({ show }: LoadingOverlayProps) {
}
interface LoadingConnectionOverlayProps {
show: boolean;
text: string;
readonly show: boolean;
readonly text: string;
}
export function LoadingConnectionOverlay({ show, text }: LoadingConnectionOverlayProps) {
return (
@ -91,8 +91,8 @@ export function LoadingConnectionOverlay({ show, text }: LoadingConnectionOverla
}
interface ConnectionErrorOverlayProps {
show: boolean;
setupPeerConnection: () => Promise<void>;
readonly show: boolean;
readonly setupPeerConnection: () => Promise<void>;
}
export function ConnectionFailedOverlay({
@ -153,7 +153,7 @@ export function ConnectionFailedOverlay({
}
interface PeerConnectionDisconnectedOverlay {
show: boolean;
readonly show: boolean;
}
export function PeerConnectionDisconnectedOverlay({
@ -207,8 +207,8 @@ export function PeerConnectionDisconnectedOverlay({
}
interface HDMIErrorOverlayProps {
show: boolean;
hdmiState: string;
readonly show: boolean;
readonly hdmiState: string;
}
export function HDMIErrorOverlay({ show, hdmiState }: HDMIErrorOverlayProps) {
@ -310,8 +310,8 @@ export function HDMIErrorOverlay({ show, hdmiState }: HDMIErrorOverlayProps) {
}
interface NoAutoplayPermissionsOverlayProps {
show: boolean;
onPlayClick: () => void;
readonly show: boolean;
readonly onPlayClick: () => void;
}
export function NoAutoplayPermissionsOverlay({
@ -361,7 +361,7 @@ export function NoAutoplayPermissionsOverlay({
}
interface PointerLockBarProps {
show: boolean;
readonly show: boolean;
}
export function PointerLockBar({ show }: PointerLockBarProps) {
@ -369,10 +369,10 @@ export function PointerLockBar({ show }: PointerLockBarProps) {
<AnimatePresence mode="wait">
{show ? (
<motion.div
className="absolute -top-[36px] left-0 right-0 z-20 bg-white"
initial={{ y: 20, opacity: 0, zIndex: 0 }}
animate={{ opacity: 1, y: 0 }}
exit={{ y: 43, zIndex: 0 }}
className="flex w-full items-center justify-between bg-transparent"
initial={{ opacity: 0, zIndex: 0 }}
animate={{ opacity: 1, zIndex: 20 }}
exit={{ opacity: 0, zIndex: 0 }}
transition={{ duration: 0.5, ease: "easeInOut", delay: 0.5 }}
>
<div>

View File

@ -1,23 +1,22 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useResizeObserver } from "usehooks-ts";
import VirtualKeyboard from "@components/VirtualKeyboard";
import Actionbar from "@components/ActionBar";
import MacroBar from "@/components/MacroBar";
import InfoBar from "@components/InfoBar";
import notifications from "@/notifications";
import useKeyboard from "@/hooks/useKeyboard";
import { useJsonRpc } from "@/hooks/useJsonRpc";
import { cx } from "@/cva.config";
import { keys, modifiers } from "@/keyboardMappings";
import {
useHidStore,
useMouseStore,
useRTCStore,
useSettingsStore,
useUiStore,
useVideoStore,
} from "@/hooks/stores";
import { keys, modifiers } from "@/keyboardMappings";
import { cx } from "@/cva.config";
import VirtualKeyboard from "@components/VirtualKeyboard";
import Actionbar from "@components/ActionBar";
import MacroBar from "@/components/MacroBar";
import InfoBar from "@components/InfoBar";
import useKeyboard from "@/hooks/useKeyboard";
import { useJsonRpc } from "@/hooks/useJsonRpc";
import notifications from "@/notifications";
import {
HDMIErrorOverlay,
@ -47,6 +46,11 @@ export default function WebRTCVideo() {
clientHeight: videoClientHeight,
} = useVideoStore();
// Video enhancement settings
const videoSaturation = useSettingsStore(state => state.videoSaturation);
const videoBrightness = useSettingsStore(state => state.videoBrightness);
const videoContrast = useSettingsStore(state => state.videoContrast);
// HID related states
const keyboardLedStateSyncAvailable = useHidStore(state => state.keyboardLedStateSyncAvailable);
const keyboardLedSync = useSettingsStore(state => state.keyboardLedSync);
@ -67,8 +71,9 @@ export default function WebRTCVideo() {
const hdmiError = ["no_lock", "no_signal", "out_of_range"].includes(hdmiState);
const isVideoLoading = !isPlaying;
const [blockWheelEvent, setBlockWheelEvent] = useState(false);
// Misc states and hooks
const disableVideoFocusTrap = useUiStore(state => state.disableVideoFocusTrap);
const [send] = useJsonRpc();
// Video-related
@ -106,8 +111,9 @@ export default function WebRTCVideo() {
);
// Pointer lock and keyboard lock related
const isPointerLockPossible = window.location.protocol === "https:";
const isPointerLockPossible = window.location.protocol === "https:" || window.location.hostname === "localhost";
const isFullscreenEnabled = document.fullscreenEnabled;
const checkNavigatorPermissions = useCallback(async (permissionName: string) => {
const name = permissionName as PermissionName;
const { state } = await navigator.permissions.query({ name });
@ -115,23 +121,47 @@ export default function WebRTCVideo() {
}, []);
const requestPointerLock = useCallback(async () => {
if (document.pointerLockElement) return;
if (!isPointerLockPossible
|| videoElm.current === null
|| document.pointerLockElement) return;
const isPointerLockGranted = await checkNavigatorPermissions("pointer-lock");
if (isPointerLockGranted && settings.mouseMode === "relative") {
videoElm.current?.requestPointerLock();
await videoElm.current.requestPointerLock();
}
}, [checkNavigatorPermissions, settings.mouseMode]);
}, [checkNavigatorPermissions, isPointerLockPossible, settings.mouseMode]);
const requestKeyboardLock = useCallback(async () => {
if (videoElm.current === null) return;
const isKeyboardLockGranted = await checkNavigatorPermissions("keyboard-lock");
if (isKeyboardLockGranted) {
if ("keyboard" in navigator) {
// @ts-expect-error - keyboard lock is not supported in all browsers
await navigator.keyboard.lock();
}
}
}, [checkNavigatorPermissions]);
const releaseKeyboardLock = useCallback(async () => {
if (videoElm.current === null || document.fullscreenElement !== videoElm.current) return;
if ("keyboard" in navigator) {
// @ts-expect-error - keyboard unlock is not supported in all browsers
await navigator.keyboard.unlock();
}
}, []);
useEffect(() => {
if (!isPointerLockPossible || !videoElm.current) return;
const handlePointerLockChange = () => {
if (document.pointerLockElement) {
notifications.success("Pointer lock Enabled, hold escape to exit");
notifications.success("Pointer lock Enabled, press escape to unlock");
setIsPointerLockActive(true);
} else {
notifications.success("Pointer lock disabled");
notifications.success("Pointer lock Disabled");
setIsPointerLockActive(false);
}
};
@ -144,27 +174,39 @@ export default function WebRTCVideo() {
return () => {
abortController.abort();
};
}, [isPointerLockPossible, videoElm]);
}, [isPointerLockPossible]);
const requestFullscreen = useCallback(async () => {
videoElm.current?.requestFullscreen({
navigationUI: "show",
});
if (!isFullscreenEnabled || !videoElm.current) return;
// we do not care about pointer lock if it's for fullscreen
// per https://wicg.github.io/keyboard-lock/#system-key-press-handler
// If keyboard lock is activated after fullscreen is already in effect, then the user my
// see multiple messages about how to exit fullscreen. For this reason, we recommend that
// developers call lock() before they enter fullscreen:
await requestKeyboardLock();
await requestPointerLock();
const isKeyboardLockGranted = await checkNavigatorPermissions("keyboard-lock");
if (isKeyboardLockGranted) {
if ("keyboard" in navigator) {
// @ts-expect-error - keyboard lock is not supported in all browsers
await navigator.keyboard.lock();
await videoElm.current.requestFullscreen({
navigationUI: "show",
});
}, [isFullscreenEnabled, requestKeyboardLock, requestPointerLock]);
// setup to release the keyboard lock anytime the fullscreen ends
useEffect(() => {
if (!videoElm.current) return;
const handleFullscreenChange = () => {
if (!document.fullscreenElement) {
releaseKeyboardLock();
}
}
}, [requestPointerLock, checkNavigatorPermissions]);
};
document.addEventListener("fullscreenchange ", handleFullscreenChange);
}, [releaseKeyboardLock]);
// Mouse-related
const calcDelta = (pos: number) => (Math.abs(pos) < 10 ? pos * 2 : pos);
const sendRelMouseMovement = useCallback(
(x: number, y: number, buttons: number) => {
if (settings.mouseMode !== "relative") return;
@ -179,18 +221,13 @@ export default function WebRTCVideo() {
const relMouseMoveHandler = useCallback(
(e: MouseEvent) => {
if (settings.mouseMode !== "relative") return;
if (isPointerLockActive === false && isPointerLockPossible === true) return;
if (isPointerLockActive === false && isPointerLockPossible) return;
// Send mouse movement
const { buttons } = e;
sendRelMouseMovement(e.movementX, e.movementY, buttons);
},
[
isPointerLockActive,
isPointerLockPossible,
sendRelMouseMovement,
settings.mouseMode,
],
[isPointerLockActive, isPointerLockPossible, sendRelMouseMovement, settings.mouseMode],
);
const sendAbsMouseMovement = useCallback(
@ -244,18 +281,16 @@ export default function WebRTCVideo() {
const { buttons } = e;
sendAbsMouseMovement(x, y, buttons);
},
[
sendAbsMouseMovement,
videoClientHeight,
videoClientWidth,
videoWidth,
videoHeight,
settings.mouseMode,
],
[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;
@ -263,7 +298,7 @@ export default function WebRTCVideo() {
const accelScrollValue = e.deltaY / 100;
// Calculate the no accel scroll value
const noAccelScrollValue = e.deltaY > 0 ? 1 : e.deltaY < 0 ? -1 : 0;
const noAccelScrollValue = Math.sign(e.deltaY);
// Get scroll value
const scrollValue = isAccel ? accelScrollValue : noAccelScrollValue;
@ -275,8 +310,14 @@ export default function WebRTCVideo() {
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],
[send, blockWheelEvent, settings],
);
const resetMousePosition = useCallback(() => {
@ -356,13 +397,6 @@ export default function WebRTCVideo() {
let code = e.code;
const key = e.key;
// if (document.activeElement?.id !== "videoFocusTrap") {
// console.log("KEYUP: Not focusing on the video", document.activeElement);
// return;
// }
// console.log(document.activeElement);
if (!isKeyboardLedManagedByHost) {
setIsNumLockActive(e.getModifierState("NumLock"));
setIsCapsLockActive(e.getModifierState("CapsLock"));
@ -440,13 +474,15 @@ export default function WebRTCVideo() {
);
const videoKeyUpHandler = useCallback((e: KeyboardEvent) => {
if (!videoElm.current) return;
// In fullscreen mode in chrome & safari, the space key is used to pause/play the video
// there is no way to prevent this, so we need to simply force play the video when it's paused.
// Fix only works in chrome based browsers.
if (e.code === "Space") {
if (videoElm.current?.paused == true) {
if (videoElm.current.paused) {
console.log("Force playing video");
videoElm.current?.play();
videoElm.current.play();
}
}
}, []);
@ -455,7 +491,6 @@ export default function WebRTCVideo() {
(mediaStream: MediaStream) => {
if (!videoElm.current) return;
const videoElmRefValue = videoElm.current;
// console.log("Adding stream to video element", videoElmRefValue);
videoElmRefValue.srcObject = mediaStream;
updateVideoSizeStore(videoElmRefValue);
},
@ -471,7 +506,6 @@ export default function WebRTCVideo() {
peerConnection.addEventListener(
"track",
(e: RTCTrackEvent) => {
// console.log("Adding stream to video element");
addStreamToVideoElm(e.streams[0]);
},
{ signal },
@ -487,7 +521,6 @@ export default function WebRTCVideo() {
useEffect(
function updateVideoStream() {
if (!mediaStream) return;
console.log("Updating video stream from mediaStream");
// We set the as early as possible
addStreamToVideoElm(mediaStream);
},
@ -509,9 +542,6 @@ export default function WebRTCVideo() {
document.addEventListener("keydown", keyDownHandler, { signal });
document.addEventListener("keyup", keyUpHandler, { signal });
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
window.clearKeys = () => sendKeyboardEvent([], []);
window.addEventListener("blur", resetKeyboardState, { signal });
document.addEventListener("visibilitychange", resetKeyboardState, { signal });
@ -519,7 +549,7 @@ export default function WebRTCVideo() {
abortController.abort();
};
},
[keyDownHandler, keyUpHandler, resetKeyboardState, sendKeyboardEvent],
[keyDownHandler, keyUpHandler, resetKeyboardState],
);
// Setup Video Event Listeners
@ -541,38 +571,42 @@ export default function WebRTCVideo() {
abortController.abort();
};
},
[
absMouseMoveHandler,
resetMousePosition,
onVideoPlaying,
mouseWheelHandler,
videoKeyUpHandler,
],
[onVideoPlaying, videoKeyUpHandler],
);
// Setup Absolute Mouse Events
// Setup Mouse Events
useEffect(
function setAbsoluteMouseModeEventListeners() {
function setMouseModeEventListeners() {
const videoElmRefValue = videoElm.current;
if (!videoElmRefValue) return;
if (settings.mouseMode !== "absolute") return;
const isRelativeMouseMode = (settings.mouseMode === "relative");
const abortController = new AbortController();
const signal = abortController.signal;
videoElmRefValue.addEventListener("mousemove", absMouseMoveHandler, { signal });
videoElmRefValue.addEventListener("pointerdown", absMouseMoveHandler, { signal });
videoElmRefValue.addEventListener("pointerup", absMouseMoveHandler, { signal });
videoElmRefValue.addEventListener("mousemove", isRelativeMouseMode ? relMouseMoveHandler : absMouseMoveHandler, { signal });
videoElmRefValue.addEventListener("pointerdown", isRelativeMouseMode ? relMouseMoveHandler : absMouseMoveHandler, { signal });
videoElmRefValue.addEventListener("pointerup", isRelativeMouseMode ? relMouseMoveHandler :absMouseMoveHandler, { signal });
videoElmRefValue.addEventListener("wheel", mouseWheelHandler, {
signal,
passive: true,
});
// Reset the mouse position when the window is blurred or the document is hidden
const local = resetMousePosition;
window.addEventListener("blur", local, { signal });
document.addEventListener("visibilitychange", local, { signal });
if (isRelativeMouseMode) {
videoElmRefValue.addEventListener("click",
() => {
if (isPointerLockPossible && !isPointerLockActive && !document.pointerLockElement) {
requestPointerLock();
}
},
{ signal },
);
} else {
// Reset the mouse position when the window is blurred or the document is hidden
window.addEventListener("blur", resetMousePosition, { signal });
document.addEventListener("visibilitychange", resetMousePosition, { signal });
}
const preventContextMenu = (e: MouseEvent) => e.preventDefault();
videoElmRefValue.addEventListener("contextmenu", preventContextMenu, { signal });
@ -580,65 +614,18 @@ export default function WebRTCVideo() {
abortController.abort();
};
},
[absMouseMoveHandler, mouseWheelHandler, resetMousePosition, settings.mouseMode],
[absMouseMoveHandler, isPointerLockActive, isPointerLockPossible, mouseWheelHandler, relMouseMoveHandler, requestPointerLock, resetMousePosition, settings.mouseMode],
);
// Setup Relative Mouse Events
const containerRef = useRef<HTMLDivElement>(null);
useEffect(
function setupRelativeMouseEventListeners() {
if (settings.mouseMode !== "relative") return;
// Relative mouse mode should only be active if the pointer lock is active and Pointer Lock is possible
const videoElmRefValue = videoElm.current;
if (!videoElmRefValue) return;
const abortController = new AbortController();
const signal = abortController.signal;
videoElmRefValue.addEventListener("mousemove", relMouseMoveHandler, { signal });
videoElmRefValue.addEventListener("pointerdown", relMouseMoveHandler, { signal });
videoElmRefValue.addEventListener("pointerup", relMouseMoveHandler, { signal });
videoElmRefValue.addEventListener(
"click",
() => {
if (isPointerLockPossible && !document.pointerLockElement) {
requestPointerLock();
}
},
{ signal },
);
videoElmRefValue.addEventListener("wheel", mouseWheelHandler, {
signal,
passive: true,
});
const preventContextMenu = (e: MouseEvent) => e.preventDefault();
videoElmRefValue.addEventListener("contextmenu", preventContextMenu, { signal });
return () => {
abortController.abort();
};
},
[
settings.mouseMode,
relMouseMoveHandler,
mouseWheelHandler,
disableVideoFocusTrap,
requestPointerLock,
isPointerLockPossible,
isPointerLockActive,
],
);
const hasNoAutoPlayPermissions = useMemo(() => {
if (peerConnection?.connectionState !== "connected") return false;
if (isPlaying) return false;
if (hdmiError) return false;
if (videoHeight === 0 || videoWidth === 0) return false;
return true;
}, [peerConnection?.connectionState, isPlaying, hdmiError, videoHeight, videoWidth]);
}, [hdmiError, isPlaying, peerConnection?.connectionState, videoHeight, videoWidth]);
const showPointerLockBar = useMemo(() => {
if (settings.mouseMode !== "relative") return false;
@ -648,15 +635,7 @@ export default function WebRTCVideo() {
if (!isPlaying) return false;
if (videoHeight === 0 || videoWidth === 0) return false;
return true;
}, [
settings.mouseMode,
isPointerLockPossible,
isPointerLockActive,
isVideoLoading,
isPlaying,
videoHeight,
videoWidth,
]);
}, [isPlaying, isPointerLockActive, isPointerLockPossible, isVideoLoading, settings.mouseMode, videoHeight, videoWidth]);
return (
<div className="grid h-full w-full grid-rows-(--grid-layout)">
@ -686,10 +665,10 @@ export default function WebRTCVideo() {
<div className="relative grow overflow-hidden">
<div className="flex h-full flex-col">
<div className="grid grow grid-rows-(--grid-bodyFooter) overflow-hidden">
{/* In relative mouse mode and under https, we enable the pointer lock, and to do so we need a bar to show the user to click on the video to enable mouse control */}
<PointerLockBar show={showPointerLockBar} />
<div className="relative mx-4 my-2 flex items-center justify-center overflow-hidden">
<div className="relative flex h-full w-full items-center justify-center">
{/* In relative mouse mode and under https, we enable the pointer lock, and to do so we need a bar to show the user to click on the video to enable mouse control */}
<PointerLockBar show={showPointerLockBar} />
<video
ref={videoElm}
autoPlay={true}
@ -700,6 +679,9 @@ export default function WebRTCVideo() {
playsInline
disablePictureInPicture
controlsList="nofullscreen"
style={{
filter: `saturate(${videoSaturation}) brightness(${videoBrightness}) contrast(${videoContrast})`,
}}
className={cx(
"max-h-full min-h-[384px] max-w-full min-w-[512px] bg-black/50 object-contain transition-all duration-1000",
{

View File

@ -10,11 +10,11 @@ import { SettingsPageHeader } from "@components/SettingsPageheader";
import { useJsonRpc } from "@/hooks/useJsonRpc";
import { useHidStore, useRTCStore, useUiStore, useSettingsStore } from "@/hooks/stores";
import { keys, modifiers } from "@/keyboardMappings";
import { layouts, chars } from "@/keyboardLayouts";
import { KeyStroke, KeyboardLayout, selectedKeyboard } from "@/keyboardLayouts";
import notifications from "@/notifications";
const hidKeyboardPayload = (keys: number[], modifier: number) => {
return { keys, modifier };
const hidKeyboardPayload = (modifier: number, keys: number[]) => {
return { modifier, keys };
};
const modifierCode = (shift?: boolean, altRight?: boolean) => {
@ -62,49 +62,56 @@ export default function PasteModal() {
const onConfirmPaste = useCallback(async () => {
setPasteMode(false);
setDisableVideoFocusTrap(false);
if (rpcDataChannel?.readyState !== "open" || !TextAreaRef.current) return;
if (!safeKeyboardLayout) return;
if (!chars[safeKeyboardLayout]) return;
const keyboard: KeyboardLayout = selectedKeyboard(safeKeyboardLayout);
if (!keyboard) return;
const text = TextAreaRef.current.value;
try {
for (const char of text) {
const { key, shift, altRight, deadKey, accentKey } = chars[safeKeyboardLayout][char]
const keyprops = keyboard.chars[char];
if (!keyprops) continue;
const { key, shift, altRight, deadKey, accentKey } = keyprops;
if (!key) continue;
const keyz = [ keys[key] ];
const modz = [ modifierCode(shift, altRight) ];
if (deadKey) {
keyz.push(keys["Space"]);
modz.push(noModifier);
}
// if this is an accented character, we need to send that accent FIRST
if (accentKey) {
keyz.unshift(keys[accentKey.key])
modz.unshift(modifierCode(accentKey.shift, accentKey.altRight))
await sendKeystroke({modifier: modifierCode(accentKey.shift, accentKey.altRight), keys: [ keys[accentKey.key] ] })
}
for (const [index, kei] of keyz.entries()) {
await new Promise<void>((resolve, reject) => {
send(
"keyboardReport",
hidKeyboardPayload([kei], modz[index]),
params => {
if ("error" in params) return reject(params.error);
send("keyboardReport", hidKeyboardPayload([], 0), params => {
if ("error" in params) return reject(params.error);
resolve();
});
},
);
});
// now send the actual key
await sendKeystroke({ modifier: modifierCode(shift, altRight), keys: [ keys[key] ]});
// if what was requested was a dead key, we need to send an unmodified space to emit
// just the accent character
if (deadKey) {
await sendKeystroke({ modifier: noModifier, keys: [ keys["Space"] ] });
}
// now send a message with no keys down to "release" the keys
await sendKeystroke({ modifier: 0, keys: [] });
}
} catch (error) {
console.error(error);
console.error("Failed to paste text:", error);
notifications.error("Failed to paste text");
}
}, [rpcDataChannel?.readyState, send, setDisableVideoFocusTrap, setPasteMode, safeKeyboardLayout]);
async function sendKeystroke(stroke: KeyStroke) {
await new Promise<void>((resolve, reject) => {
send(
"keyboardReport",
hidKeyboardPayload(stroke.modifier, stroke.keys),
params => {
if ("error" in params) return reject(params.error);
resolve();
}
);
});
}
}, [rpcDataChannel?.readyState, safeKeyboardLayout, send, setDisableVideoFocusTrap, setPasteMode]);
useEffect(() => {
if (TextAreaRef.current) {
@ -154,7 +161,7 @@ export default function PasteModal() {
// @ts-expect-error TS doesn't recognize Intl.Segmenter in some environments
[...new Intl.Segmenter().segment(value)]
.map(x => x.segment)
.filter(char => !chars[safeKeyboardLayout][char]),
.filter(char => !selectedKeyboard(safeKeyboardLayout).chars[char]),
),
];
@ -175,7 +182,7 @@ export default function PasteModal() {
</div>
<div className="space-y-4">
<p className="text-xs text-slate-600 dark:text-slate-400">
Sending text using keyboard layout: {layouts[safeKeyboardLayout]}
Sending text using keyboard layout: {selectedKeyboard(safeKeyboardLayout).name}
</p>
</div>
</div>

View File

@ -14,7 +14,7 @@ import AddDeviceForm from "./AddDeviceForm";
export default function WakeOnLanModal() {
const [storedDevices, setStoredDevices] = useState<StoredDevice[]>([]);
const [showAddForm, setShowAddForm] = useState(false);
const setDisableFocusTrap = useUiStore(state => state.setDisableVideoFocusTrap);
const setDisableVideoFocusTrap = useUiStore(state => state.setDisableVideoFocusTrap);
const rpcDataChannel = useRTCStore(state => state.rpcDataChannel);
@ -24,9 +24,9 @@ export default function WakeOnLanModal() {
const [addDeviceErrorMessage, setAddDeviceErrorMessage] = useState<string | null>(null);
const onCancelWakeOnLanModal = useCallback(() => {
setDisableVideoFocusTrap(false);
close();
setDisableFocusTrap(false);
}, [close, setDisableFocusTrap]);
}, [close, setDisableVideoFocusTrap]);
const onSendMagicPacket = useCallback(
(macAddress: string) => {
@ -43,12 +43,12 @@ export default function WakeOnLanModal() {
}
} else {
notifications.success("Magic Packet sent successfully");
setDisableFocusTrap(false);
setDisableVideoFocusTrap(false);
close();
}
});
},
[close, rpcDataChannel?.readyState, send, setDisableFocusTrap],
[close, rpcDataChannel?.readyState, send, setDisableVideoFocusTrap],
);
const syncStoredDevices = useCallback(() => {
@ -78,7 +78,7 @@ export default function WakeOnLanModal() {
}
});
},
[storedDevices, send, syncStoredDevices],
[send, storedDevices, syncStoredDevices],
);
const onAddDevice = useCallback(

View File

@ -314,8 +314,19 @@ interface SettingsState {
keyboardLedSync: KeyboardLedSync;
setKeyboardLedSync: (sync: KeyboardLedSync) => void;
scrollThrottling: number;
setScrollThrottling: (value: number) => void;
showPressedKeys: boolean;
setShowPressedKeys: (show: boolean) => void;
// Video enhancement settings
videoSaturation: number;
setVideoSaturation: (value: number) => void;
videoBrightness: number;
setVideoBrightness: (value: number) => void;
videoContrast: number;
setVideoContrast: (value: number) => void;
}
export const useSettingsStore = create(
@ -354,8 +365,19 @@ export const useSettingsStore = create(
keyboardLedSync: "auto",
setKeyboardLedSync: sync => set({ keyboardLedSync: sync }),
scrollThrottling: 0,
setScrollThrottling: value => set({ scrollThrottling: value }),
showPressedKeys: true,
setShowPressedKeys: show => set({ showPressedKeys: show }),
// Video enhancement settings with default values (1.0 = normal)
videoSaturation: 1.0,
setVideoSaturation: value => set({ videoSaturation: value }),
videoBrightness: 1.0,
setVideoBrightness: value => set({ videoBrightness: value }),
videoContrast: 1.0,
setVideoContrast: value => set({ videoContrast: value }),
}),
{
name: "settings",
@ -919,5 +941,5 @@ export const useMacrosStore = create<MacrosState>((set, get) => ({
} finally {
set({ loading: false });
}
},
}
}));

View File

@ -1,45 +1,32 @@
import { chars as chars_fr_BE, name as name_fr_BE } from "@/keyboardLayouts/fr_BE"
import { chars as chars_cs_CZ, name as name_cs_CZ } from "@/keyboardLayouts/cs_CZ"
import { chars as chars_en_UK, name as name_en_UK } from "@/keyboardLayouts/en_UK"
import { chars as chars_en_US, name as name_en_US } from "@/keyboardLayouts/en_US"
import { chars as chars_fr_FR, name as name_fr_FR } from "@/keyboardLayouts/fr_FR"
import { chars as chars_de_DE, name as name_de_DE } from "@/keyboardLayouts/de_DE"
import { chars as chars_it_IT, name as name_it_IT } from "@/keyboardLayouts/it_IT"
import { chars as chars_nb_NO, name as name_nb_NO } from "@/keyboardLayouts/nb_NO"
import { chars as chars_es_ES, name as name_es_ES } from "@/keyboardLayouts/es_ES"
import { chars as chars_sv_SE, name as name_sv_SE } from "@/keyboardLayouts/sv_SE"
import { chars as chars_fr_CH, name as name_fr_CH } from "@/keyboardLayouts/fr_CH"
import { chars as chars_de_CH, name as name_de_CH } from "@/keyboardLayouts/de_CH"
export interface KeyStroke { modifier: number; keys: number[]; }
export interface KeyInfo { key: string | number; shift?: boolean, altRight?: boolean }
export interface KeyCombo extends KeyInfo { deadKey?: boolean, accentKey?: KeyInfo }
export interface KeyboardLayout { isoCode: string, name: string, chars: Record<string, KeyCombo> }
interface KeyInfo { key: string | number; shift?: boolean, altRight?: boolean }
export type KeyCombo = KeyInfo & { deadKey?: boolean, accentKey?: KeyInfo }
// to add a new layout, create a file like the above and add it to the list
import { cs_CZ } from "@/keyboardLayouts/cs_CZ"
import { de_CH } from "@/keyboardLayouts/de_CH"
import { de_DE } from "@/keyboardLayouts/de_DE"
import { en_US } from "@/keyboardLayouts/en_US"
import { en_UK } from "@/keyboardLayouts/en_UK"
import { es_ES } from "@/keyboardLayouts/es_ES"
import { fr_BE } from "@/keyboardLayouts/fr_BE"
import { fr_CH } from "@/keyboardLayouts/fr_CH"
import { fr_FR } from "@/keyboardLayouts/fr_FR"
import { it_IT } from "@/keyboardLayouts/it_IT"
import { nb_NO } from "@/keyboardLayouts/nb_NO"
import { sv_SE } from "@/keyboardLayouts/sv_SE"
export const layouts: Record<string, string> = {
be_FR: name_fr_BE,
cs_CZ: name_cs_CZ,
en_UK: name_en_UK,
en_US: name_en_US,
fr_FR: name_fr_FR,
de_DE: name_de_DE,
it_IT: name_it_IT,
nb_NO: name_nb_NO,
es_ES: name_es_ES,
sv_SE: name_sv_SE,
fr_CH: name_fr_CH,
de_CH: name_de_CH,
}
export const keyboards: KeyboardLayout[] = [ cs_CZ, de_CH, de_DE, en_UK, en_US, es_ES, fr_BE, fr_CH, fr_FR, it_IT, nb_NO, sv_SE ];
export const chars: Record<string, Record<string, KeyCombo>> = {
be_FR: chars_fr_BE,
cs_CZ: chars_cs_CZ,
en_UK: chars_en_UK,
en_US: chars_en_US,
fr_FR: chars_fr_FR,
de_DE: chars_de_DE,
it_IT: chars_it_IT,
nb_NO: chars_nb_NO,
es_ES: chars_es_ES,
sv_SE: chars_sv_SE,
fr_CH: chars_fr_CH,
de_CH: chars_de_CH,
export const selectedKeyboard = (isoCode: string): KeyboardLayout => {
// fallback to original behaviour of en-US if no isoCode given
return keyboards.find(keyboard => keyboard.isoCode == isoCode)
?? keyboards.find(keyboard => keyboard.isoCode == "en-US")!;
};
export const keyboardOptions = () => {
return keyboards.map((keyboard) => {
return { label: keyboard.name, value: keyboard.isoCode }
});
}

View File

@ -1,6 +1,6 @@
import { KeyCombo } from "../keyboardLayouts"
import { KeyboardLayout, KeyCombo } from "../keyboardLayouts"
export const name = "Čeština";
const name = "Čeština";
const keyTrema = { key: "Backslash" } // tréma (umlaut), two dots placed above a vowel
const keyAcute = { key: "Equal" } // accent aigu (acute accent), mark ´ placed above the letter
@ -13,7 +13,7 @@ const keyOverdot = { key: "Digit8", shift: true, altRight: true } // overdot (do
const keyHook = { key: "Digit6", shift: true, altRight: true } // ogonoek (little hook), mark ˛ placed beneath a letter
const keyCedille = { key: "Equal", shift: true, altRight: true } // accent cedille (cedilla), mark ¸ placed beneath a letter
export const chars = {
const chars = {
A: { key: "KeyA", shift: true },
"Ä": { key: "KeyA", shift: true, accentKey: keyTrema },
"Á": { key: "KeyA", shift: true, accentKey: keyAcute },
@ -242,3 +242,9 @@ export const chars = {
Enter: { key: "Enter" },
Tab: { key: "Tab" },
} as Record<string, KeyCombo>;
export const cs_CZ: KeyboardLayout = {
isoCode: "cs-CZ",
name: name,
chars: chars
};

View File

@ -1,6 +1,6 @@
import { KeyCombo } from "../keyboardLayouts"
import { KeyboardLayout, KeyCombo } from "../keyboardLayouts"
export const name = "Schwiizerdütsch";
const name = "Schwiizerdütsch";
const keyTrema = { key: "BracketRight" } // tréma (umlaut), two dots placed above a vowel
const keyAcute = { key: "Minus", altRight: true } // accent aigu (acute accent), mark ´ placed above the letter
@ -8,7 +8,7 @@ const keyHat = { key: "Equal" } // accent circonflexe (accent hat), mark ^ place
const keyGrave = { key: "Equal", shift: true } // accent grave, mark ` placed above the letter
const keyTilde = { key: "Equal", altRight: true } // tilde, mark ~ placed above the letter
export const chars = {
const chars = {
A: { key: "KeyA", shift: true },
"Ä": { key: "KeyA", shift: true, accentKey: keyTrema },
"Á": { key: "KeyA", shift: true, accentKey: keyAcute },
@ -163,3 +163,9 @@ export const chars = {
Enter: { key: "Enter" },
Tab: { key: "Tab" },
} as Record<string, KeyCombo>;
export const de_CH: KeyboardLayout = {
isoCode: "de-CH",
name: name,
chars: chars
};

View File

@ -1,12 +1,12 @@
import { KeyCombo } from "../keyboardLayouts"
import { KeyboardLayout, KeyCombo } from "../keyboardLayouts"
export const name = "Deutsch";
const name = "Deutsch";
const keyAcute = { key: "Equal" } // accent aigu (acute accent), mark ´ placed above the letter
const keyHat = { key: "Backquote" } // accent circonflexe (accent hat), mark ^ placed above the letter
const keyGrave = { key: "Equal", shift: true } // accent grave, mark ` placed above the letter
export const chars = {
const chars = {
A: { key: "KeyA", shift: true },
"Á": { key: "KeyA", shift: true, accentKey: keyAcute },
"Â": { key: "KeyA", shift: true, accentKey: keyHat },
@ -150,3 +150,9 @@ export const chars = {
Enter: { key: "Enter" },
Tab: { key: "Tab" },
} as Record<string, KeyCombo>;
export const de_DE: KeyboardLayout = {
isoCode: "de-DE",
name: name,
chars: chars
};

View File

@ -1,8 +1,8 @@
import { KeyCombo } from "../keyboardLayouts"
import { KeyboardLayout, KeyCombo } from "../keyboardLayouts"
export const name = "English (UK)";
const name = "English (UK)";
export const chars = {
const chars = {
A: { key: "KeyA", shift: true },
B: { key: "KeyB", shift: true },
C: { key: "KeyC", shift: true },
@ -105,3 +105,9 @@ export const chars = {
Enter: { key: "Enter" },
Tab: { key: "Tab" },
} as Record<string, KeyCombo>
export const en_UK: KeyboardLayout = {
isoCode: "en-UK",
name: name,
chars: chars
};

View File

@ -1,8 +1,8 @@
import { KeyCombo } from "../keyboardLayouts"
import { KeyboardLayout, KeyCombo } from "../keyboardLayouts"
export const name = "English (US)";
const name = "English (US)";
export const chars = {
const chars = {
A: { key: "KeyA", shift: true },
B: { key: "KeyB", shift: true },
C: { key: "KeyC", shift: true },
@ -111,3 +111,9 @@ export const chars = {
Insert: { key: "Insert", shift: false },
Delete: { key: "Delete", shift: false },
} as Record<string, KeyCombo>
export const en_US: KeyboardLayout = {
isoCode: "en-US",
name: name,
chars: chars
};

View File

@ -1,6 +1,6 @@
import { KeyCombo } from "../keyboardLayouts"
import { KeyboardLayout, KeyCombo } from "../keyboardLayouts"
export const name = "Español";
const name = "Español";
const keyTrema = { key: "Quote", shift: true } // tréma (umlaut), two dots placed above a vowel
const keyAcute = { key: "Quote" } // accent aigu (acute accent), mark ´ placed above the letter
@ -8,7 +8,7 @@ const keyHat = { key: "BracketRight", shift: true } // accent circonflexe (accen
const keyGrave = { key: "BracketRight" } // accent grave, mark ` placed above the letter
const keyTilde = { key: "Key4", altRight: true } // tilde, mark ~ placed above the letter
export const chars = {
const chars = {
A: { key: "KeyA", shift: true },
"Ä": { key: "KeyA", shift: true, accentKey: keyTrema },
"Á": { key: "KeyA", shift: true, accentKey: keyAcute },
@ -166,3 +166,9 @@ export const chars = {
Enter: { key: "Enter" },
Tab: { key: "Tab" },
} as Record<string, KeyCombo>;
export const es_ES: KeyboardLayout = {
isoCode: "es-ES",
name: name,
chars: chars
};

View File

@ -1,6 +1,6 @@
import { KeyCombo } from "../keyboardLayouts"
import { KeyboardLayout, KeyCombo } from "../keyboardLayouts"
export const name = "Belgisch Nederlands";
const name = "Belgisch Nederlands";
const keyTrema = { key: "BracketLeft", shift: true } // tréma (umlaut), two dots placed above a vowel
const keyHat = { key: "BracketLeft" } // accent circonflexe (accent hat), mark ^ placed above the letter
@ -8,7 +8,7 @@ const keyAcute = { key: "Semicolon", altRight: true } // accent aigu (acute acce
const keyGrave = { key: "Quote", shift: true } // accent grave, mark ` placed above the letter
const keyTilde = { key: "Slash", altRight: true } // tilde, mark ~ placed above the letter
export const chars = {
const chars = {
A: { key: "KeyQ", shift: true },
"Ä": { key: "KeyQ", shift: true, accentKey: keyTrema },
"Â": { key: "KeyQ", shift: true, accentKey: keyHat },
@ -165,3 +165,9 @@ export const chars = {
Enter: { key: "Enter" },
Tab: { key: "Tab" },
} as Record<string, KeyCombo>;
export const fr_BE: KeyboardLayout = {
isoCode: "fr-BE",
name: name,
chars: chars
};

View File

@ -1,11 +1,11 @@
import { KeyCombo } from "../keyboardLayouts"
import { KeyboardLayout, KeyCombo } from "../keyboardLayouts"
import { chars as chars_de_CH } from "./de_CH"
import { de_CH } from "./de_CH"
export const name = "Français de Suisse";
const name = "Français de Suisse";
export const chars = {
...chars_de_CH,
const chars = {
...de_CH.chars,
"è": { key: "BracketLeft" },
"ü": { key: "BracketLeft", shift: true },
"é": { key: "Semicolon" },
@ -13,3 +13,9 @@ export const chars = {
"à": { key: "Quote" },
"ä": { key: "Quote", shift: true },
} as Record<string, KeyCombo>;
export const fr_CH: KeyboardLayout = {
isoCode: "fr-CH",
name: name,
chars: chars
};

View File

@ -1,11 +1,11 @@
import { KeyCombo } from "../keyboardLayouts"
import { KeyboardLayout, KeyCombo } from "../keyboardLayouts"
export const name = "Français";
const name = "Français";
const keyTrema = { key: "BracketLeft", shift: true } // tréma (umlaut), two dots placed above a vowel
const keyHat = { key: "BracketLeft" } // accent circonflexe (accent hat), mark ^ placed above the letter
export const chars = {
const chars = {
A: { key: "KeyQ", shift: true },
"Ä": { key: "KeyQ", shift: true, accentKey: keyTrema },
"Â": { key: "KeyQ", shift: true, accentKey: keyHat },
@ -137,3 +137,9 @@ export const chars = {
Enter: { key: "Enter" },
Tab: { key: "Tab" },
} as Record<string, KeyCombo>;
export const fr_FR: KeyboardLayout = {
isoCode: "fr-FR",
name: name,
chars: chars
};

View File

@ -1,8 +1,8 @@
import { KeyCombo } from "../keyboardLayouts"
import { KeyboardLayout, KeyCombo } from "../keyboardLayouts"
export const name = "Italiano";
const name = "Italiano";
export const chars = {
const chars = {
A: { key: "KeyA", shift: true },
B: { key: "KeyB", shift: true },
C: { key: "KeyC", shift: true },
@ -111,3 +111,9 @@ export const chars = {
Enter: { key: "Enter" },
Tab: { key: "Tab" },
} as Record<string, KeyCombo>;
export const it_IT: KeyboardLayout = {
isoCode: "it-IT",
name: name,
chars: chars
};

View File

@ -1,6 +1,6 @@
import { KeyCombo } from "../keyboardLayouts"
import { KeyboardLayout, KeyCombo } from "../keyboardLayouts"
export const name = "Norsk bokmål";
const name = "Norsk bokmål";
const keyTrema = { key: "BracketRight" } // tréma (umlaut), two dots placed above a vowel
const keyAcute = { key: "Equal", altRight: true } // accent aigu (acute accent), mark ´ placed above the letter
@ -8,7 +8,7 @@ const keyHat = { key: "BracketRight", shift: true } // accent circonflexe (accen
const keyGrave = { key: "Equal", shift: true } // accent grave, mark ` placed above the letter
const keyTilde = { key: "BracketRight", altRight: true } // tilde, mark ~ placed above the letter
export const chars = {
const chars = {
A: { key: "KeyA", shift: true },
"Ä": { key: "KeyA", shift: true, accentKey: keyTrema },
"Á": { key: "KeyA", shift: true, accentKey: keyAcute },
@ -165,3 +165,9 @@ export const chars = {
Enter: { key: "Enter" },
Tab: { key: "Tab" },
} as Record<string, KeyCombo>;
export const nb_NO: KeyboardLayout = {
isoCode: "nb-NO",
name: name,
chars: chars
};

View File

@ -1,6 +1,6 @@
import { KeyCombo } from "../keyboardLayouts"
import { KeyboardLayout, KeyCombo } from "../keyboardLayouts"
export const name = "Svenska";
const name = "Svenska";
const keyTrema = { key: "BracketRight" } // tréma (umlaut), two dots placed above a vowel
const keyAcute = { key: "Equal" } // accent aigu (acute accent), mark ´ placed above the letter
@ -8,7 +8,7 @@ const keyHat = { key: "BracketRight", shift: true } // accent circonflexe (accen
const keyGrave = { key: "Equal", shift: true } // accent grave, mark ` placed above the letter
const keyTilde = { key: "BracketRight", altRight: true } // tilde, mark ~ placed above the letter
export const chars = {
const chars = {
A: { key: "KeyA", shift: true },
"Á": { key: "KeyA", shift: true, accentKey: keyAcute },
"Â": { key: "KeyA", shift: true, accentKey: keyHat },
@ -162,3 +162,9 @@ export const chars = {
Enter: { key: "Enter" },
Tab: { key: "Tab" },
} as Record<string, KeyCombo>;
export const sv_SE: KeyboardLayout = {
isoCode: "sv-SE",
name: name,
chars: chars
};

View File

@ -1,17 +1,19 @@
// Key codes and modifiers correspond to definitions in the
// [Linux USB HID gadget driver](https://www.kernel.org/doc/Documentation/usb/gadget_hid.txt)
// [Section 10. Keyboard/Keypad Page 0x07](https://usb.org/sites/default/files/hut1_21.pdf)
export const keys = {
ArrowDown: 0x51,
ArrowLeft: 0x50,
ArrowRight: 0x4f,
ArrowUp: 0x52,
Backquote: 0x35,
Backquote: 0x35, // aka Grave
Backslash: 0x31,
Backspace: 0x2a,
BracketLeft: 0x2f,
BracketRight: 0x30,
BracketLeft: 0x2f, // aka LeftBrace
BracketRight: 0x30, // aka RightBrace
CapsLock: 0x39,
Comma: 0x36,
Compose: 0x65,
ContextMenu: 0,
Delete: 0x4c,
Digit0: 0x27,
@ -40,10 +42,21 @@ export const keys = {
F10: 0x43,
F11: 0x44,
F12: 0x45,
F13: 0x68,
F14: 0x69,
F15: 0x6a,
F16: 0x6b,
F17: 0x6c,
F18: 0x6d,
F19: 0x6e,
F20: 0x6f,
F21: 0x70,
F22: 0x71,
F23: 0x72,
F24: 0x73,
Home: 0x4a,
HashTilde: 0x32, // non-US # and ~
Insert: 0x49,
IntlBackslash: 0x64,
IntlBackslash: 0x64, // non-US \ and |
KeyA: 0x04,
KeyB: 0x05,
KeyC: 0x06,
@ -72,30 +85,35 @@ export const keys = {
KeyZ: 0x1d,
KeypadExclamation: 0xcf,
Minus: 0x2d,
NumLock: 0x53,
Numpad0: 0x62,
Numpad1: 0x59,
Numpad2: 0x5a,
Numpad3: 0x5b,
Numpad4: 0x5c,
None: 0x00,
NumLock: 0x53, // and Clear
Numpad0: 0x62, // and Insert
Numpad1: 0x59, // and End
Numpad2: 0x5a, // and Down Arrow
Numpad3: 0x5b, // and Page Down
Numpad4: 0x5c, // and Left Arrow
Numpad5: 0x5d,
Numpad6: 0x5e,
Numpad7: 0x5f,
Numpad8: 0x60,
Numpad9: 0x61,
Numpad6: 0x5e, // and Right Arrow
Numpad7: 0x5f, // and Home
Numpad8: 0x60, // and Up Arrow
Numpad9: 0x61, // and Page Up
NumpadAdd: 0x57,
NumpadComma: 0x85,
NumpadDecimal: 0x63,
NumpadDivide: 0x54,
NumpadEnter: 0x58,
NumpadEqual: 0x67,
NumpadLeftParen: 0xb6,
NumpadMultiply: 0x55,
NumpadRightParen: 0xb7,
NumpadSubtract: 0x56,
NumpadDecimal: 0x63,
PageDown: 0x4e,
PageUp: 0x4b,
Period: 0x37,
PrintScreen: 0x46,
Pause: 0x48,
Quote: 0x34,
Power: 0x66,
Quote: 0x34, // aka Single Quote or Apostrophe
ScrollLock: 0x47,
Semicolon: 0x33,
Slash: 0x38,

View File

@ -1,10 +1,10 @@
import { SettingsItem } from "./devices.$id.settings";
import { Checkbox } from "@/components/Checkbox";
import { SettingsPageHeader } from "@/components/SettingsPageheader";
import { useSettingsStore } from "@/hooks/stores";
import { SettingsItem } from "./devices.$id.settings";
export default function SettingsCtrlAltDelRoute() {
const enableCtrlAltDel = useSettingsStore(state => state.actionBarCtrlAltDel);
const setEnableCtrlAltDel = useSettingsStore(state => state.setActionBarCtrlAltDel);

View File

@ -4,7 +4,7 @@ import { KeyboardLedSync, useSettingsStore } from "@/hooks/stores";
import { useJsonRpc } from "@/hooks/useJsonRpc";
import notifications from "@/notifications";
import { SettingsPageHeader } from "@components/SettingsPageheader";
import { layouts } from "@/keyboardLayouts";
import { keyboardOptions } from "@/keyboardLayouts";
import { Checkbox } from "@/components/Checkbox";
import { SelectMenuBasic } from "../components/SelectMenuBasic";
@ -32,7 +32,7 @@ export default function SettingsKeyboardRoute() {
return "en-US";
}, [keyboardLayout]);
const layoutOptions = Object.entries(layouts).map(([code, language]) => { return { value: code, label: language } })
const layoutOptions = keyboardOptions();
const ledSyncOptions = [
{ value: "auto", label: "Automatic" },
{ value: "browser", label: "Browser Only" },

View File

@ -9,6 +9,7 @@ import { useSettingsStore } from "@/hooks/stores";
import { useJsonRpc } from "@/hooks/useJsonRpc";
import notifications from "@/notifications";
import { SettingsPageHeader } from "@components/SettingsPageheader";
import { SelectMenuBasic } from "@components/SelectMenuBasic";
import { useFeatureFlag } from "../hooks/useFeatureFlag";
import { cx } from "../cva.config";
@ -26,6 +27,19 @@ export default function SettingsMouseRoute() {
const [jiggler, setJiggler] = useState(false);
const scrollThrottling = useSettingsStore(state => state.scrollThrottling);
const setScrollThrottling = useSettingsStore(
state => state.setScrollThrottling,
);
const scrollThrottlingOptions = [
{ value: "0", label: "Off" },
{ value: "10", label: "Low" },
{ value: "25", label: "Medium" },
{ value: "50", label: "High" },
{ value: "100", label: "Very High" },
];
const [send] = useJsonRpc();
useEffect(() => {
@ -65,6 +79,21 @@ export default function SettingsMouseRoute() {
/>
</SettingsItem>
<SettingsItem
title="Scroll Throttling"
description="Reduce the frequency of scroll events"
>
<SelectMenuBasic
size="SM"
label=""
className="max-w-[292px]"
value={scrollThrottling}
fullWidth
onChange={e => setScrollThrottling(parseInt(e.target.value))}
options={scrollThrottlingOptions}
/>
</SettingsItem>
<SettingsItem
title="Jiggler"
description="Simulate movement of a computer mouse. Prevents sleep mode, standby mode or the screensaver from activating"

View File

@ -79,7 +79,7 @@ export default function SettingsRoute() {
return () => {
setDisableVideoFocusTrap(false);
};
}, [setDisableVideoFocusTrap, sendKeyboardEvent]);
}, [sendKeyboardEvent, setDisableVideoFocusTrap]);
return (
<div className="pointer-events-auto relative mx-auto max-w-4xl translate-x-0 transform text-left dark:text-white">

View File

@ -4,6 +4,7 @@ import { Button } from "@/components/Button";
import { TextAreaWithLabel } from "@/components/TextArea";
import { useJsonRpc } from "@/hooks/useJsonRpc";
import { SettingsPageHeader } from "@components/SettingsPageheader";
import { useSettingsStore } from "@/hooks/stores";
import notifications from "../notifications";
import { SelectMenuBasic } from "../components/SelectMenuBasic";
@ -45,6 +46,14 @@ export default function SettingsVideoRoute() {
const [customEdidValue, setCustomEdidValue] = useState<string | null>(null);
const [edid, setEdid] = useState<string | null>(null);
// Video enhancement settings from store
const videoSaturation = useSettingsStore(state => state.videoSaturation);
const setVideoSaturation = useSettingsStore(state => state.setVideoSaturation);
const videoBrightness = useSettingsStore(state => state.videoBrightness);
const setVideoBrightness = useSettingsStore(state => state.setVideoBrightness);
const videoContrast = useSettingsStore(state => state.videoContrast);
const setVideoContrast = useSettingsStore(state => state.setVideoContrast);
useEffect(() => {
send("getStreamQualityFactor", {}, resp => {
if ("error" in resp) return;
@ -126,6 +135,73 @@ export default function SettingsVideoRoute() {
onChange={e => handleStreamQualityChange(e.target.value)}
/>
</SettingsItem>
{/* Video Enhancement Settings */}
<SettingsItem
title="Video Enhancement"
description="Adjust color settings to make the video output more vibrant and colorful"
/>
<div className="space-y-4 pl-4">
<SettingsItem
title="Saturation"
description={`Color saturation (${videoSaturation.toFixed(1)}x)`}
>
<input
type="range"
min="0.5"
max="2.0"
step="0.1"
value={videoSaturation}
onChange={e => setVideoSaturation(parseFloat(e.target.value))}
className="w-32 h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer dark:bg-gray-700"
/>
</SettingsItem>
<SettingsItem
title="Brightness"
description={`Brightness level (${videoBrightness.toFixed(1)}x)`}
>
<input
type="range"
min="0.5"
max="1.5"
step="0.1"
value={videoBrightness}
onChange={e => setVideoBrightness(parseFloat(e.target.value))}
className="w-32 h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer dark:bg-gray-700"
/>
</SettingsItem>
<SettingsItem
title="Contrast"
description={`Contrast level (${videoContrast.toFixed(1)}x)`}
>
<input
type="range"
min="0.5"
max="2.0"
step="0.1"
value={videoContrast}
onChange={e => setVideoContrast(parseFloat(e.target.value))}
className="w-32 h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer dark:bg-gray-700"
/>
</SettingsItem>
<div className="flex gap-2">
<Button
size="SM"
theme="light"
text="Reset to Default"
onClick={() => {
setVideoSaturation(1.0);
setVideoBrightness(1.0);
setVideoContrast(1.0);
}}
/>
</div>
</div>
<SettingsItem
title="EDID"
description="Adjust the EDID settings for the display"

View File

@ -707,7 +707,7 @@ export default function KvmIdRoute() {
}, [diskChannel, file]);
// System update
const disableKeyboardFocusTrap = useUiStore(state => state.disableVideoFocusTrap);
const disableVideoFocusTrap = useUiStore(state => state.disableVideoFocusTrap);
const [kvmTerminal, setKvmTerminal] = useState<RTCDataChannel | null>(null);
const [serialConsole, setSerialConsole] = useState<RTCDataChannel | null>(null);
@ -805,7 +805,7 @@ export default function KvmIdRoute() {
)}
<div className="relative h-full">
<FocusTrap
paused={disableKeyboardFocusTrap}
paused={disableVideoFocusTrap}
focusTrapOptions={{
allowOutsideClick: true,
escapeDeactivates: false,