mirror of https://github.com/jetkvm/kvm.git
Compare commits
1 Commits
a4683e8431
...
4dbb59d980
| Author | SHA1 | Date |
|---|---|---|
|
|
4dbb59d980 |
|
|
@ -43,7 +43,7 @@ jobs:
|
||||||
cache: "npm"
|
cache: "npm"
|
||||||
cache-dependency-path: "**/package-lock.json"
|
cache-dependency-path: "**/package-lock.json"
|
||||||
- name: Set up Golang
|
- name: Set up Golang
|
||||||
uses: actions/setup-go@v6
|
uses: actions/setup-go@v6.0.0
|
||||||
with:
|
with:
|
||||||
go-version: "^1.25.1"
|
go-version: "^1.25.1"
|
||||||
- name: Build frontend
|
- name: Build frontend
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ jobs:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v5
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v6
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: oldstable
|
go-version: oldstable
|
||||||
- name: Setup build environment variables
|
- name: Setup build environment variables
|
||||||
|
|
|
||||||
|
|
@ -104,7 +104,7 @@ jobs:
|
||||||
EOF
|
EOF
|
||||||
ssh jkci "cat /tmp/device-tests.json" > device-tests.json
|
ssh jkci "cat /tmp/device-tests.json" > device-tests.json
|
||||||
- name: Set up Golang
|
- name: Set up Golang
|
||||||
uses: actions/setup-go@v6
|
uses: actions/setup-go@v5.5.0
|
||||||
with:
|
with:
|
||||||
go-version: "1.24.4"
|
go-version: "1.24.4"
|
||||||
- name: Golang Test Report
|
- name: Golang Test Report
|
||||||
|
|
|
||||||
|
|
@ -20,10 +20,3 @@ node_modules
|
||||||
#internal/native/include
|
#internal/native/include
|
||||||
#internal/native/lib
|
#internal/native/lib
|
||||||
internal/audio/bin/
|
internal/audio/bin/
|
||||||
|
|
||||||
# backup files
|
|
||||||
*.bak
|
|
||||||
|
|
||||||
# core dumps
|
|
||||||
core
|
|
||||||
core.*
|
|
||||||
|
|
|
||||||
2
Makefile
2
Makefile
|
|
@ -99,7 +99,6 @@ build_audio_output: build_audio_deps
|
||||||
-o $(BIN_DIR)/jetkvm_audio_output \
|
-o $(BIN_DIR)/jetkvm_audio_output \
|
||||||
internal/audio/c/jetkvm_audio_output.c \
|
internal/audio/c/jetkvm_audio_output.c \
|
||||||
internal/audio/c/ipc_protocol.c \
|
internal/audio/c/ipc_protocol.c \
|
||||||
internal/audio/c/audio_common.c \
|
|
||||||
internal/audio/c/audio.c \
|
internal/audio/c/audio.c \
|
||||||
$(CGO_LDFLAGS); \
|
$(CGO_LDFLAGS); \
|
||||||
fi
|
fi
|
||||||
|
|
@ -115,7 +114,6 @@ build_audio_input: build_audio_deps
|
||||||
-o $(BIN_DIR)/jetkvm_audio_input \
|
-o $(BIN_DIR)/jetkvm_audio_input \
|
||||||
internal/audio/c/jetkvm_audio_input.c \
|
internal/audio/c/jetkvm_audio_input.c \
|
||||||
internal/audio/c/ipc_protocol.c \
|
internal/audio/c/ipc_protocol.c \
|
||||||
internal/audio/c/audio_common.c \
|
|
||||||
internal/audio/c/audio.c \
|
internal/audio/c/audio.c \
|
||||||
$(CGO_LDFLAGS); \
|
$(CGO_LDFLAGS); \
|
||||||
fi
|
fi
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ var audioControlService *audio.AudioControlService
|
||||||
|
|
||||||
func ensureAudioControlService() *audio.AudioControlService {
|
func ensureAudioControlService() *audio.AudioControlService {
|
||||||
if audioControlService == nil {
|
if audioControlService == nil {
|
||||||
sessionProvider := &KVMSessionProvider{}
|
sessionProvider := &SessionProviderImpl{}
|
||||||
audioControlService = audio.NewAudioControlService(sessionProvider, logger)
|
audioControlService = audio.NewAudioControlService(sessionProvider, logger)
|
||||||
|
|
||||||
// Set up RPC callback function for the audio package
|
// Set up RPC callback function for the audio package
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
package kvm
|
||||||
|
|
||||||
|
import "github.com/jetkvm/kvm/internal/audio"
|
||||||
|
|
||||||
|
// SessionProviderImpl implements the audio.SessionProvider interface
|
||||||
|
type SessionProviderImpl struct{}
|
||||||
|
|
||||||
|
// NewSessionProvider creates a new session provider
|
||||||
|
func NewSessionProvider() *SessionProviderImpl {
|
||||||
|
return &SessionProviderImpl{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsSessionActive returns whether there's an active session
|
||||||
|
func (sp *SessionProviderImpl) IsSessionActive() bool {
|
||||||
|
return currentSession != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAudioInputManager returns the current session's audio input manager
|
||||||
|
func (sp *SessionProviderImpl) GetAudioInputManager() *audio.AudioInputManager {
|
||||||
|
if currentSession == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return currentSession.AudioInputManager
|
||||||
|
}
|
||||||
48
go.mod
48
go.mod
|
|
@ -5,14 +5,13 @@ go 1.24.4
|
||||||
require (
|
require (
|
||||||
github.com/Masterminds/semver/v3 v3.4.0
|
github.com/Masterminds/semver/v3 v3.4.0
|
||||||
github.com/beevik/ntp v1.4.3
|
github.com/beevik/ntp v1.4.3
|
||||||
github.com/coder/websocket v1.8.14
|
github.com/coder/websocket v1.8.13
|
||||||
github.com/coreos/go-oidc/v3 v3.15.0
|
github.com/coreos/go-oidc/v3 v3.15.0
|
||||||
github.com/creack/pty v1.1.24
|
github.com/creack/pty v1.1.24
|
||||||
github.com/erikdubbelboer/gspt v0.0.0-20210805194459-ce36a5128377
|
|
||||||
github.com/fsnotify/fsnotify v1.9.0
|
github.com/fsnotify/fsnotify v1.9.0
|
||||||
github.com/gin-contrib/logger v1.2.6
|
github.com/gin-contrib/logger v1.2.6
|
||||||
github.com/gin-gonic/gin v1.11.0
|
github.com/gin-gonic/gin v1.10.1
|
||||||
github.com/go-co-op/gocron/v2 v2.16.6
|
github.com/go-co-op/gocron/v2 v2.16.5
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/guregu/null/v6 v6.0.0
|
github.com/guregu/null/v6 v6.0.0
|
||||||
github.com/gwatts/rootcerts v0.0.0-20250901182336-dc5ae18bd79f
|
github.com/gwatts/rootcerts v0.0.0-20250901182336-dc5ae18bd79f
|
||||||
|
|
@ -20,8 +19,8 @@ require (
|
||||||
github.com/pion/mdns/v2 v2.0.7
|
github.com/pion/mdns/v2 v2.0.7
|
||||||
github.com/pion/webrtc/v4 v4.1.4
|
github.com/pion/webrtc/v4 v4.1.4
|
||||||
github.com/pojntfx/go-nbd v0.3.2
|
github.com/pojntfx/go-nbd v0.3.2
|
||||||
github.com/prometheus/client_golang v1.23.2
|
github.com/prometheus/client_golang v1.23.0
|
||||||
github.com/prometheus/common v0.66.1
|
github.com/prometheus/common v0.66.0
|
||||||
github.com/prometheus/procfs v0.17.0
|
github.com/prometheus/procfs v0.17.0
|
||||||
github.com/psanford/httpreadat v0.1.0
|
github.com/psanford/httpreadat v0.1.0
|
||||||
github.com/rs/xid v1.6.0
|
github.com/rs/xid v1.6.0
|
||||||
|
|
@ -31,32 +30,37 @@ require (
|
||||||
github.com/vearutop/statigz v1.5.0
|
github.com/vearutop/statigz v1.5.0
|
||||||
github.com/vishvananda/netlink v1.3.1
|
github.com/vishvananda/netlink v1.3.1
|
||||||
go.bug.st/serial v1.6.4
|
go.bug.st/serial v1.6.4
|
||||||
golang.org/x/crypto v0.42.0
|
golang.org/x/crypto v0.41.0
|
||||||
golang.org/x/net v0.44.0
|
golang.org/x/net v0.43.0
|
||||||
golang.org/x/sys v0.36.0
|
golang.org/x/sys v0.35.0
|
||||||
)
|
)
|
||||||
|
|
||||||
replace github.com/pojntfx/go-nbd v0.3.2 => github.com/chemhack/go-nbd v0.0.0-20241006125820-59e45f5b1e7b
|
replace github.com/pojntfx/go-nbd v0.3.2 => github.com/chemhack/go-nbd v0.0.0-20241006125820-59e45f5b1e7b
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d // indirect
|
||||||
github.com/beorn7/perks v1.0.1 // indirect
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
github.com/bytedance/sonic v1.14.0 // indirect
|
github.com/bytedance/sonic v1.13.3 // indirect
|
||||||
github.com/bytedance/sonic/loader v0.3.0 // indirect
|
github.com/bytedance/sonic/loader v0.2.4 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
github.com/cloudwego/base64x v0.1.5 // indirect
|
||||||
github.com/creack/goselect v0.1.2 // indirect
|
github.com/creack/goselect v0.1.2 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
github.com/erikdubbelboer/gspt v0.0.0-20210805194459-ce36a5128377 // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
|
||||||
github.com/gin-contrib/sse v1.1.0 // indirect
|
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||||
github.com/go-jose/go-jose/v4 v4.1.0 // indirect
|
github.com/go-jose/go-jose/v4 v4.1.0 // indirect
|
||||||
|
github.com/go-ole/go-ole v1.2.4 // indirect
|
||||||
github.com/go-playground/locales v0.14.1 // indirect
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
github.com/go-playground/validator/v10 v10.27.0 // indirect
|
github.com/go-playground/validator/v10 v10.26.0 // indirect
|
||||||
github.com/goccy/go-json v0.10.5 // indirect
|
github.com/goccy/go-json v0.10.5 // indirect
|
||||||
github.com/goccy/go-yaml v1.18.0 // indirect
|
github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc // indirect
|
||||||
github.com/jonboulle/clockwork v0.5.0 // indirect
|
github.com/jonboulle/clockwork v0.5.0 // indirect
|
||||||
|
github.com/jpillora/overseer v1.1.6 // indirect
|
||||||
|
github.com/jpillora/s3 v1.1.4 // indirect
|
||||||
github.com/json-iterator/go v1.1.12 // indirect
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
|
||||||
github.com/leodido/go-urn v1.4.0 // indirect
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
|
@ -80,22 +84,16 @@ require (
|
||||||
github.com/pion/turn/v4 v4.1.1 // indirect
|
github.com/pion/turn/v4 v4.1.1 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/prometheus/client_model v0.6.2 // indirect
|
github.com/prometheus/client_model v0.6.2 // indirect
|
||||||
github.com/quic-go/qpack v0.5.1 // indirect
|
|
||||||
github.com/quic-go/quic-go v0.54.0 // indirect
|
|
||||||
github.com/robfig/cron/v3 v3.0.1 // indirect
|
github.com/robfig/cron/v3 v3.0.1 // indirect
|
||||||
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
github.com/ugorji/go/codec v1.3.0 // indirect
|
github.com/ugorji/go/codec v1.3.0 // indirect
|
||||||
github.com/vishvananda/netns v0.0.5 // indirect
|
github.com/vishvananda/netns v0.0.5 // indirect
|
||||||
github.com/wlynxg/anet v0.0.5 // indirect
|
github.com/wlynxg/anet v0.0.5 // indirect
|
||||||
go.uber.org/mock v0.5.0 // indirect
|
golang.org/x/arch v0.18.0 // indirect
|
||||||
go.yaml.in/yaml/v2 v2.4.2 // indirect
|
|
||||||
golang.org/x/arch v0.20.0 // indirect
|
|
||||||
golang.org/x/mod v0.27.0 // indirect
|
|
||||||
golang.org/x/oauth2 v0.30.0 // indirect
|
golang.org/x/oauth2 v0.30.0 // indirect
|
||||||
golang.org/x/sync v0.17.0 // indirect
|
golang.org/x/text v0.28.0 // indirect
|
||||||
golang.org/x/text v0.29.0 // indirect
|
google.golang.org/protobuf v1.36.8 // indirect
|
||||||
golang.org/x/tools v0.36.0 // indirect
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
google.golang.org/protobuf v1.36.9 // indirect
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|
|
||||||
104
go.sum
104
go.sum
|
|
@ -1,5 +1,7 @@
|
||||||
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
|
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
|
||||||
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
|
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
|
||||||
|
github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d h1:G0m3OIz70MZUWq3EgK3CesDbo8upS2Vm9/P3FtgI+Jk=
|
||||||
|
github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
|
||||||
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
|
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
|
||||||
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
|
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
|
||||||
github.com/beevik/ntp v1.4.3 h1:PlbTvE5NNy4QHmA4Mg57n7mcFTmr1W1j3gcK7L1lqho=
|
github.com/beevik/ntp v1.4.3 h1:PlbTvE5NNy4QHmA4Mg57n7mcFTmr1W1j3gcK7L1lqho=
|
||||||
|
|
@ -8,18 +10,20 @@ 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/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||||
github.com/bool64/dev v0.2.39 h1:kP8DnMGlWXhGYJEZE/J0l/gVBdbuhoPGL+MJG4QbofE=
|
github.com/bool64/dev v0.2.39 h1:kP8DnMGlWXhGYJEZE/J0l/gVBdbuhoPGL+MJG4QbofE=
|
||||||
github.com/bool64/dev v0.2.39/go.mod h1:iJbh1y/HkunEPhgebWRNcs8wfGq7sjvJ6W5iabL8ACg=
|
github.com/bool64/dev v0.2.39/go.mod h1:iJbh1y/HkunEPhgebWRNcs8wfGq7sjvJ6W5iabL8ACg=
|
||||||
github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ=
|
github.com/bytedance/sonic v1.13.3 h1:MS8gmaH16Gtirygw7jV91pDCN33NyMrPbN7qiYhEsF0=
|
||||||
github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA=
|
github.com/bytedance/sonic v1.13.3/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
|
||||||
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
|
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||||
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY=
|
||||||
|
github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/chemhack/go-nbd v0.0.0-20241006125820-59e45f5b1e7b h1:dSbDgy72Y1sjLPWLv7vs0fMFuhMBMViiT9PJZiZWZNs=
|
github.com/chemhack/go-nbd v0.0.0-20241006125820-59e45f5b1e7b h1:dSbDgy72Y1sjLPWLv7vs0fMFuhMBMViiT9PJZiZWZNs=
|
||||||
github.com/chemhack/go-nbd v0.0.0-20241006125820-59e45f5b1e7b/go.mod h1:SehHnbi2e8NiSAKby42Itm8SIoS7b+wAprsfPH3qgYk=
|
github.com/chemhack/go-nbd v0.0.0-20241006125820-59e45f5b1e7b/go.mod h1:SehHnbi2e8NiSAKby42Itm8SIoS7b+wAprsfPH3qgYk=
|
||||||
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
|
||||||
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||||
github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g=
|
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||||
github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
|
github.com/coder/websocket v1.8.13 h1:f3QZdXy7uGVz+4uCJy2nTZyM0yTBj8yANEHhqlXZ9FE=
|
||||||
|
github.com/coder/websocket v1.8.13/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
|
||||||
github.com/coreos/go-oidc/v3 v3.15.0 h1:R6Oz8Z4bqWR7VFQ+sPSvZPQv4x8M+sJkDO5ojgwlyAg=
|
github.com/coreos/go-oidc/v3 v3.15.0 h1:R6Oz8Z4bqWR7VFQ+sPSvZPQv4x8M+sJkDO5ojgwlyAg=
|
||||||
github.com/coreos/go-oidc/v3 v3.15.0/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmrfah6hnSYEU=
|
github.com/coreos/go-oidc/v3 v3.15.0/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmrfah6hnSYEU=
|
||||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||||
|
|
@ -40,42 +44,50 @@ github.com/gin-contrib/logger v1.2.6 h1:EPolruKUTzNXMVBD9LuAFQmRjTs7AH7yKGuXgYqr
|
||||||
github.com/gin-contrib/logger v1.2.6/go.mod h1:7niPrd7F0Nscw/zvgz8RiGJxSdbKM2yfQNy8xCHcm64=
|
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 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
||||||
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
||||||
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
|
github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
|
||||||
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
|
github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||||
github.com/go-co-op/gocron/v2 v2.16.6 h1:zI2Ya9sqvuLcgqJgV79LwoJXM8h20Z/drtB7ATbpRWo=
|
github.com/go-co-op/gocron/v2 v2.16.5 h1:j228Jxk7bb9CF8LKR3gS+bK3rcjRUINjlVI+ZMp26Ss=
|
||||||
github.com/go-co-op/gocron/v2 v2.16.6/go.mod h1:zAfC/GFQ668qHxOVl/D68Jh5Ce7sDqX6TJnSQyRkRBc=
|
github.com/go-co-op/gocron/v2 v2.16.5/go.mod h1:zAfC/GFQ668qHxOVl/D68Jh5Ce7sDqX6TJnSQyRkRBc=
|
||||||
github.com/go-jose/go-jose/v4 v4.1.0 h1:cYSYxd3pw5zd2FSXk2vGdn9igQU2PS8MuxrCOCl0FdY=
|
github.com/go-jose/go-jose/v4 v4.1.0 h1:cYSYxd3pw5zd2FSXk2vGdn9igQU2PS8MuxrCOCl0FdY=
|
||||||
github.com/go-jose/go-jose/v4 v4.1.0/go.mod h1:GG/vqmYm3Von2nYiB2vGTXzdoNKE5tix5tuc6iAd+sw=
|
github.com/go-jose/go-jose/v4 v4.1.0/go.mod h1:GG/vqmYm3Von2nYiB2vGTXzdoNKE5tix5tuc6iAd+sw=
|
||||||
|
github.com/go-ole/go-ole v1.2.4 h1:nNBDSCOigTSiarFpYE9J/KtEA1IOW4CNeqT9TQDqCxI=
|
||||||
|
github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM=
|
||||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
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/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||||
github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
|
github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k=
|
||||||
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
|
github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
|
||||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||||
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
|
|
||||||
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
|
||||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc h1:GN2Lv3MGO7AS6PrRoT6yV5+wkrOpcszoIsO4+4ds248=
|
||||||
|
github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc/go.mod h1:+JKpmjMGhpgPL+rXZ5nsZieVzvarn86asRlBg4uNGnk=
|
||||||
github.com/guregu/null/v6 v6.0.0 h1:N14VRS+4di81i1PXRiprbQJ9EM9gqBa0+KVMeS/QSjQ=
|
github.com/guregu/null/v6 v6.0.0 h1:N14VRS+4di81i1PXRiprbQJ9EM9gqBa0+KVMeS/QSjQ=
|
||||||
github.com/guregu/null/v6 v6.0.0/go.mod h1:hrMIhIfrOZeLPZhROSn149tpw2gHkidAqxoXNyeX3iQ=
|
github.com/guregu/null/v6 v6.0.0/go.mod h1:hrMIhIfrOZeLPZhROSn149tpw2gHkidAqxoXNyeX3iQ=
|
||||||
github.com/gwatts/rootcerts v0.0.0-20250901182336-dc5ae18bd79f h1:08t2PbrkDgW2+mwCQ3jhKUBrCM9Bc9SeH5j2Dst3B+0=
|
github.com/gwatts/rootcerts v0.0.0-20250901182336-dc5ae18bd79f h1:08t2PbrkDgW2+mwCQ3jhKUBrCM9Bc9SeH5j2Dst3B+0=
|
||||||
github.com/gwatts/rootcerts v0.0.0-20250901182336-dc5ae18bd79f/go.mod h1:5Kt9XkWvkGi2OHOq0QsGxebHmhCcqJ8KCbNg/a6+n+g=
|
github.com/gwatts/rootcerts v0.0.0-20250901182336-dc5ae18bd79f/go.mod h1:5Kt9XkWvkGi2OHOq0QsGxebHmhCcqJ8KCbNg/a6+n+g=
|
||||||
github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I=
|
github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I=
|
||||||
github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60=
|
github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60=
|
||||||
|
github.com/jpillora/overseer v1.1.6 h1:3ygYfNcR3FfOr22miu3vR1iQcXKMHbmULBh98rbkIyo=
|
||||||
|
github.com/jpillora/overseer v1.1.6/go.mod h1:aPXQtxuVb9PVWRWTXpo+LdnC/YXQ0IBLNXqKMJmgk88=
|
||||||
|
github.com/jpillora/s3 v1.1.4 h1:YCCKDWzb/Ye9EBNd83ATRF/8wPEy0xd43Rezb6u6fzc=
|
||||||
|
github.com/jpillora/s3 v1.1.4/go.mod h1:yedE603V+crlFi1Kl/5vZJaBu9pUzE9wvKegU/lF2zs=
|
||||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
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/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
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/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||||
|
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
|
|
@ -140,20 +152,16 @@ github.com/pion/webrtc/v4 v4.1.4/go.mod h1:Oab9npu1iZtQRMic3K3toYq5zFPvToe/QBw7d
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
|
github.com/prometheus/client_golang v1.23.0 h1:ust4zpdl9r4trLY/gSjlm07PuiBq2ynaXXlptpfy8Uc=
|
||||||
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
|
github.com/prometheus/client_golang v1.23.0/go.mod h1:i/o0R9ByOnHX0McrTMTyhYvKE4haaf2mW08I+jGAjEE=
|
||||||
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||||
github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs=
|
github.com/prometheus/common v0.66.0 h1:K/rJPHrG3+AoQs50r2+0t7zMnMzek2Vbv31OFVsMeVY=
|
||||||
github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
|
github.com/prometheus/common v0.66.0/go.mod h1:Ux6NtV1B4LatamKE63tJBntoxD++xmtI/lK0VtEplN4=
|
||||||
github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0=
|
github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0=
|
||||||
github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw=
|
github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw=
|
||||||
github.com/psanford/httpreadat v0.1.0 h1:VleW1HS2zO7/4c7c7zNl33fO6oYACSagjJIyMIwZLUE=
|
github.com/psanford/httpreadat v0.1.0 h1:VleW1HS2zO7/4c7c7zNl33fO6oYACSagjJIyMIwZLUE=
|
||||||
github.com/psanford/httpreadat v0.1.0/go.mod h1:Zg7P+TlBm3bYbyHTKv/EdtSJZn3qwbPwpfZ/I9GKCRE=
|
github.com/psanford/httpreadat v0.1.0/go.mod h1:Zg7P+TlBm3bYbyHTKv/EdtSJZn3qwbPwpfZ/I9GKCRE=
|
||||||
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
|
|
||||||
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
|
|
||||||
github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg=
|
|
||||||
github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
|
|
||||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||||
|
|
@ -162,12 +170,15 @@ github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
|
||||||
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||||
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
|
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
|
||||||
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
|
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
|
||||||
|
github.com/smartystreets/assertions v1.0.1/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM=
|
||||||
|
github.com/smartystreets/gunit v1.1.3/go.mod h1:EH5qMBab2UclzXUcpR8b93eHsIlp9u+pDQIRp5DZNzQ=
|
||||||
github.com/sourcegraph/tf-dag v0.2.2-0.20250131204052-3e8ff1477b4f h1:VgoRCP1efSCEZIcF2THLQ46+pIBzzgNiaUBe9wEDwYU=
|
github.com/sourcegraph/tf-dag v0.2.2-0.20250131204052-3e8ff1477b4f h1:VgoRCP1efSCEZIcF2THLQ46+pIBzzgNiaUBe9wEDwYU=
|
||||||
github.com/sourcegraph/tf-dag v0.2.2-0.20250131204052-3e8ff1477b4f/go.mod h1:pzro7BGorij2WgrjEammtrkbo3+xldxo+KaGLGUiD+Q=
|
github.com/sourcegraph/tf-dag v0.2.2-0.20250131204052-3e8ff1477b4f/go.mod h1:pzro7BGorij2WgrjEammtrkbo3+xldxo+KaGLGUiD+Q=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
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.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.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
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.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
|
|
@ -189,40 +200,33 @@ go.bug.st/serial v1.6.4 h1:7FmqNPgVp3pu2Jz5PoPtbZ9jJO5gnEnZIvnI1lzve8A=
|
||||||
go.bug.st/serial v1.6.4/go.mod h1:nofMJxTeNVny/m6+KaafC6vJGj3miwQZ6vW4BZUGJPI=
|
go.bug.st/serial v1.6.4/go.mod h1:nofMJxTeNVny/m6+KaafC6vJGj3miwQZ6vW4BZUGJPI=
|
||||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||||
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
|
golang.org/x/arch v0.18.0 h1:WN9poc33zL4AzGxqf8VtpKUnGvMi8O9lhNyBMF/85qc=
|
||||||
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
|
golang.org/x/arch v0.18.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
|
||||||
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
|
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
|
||||||
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
|
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
|
||||||
golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c=
|
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
|
||||||
golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
|
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
|
||||||
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
|
|
||||||
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
|
|
||||||
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
|
|
||||||
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
|
|
||||||
golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
|
|
||||||
golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
|
|
||||||
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
|
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
|
||||||
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
|
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
|
||||||
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
|
||||||
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
|
||||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.2.0/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=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
|
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||||
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ=
|
golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
|
||||||
golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA=
|
golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
|
||||||
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
|
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
||||||
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
|
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
||||||
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
|
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
|
||||||
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
|
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
||||||
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
|
|
||||||
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||||
|
|
|
||||||
|
|
@ -155,6 +155,36 @@ static inline void simd_clear_samples_s16(short *buffer, int samples) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interleave L/R channels using NEON (8 frames/iteration)
|
||||||
|
* Converts separate left/right buffers to interleaved stereo (LRLRLR...)
|
||||||
|
* @param left Left channel samples
|
||||||
|
* @param right Right channel samples
|
||||||
|
* @param output Interleaved stereo output buffer
|
||||||
|
* @param frames Number of stereo frames to process
|
||||||
|
*/
|
||||||
|
static inline void simd_interleave_stereo_s16(const short *left, const short *right,
|
||||||
|
short *output, int frames) {
|
||||||
|
simd_init_once();
|
||||||
|
|
||||||
|
int simd_frames = frames & ~7;
|
||||||
|
|
||||||
|
// SIMD path: interleave 8 frames (16 samples) per iteration
|
||||||
|
for (int i = 0; i < simd_frames; i += 8) {
|
||||||
|
int16x8_t left_vec = vld1q_s16(&left[i]);
|
||||||
|
int16x8_t right_vec = vld1q_s16(&right[i]);
|
||||||
|
int16x8x2_t interleaved = vzipq_s16(left_vec, right_vec);
|
||||||
|
vst1q_s16(&output[i * 2], interleaved.val[0]);
|
||||||
|
vst1q_s16(&output[i * 2 + 8], interleaved.val[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scalar path: handle remaining frames
|
||||||
|
for (int i = simd_frames; i < frames; i++) {
|
||||||
|
output[i * 2] = left[i];
|
||||||
|
output[i * 2 + 1] = right[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Apply gain using NEON Q15 fixed-point math (8 samples/iteration)
|
* Apply gain using NEON Q15 fixed-point math (8 samples/iteration)
|
||||||
* Uses vqrdmulhq_s16 for single-instruction saturating rounded multiply-high
|
* Uses vqrdmulhq_s16 for single-instruction saturating rounded multiply-high
|
||||||
|
|
@ -184,6 +214,234 @@ static inline void simd_scale_volume_s16(short *samples, int count, float volume
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Byte-swap 16-bit samples using NEON (8 samples/iteration)
|
||||||
|
* Converts between little-endian and big-endian formats
|
||||||
|
* @param samples Audio buffer to byte-swap in-place
|
||||||
|
* @param count Number of samples to process
|
||||||
|
*/
|
||||||
|
static inline void simd_swap_endian_s16(short *samples, int count) {
|
||||||
|
int simd_count = count & ~7;
|
||||||
|
|
||||||
|
// SIMD path: swap 8 samples per iteration
|
||||||
|
for (int i = 0; i < simd_count; i += 8) {
|
||||||
|
uint16x8_t samples_vec = vld1q_u16((uint16_t*)&samples[i]);
|
||||||
|
uint8x16_t samples_u8 = vreinterpretq_u8_u16(samples_vec);
|
||||||
|
uint8x16_t swapped_u8 = vrev16q_u8(samples_u8);
|
||||||
|
uint16x8_t swapped = vreinterpretq_u16_u8(swapped_u8);
|
||||||
|
vst1q_u16((uint16_t*)&samples[i], swapped);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scalar path: handle remaining samples
|
||||||
|
for (int i = simd_count; i < count; i++) {
|
||||||
|
samples[i] = __builtin_bswap16(samples[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert S16 to float using NEON (4 samples/iteration)
|
||||||
|
* Converts 16-bit signed integers to normalized float [-1.0, 1.0]
|
||||||
|
* @param input S16 audio samples
|
||||||
|
* @param output Float output buffer
|
||||||
|
* @param count Number of samples to convert
|
||||||
|
*/
|
||||||
|
static inline void simd_s16_to_float(const short *input, float *output, int count) {
|
||||||
|
const float scale = 1.0f / 32768.0f;
|
||||||
|
int simd_count = count & ~3;
|
||||||
|
float32x4_t scale_vec = vdupq_n_f32(scale);
|
||||||
|
|
||||||
|
// SIMD path: convert 4 samples per iteration
|
||||||
|
for (int i = 0; i < simd_count; i += 4) {
|
||||||
|
int16x4_t s16_data = vld1_s16(input + i);
|
||||||
|
int32x4_t s32_data = vmovl_s16(s16_data);
|
||||||
|
float32x4_t float_data = vcvtq_f32_s32(s32_data);
|
||||||
|
float32x4_t scaled = vmulq_f32(float_data, scale_vec);
|
||||||
|
vst1q_f32(output + i, scaled);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scalar path: handle remaining samples
|
||||||
|
for (int i = simd_count; i < count; i++) {
|
||||||
|
output[i] = (float)input[i] * scale;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert float to S16 using NEON (4 samples/iteration)
|
||||||
|
* Converts normalized float [-1.0, 1.0] to 16-bit signed integers with saturation
|
||||||
|
* @param input Float audio samples
|
||||||
|
* @param output S16 output buffer
|
||||||
|
* @param count Number of samples to convert
|
||||||
|
*/
|
||||||
|
static inline void simd_float_to_s16(const float *input, short *output, int count) {
|
||||||
|
const float scale = 32767.0f;
|
||||||
|
int simd_count = count & ~3;
|
||||||
|
float32x4_t scale_vec = vdupq_n_f32(scale);
|
||||||
|
|
||||||
|
// SIMD path: convert 4 samples per iteration with saturation
|
||||||
|
for (int i = 0; i < simd_count; i += 4) {
|
||||||
|
float32x4_t float_data = vld1q_f32(input + i);
|
||||||
|
float32x4_t scaled = vmulq_f32(float_data, scale_vec);
|
||||||
|
int32x4_t s32_data = vcvtq_s32_f32(scaled);
|
||||||
|
int16x4_t s16_data = vqmovn_s32(s32_data);
|
||||||
|
vst1_s16(output + i, s16_data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scalar path: handle remaining samples with clamping
|
||||||
|
for (int i = simd_count; i < count; i++) {
|
||||||
|
float scaled = input[i] * scale;
|
||||||
|
output[i] = (short)__builtin_fmaxf(__builtin_fminf(scaled, 32767.0f), -32768.0f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mono → stereo (duplicate samples) using NEON (4 frames/iteration)
|
||||||
|
* Duplicates mono samples to both L and R channels
|
||||||
|
* @param mono Mono input buffer
|
||||||
|
* @param stereo Stereo output buffer
|
||||||
|
* @param frames Number of frames to process
|
||||||
|
*/
|
||||||
|
static inline void simd_mono_to_stereo_s16(const short *mono, short *stereo, int frames) {
|
||||||
|
int simd_frames = frames & ~3;
|
||||||
|
|
||||||
|
// SIMD path: duplicate 4 frames (8 samples) per iteration
|
||||||
|
for (int i = 0; i < simd_frames; i += 4) {
|
||||||
|
int16x4_t mono_data = vld1_s16(mono + i);
|
||||||
|
int16x4x2_t stereo_data = {mono_data, mono_data};
|
||||||
|
vst2_s16(stereo + i * 2, stereo_data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scalar path: handle remaining frames
|
||||||
|
for (int i = simd_frames; i < frames; i++) {
|
||||||
|
stereo[i * 2] = mono[i];
|
||||||
|
stereo[i * 2 + 1] = mono[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stereo → mono (average L+R) using NEON (4 frames/iteration)
|
||||||
|
* Downmixes stereo to mono by averaging left and right channels
|
||||||
|
* @param stereo Interleaved stereo input buffer
|
||||||
|
* @param mono Mono output buffer
|
||||||
|
* @param frames Number of frames to process
|
||||||
|
*/
|
||||||
|
static inline void simd_stereo_to_mono_s16(const short *stereo, short *mono, int frames) {
|
||||||
|
int simd_frames = frames & ~3;
|
||||||
|
|
||||||
|
// SIMD path: average 4 stereo frames per iteration
|
||||||
|
for (int i = 0; i < simd_frames; i += 4) {
|
||||||
|
int16x4x2_t stereo_data = vld2_s16(stereo + i * 2);
|
||||||
|
int32x4_t left_wide = vmovl_s16(stereo_data.val[0]);
|
||||||
|
int32x4_t right_wide = vmovl_s16(stereo_data.val[1]);
|
||||||
|
int32x4_t sum = vaddq_s32(left_wide, right_wide);
|
||||||
|
int32x4_t avg = vshrq_n_s32(sum, 1);
|
||||||
|
int16x4_t mono_data = vqmovn_s32(avg);
|
||||||
|
vst1_s16(mono + i, mono_data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scalar path: handle remaining frames
|
||||||
|
for (int i = simd_frames; i < frames; i++) {
|
||||||
|
mono[i] = (stereo[i * 2] + stereo[i * 2 + 1]) / 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply L/R balance using NEON (4 frames/iteration)
|
||||||
|
* Adjusts stereo balance: negative = more left, positive = more right
|
||||||
|
* @param stereo Interleaved stereo buffer to modify in-place
|
||||||
|
* @param frames Number of stereo frames to process
|
||||||
|
* @param balance Balance factor [-1.0 = full left, 0.0 = center, 1.0 = full right]
|
||||||
|
*/
|
||||||
|
static inline void simd_apply_stereo_balance_s16(short *stereo, int frames, float balance) {
|
||||||
|
int simd_frames = frames & ~3;
|
||||||
|
float left_gain = balance <= 0.0f ? 1.0f : 1.0f - balance;
|
||||||
|
float right_gain = balance >= 0.0f ? 1.0f : 1.0f + balance;
|
||||||
|
float32x4_t left_gain_vec = vdupq_n_f32(left_gain);
|
||||||
|
float32x4_t right_gain_vec = vdupq_n_f32(right_gain);
|
||||||
|
|
||||||
|
// SIMD path: apply balance to 4 stereo frames per iteration
|
||||||
|
for (int i = 0; i < simd_frames; i += 4) {
|
||||||
|
int16x4x2_t stereo_data = vld2_s16(stereo + i * 2);
|
||||||
|
int32x4_t left_wide = vmovl_s16(stereo_data.val[0]);
|
||||||
|
int32x4_t right_wide = vmovl_s16(stereo_data.val[1]);
|
||||||
|
float32x4_t left_float = vcvtq_f32_s32(left_wide);
|
||||||
|
float32x4_t right_float = vcvtq_f32_s32(right_wide);
|
||||||
|
left_float = vmulq_f32(left_float, left_gain_vec);
|
||||||
|
right_float = vmulq_f32(right_float, right_gain_vec);
|
||||||
|
int32x4_t left_result = vcvtq_s32_f32(left_float);
|
||||||
|
int32x4_t right_result = vcvtq_s32_f32(right_float);
|
||||||
|
stereo_data.val[0] = vqmovn_s32(left_result);
|
||||||
|
stereo_data.val[1] = vqmovn_s32(right_result);
|
||||||
|
vst2_s16(stereo + i * 2, stereo_data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scalar path: handle remaining frames
|
||||||
|
for (int i = simd_frames; i < frames; i++) {
|
||||||
|
stereo[i * 2] = (short)(stereo[i * 2] * left_gain);
|
||||||
|
stereo[i * 2 + 1] = (short)(stereo[i * 2 + 1] * right_gain);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deinterleave stereo → L/R channels using NEON (4 frames/iteration)
|
||||||
|
* Separates interleaved stereo (LRLRLR...) into separate L and R buffers
|
||||||
|
* @param interleaved Interleaved stereo input buffer
|
||||||
|
* @param left Left channel output buffer
|
||||||
|
* @param right Right channel output buffer
|
||||||
|
* @param frames Number of stereo frames to process
|
||||||
|
*/
|
||||||
|
static inline void simd_deinterleave_stereo_s16(const short *interleaved, short *left,
|
||||||
|
short *right, int frames) {
|
||||||
|
int simd_frames = frames & ~3;
|
||||||
|
|
||||||
|
// SIMD path: deinterleave 4 frames (8 samples) per iteration
|
||||||
|
for (int i = 0; i < simd_frames; i += 4) {
|
||||||
|
int16x4x2_t stereo_data = vld2_s16(interleaved + i * 2);
|
||||||
|
vst1_s16(left + i, stereo_data.val[0]);
|
||||||
|
vst1_s16(right + i, stereo_data.val[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scalar path: handle remaining frames
|
||||||
|
for (int i = simd_frames; i < frames; i++) {
|
||||||
|
left[i] = interleaved[i * 2];
|
||||||
|
right[i] = interleaved[i * 2 + 1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find max absolute sample value for silence detection using NEON (8 samples/iteration)
|
||||||
|
* Used to detect silence (threshold < 50 = ~0.15% max volume) and audio discontinuities
|
||||||
|
* @param samples Audio buffer to analyze
|
||||||
|
* @param count Number of samples to process
|
||||||
|
* @return Maximum absolute sample value in the buffer
|
||||||
|
*/
|
||||||
|
static inline short simd_find_max_abs_s16(const short *samples, int count) {
|
||||||
|
int simd_count = count & ~7;
|
||||||
|
int16x8_t max_vec = vdupq_n_s16(0);
|
||||||
|
|
||||||
|
// SIMD path: find max of 8 samples per iteration
|
||||||
|
for (int i = 0; i < simd_count; i += 8) {
|
||||||
|
int16x8_t samples_vec = vld1q_s16(&samples[i]);
|
||||||
|
int16x8_t abs_vec = vabsq_s16(samples_vec);
|
||||||
|
max_vec = vmaxq_s16(max_vec, abs_vec);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Horizontal reduction: extract single max value from vector
|
||||||
|
int16x4_t max_half = vmax_s16(vget_low_s16(max_vec), vget_high_s16(max_vec));
|
||||||
|
int16x4_t max_folded = vpmax_s16(max_half, max_half);
|
||||||
|
max_folded = vpmax_s16(max_folded, max_folded);
|
||||||
|
short max_sample = vget_lane_s16(max_folded, 0);
|
||||||
|
|
||||||
|
// Scalar path: handle remaining samples
|
||||||
|
for (int i = simd_count; i < count; i++) {
|
||||||
|
short abs_sample = samples[i] < 0 ? -samples[i] : samples[i];
|
||||||
|
if (abs_sample > max_sample) {
|
||||||
|
max_sample = abs_sample;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return max_sample;
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// INITIALIZATION STATE TRACKING
|
// INITIALIZATION STATE TRACKING
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
|
||||||
|
|
@ -1,81 +0,0 @@
|
||||||
/*
|
|
||||||
* JetKVM Audio Common Utilities
|
|
||||||
*
|
|
||||||
* Shared functions used by both audio input and output servers
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "audio_common.h"
|
|
||||||
#include <stdio.h>
|
|
||||||
#include <stdlib.h>
|
|
||||||
#include <string.h>
|
|
||||||
#include <signal.h>
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// GLOBAL STATE FOR SIGNAL HANDLER
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
// Pointer to the running flag that will be set to 0 on shutdown
|
|
||||||
static volatile sig_atomic_t *g_running_ptr = NULL;
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// SIGNAL HANDLERS
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
static void signal_handler(int signo) {
|
|
||||||
if (signo == SIGTERM || signo == SIGINT) {
|
|
||||||
printf("Audio server: Received signal %d, shutting down...\n", signo);
|
|
||||||
if (g_running_ptr != NULL) {
|
|
||||||
*g_running_ptr = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void audio_common_setup_signal_handlers(volatile sig_atomic_t *running) {
|
|
||||||
g_running_ptr = running;
|
|
||||||
|
|
||||||
struct sigaction sa;
|
|
||||||
memset(&sa, 0, sizeof(sa));
|
|
||||||
sa.sa_handler = signal_handler;
|
|
||||||
sigemptyset(&sa.sa_mask);
|
|
||||||
sa.sa_flags = 0;
|
|
||||||
|
|
||||||
sigaction(SIGTERM, &sa, NULL);
|
|
||||||
sigaction(SIGINT, &sa, NULL);
|
|
||||||
|
|
||||||
// Ignore SIGPIPE (write to closed socket should return error, not crash)
|
|
||||||
signal(SIGPIPE, SIG_IGN);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// CONFIGURATION PARSING
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
int audio_common_parse_env_int(const char *name, int default_value) {
|
|
||||||
const char *str = getenv(name);
|
|
||||||
if (str == NULL || str[0] == '\0') {
|
|
||||||
return default_value;
|
|
||||||
}
|
|
||||||
return atoi(str);
|
|
||||||
}
|
|
||||||
|
|
||||||
const char* audio_common_parse_env_string(const char *name, const char *default_value) {
|
|
||||||
const char *str = getenv(name);
|
|
||||||
if (str == NULL || str[0] == '\0') {
|
|
||||||
return default_value;
|
|
||||||
}
|
|
||||||
return str;
|
|
||||||
}
|
|
||||||
|
|
||||||
int audio_common_is_trace_enabled(void) {
|
|
||||||
const char *pion_trace = getenv("PION_LOG_TRACE");
|
|
||||||
if (pion_trace == NULL) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if "audio" is in comma-separated list
|
|
||||||
if (strstr(pion_trace, "audio") != NULL) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
@ -1,57 +0,0 @@
|
||||||
/*
|
|
||||||
* JetKVM Audio Common Utilities
|
|
||||||
*
|
|
||||||
* Shared functions used by both audio input and output servers
|
|
||||||
*/
|
|
||||||
|
|
||||||
#ifndef JETKVM_AUDIO_COMMON_H
|
|
||||||
#define JETKVM_AUDIO_COMMON_H
|
|
||||||
|
|
||||||
#include <signal.h>
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// SIGNAL HANDLERS
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Setup signal handlers for graceful shutdown.
|
|
||||||
* Handles SIGTERM and SIGINT by setting the running flag to 0.
|
|
||||||
* Ignores SIGPIPE to prevent crashes on broken pipe writes.
|
|
||||||
*
|
|
||||||
* @param running Pointer to the volatile running flag to set on shutdown
|
|
||||||
*/
|
|
||||||
void audio_common_setup_signal_handlers(volatile sig_atomic_t *running);
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// CONFIGURATION PARSING
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse integer from environment variable.
|
|
||||||
* Returns default_value if variable is not set or empty.
|
|
||||||
*
|
|
||||||
* @param name Environment variable name
|
|
||||||
* @param default_value Default value if not set
|
|
||||||
* @return Parsed integer value or default
|
|
||||||
*/
|
|
||||||
int audio_common_parse_env_int(const char *name, int default_value);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse string from environment variable.
|
|
||||||
* Returns default_value if variable is not set or empty.
|
|
||||||
*
|
|
||||||
* @param name Environment variable name
|
|
||||||
* @param default_value Default value if not set
|
|
||||||
* @return Environment variable value or default (not duplicated)
|
|
||||||
*/
|
|
||||||
const char* audio_common_parse_env_string(const char *name, const char *default_value);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if trace logging is enabled for audio subsystem.
|
|
||||||
* Looks for "audio" in PION_LOG_TRACE comma-separated list.
|
|
||||||
*
|
|
||||||
* @return 1 if enabled, 0 otherwise
|
|
||||||
*/
|
|
||||||
int audio_common_is_trace_enabled(void);
|
|
||||||
|
|
||||||
#endif // JETKVM_AUDIO_COMMON_H
|
|
||||||
|
|
@ -11,7 +11,6 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "ipc_protocol.h"
|
#include "ipc_protocol.h"
|
||||||
#include "audio_common.h"
|
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
@ -49,25 +48,80 @@ typedef struct {
|
||||||
int trace_logging; // Enable trace logging (default: 0)
|
int trace_logging; // Enable trace logging (default: 0)
|
||||||
} audio_config_t;
|
} audio_config_t;
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// SIGNAL HANDLERS
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
static void signal_handler(int signo) {
|
||||||
|
if (signo == SIGTERM || signo == SIGINT) {
|
||||||
|
printf("Audio input server: Received signal %d, shutting down...\n", signo);
|
||||||
|
g_running = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void setup_signal_handlers(void) {
|
||||||
|
struct sigaction sa;
|
||||||
|
memset(&sa, 0, sizeof(sa));
|
||||||
|
sa.sa_handler = signal_handler;
|
||||||
|
sigemptyset(&sa.sa_mask);
|
||||||
|
sa.sa_flags = 0;
|
||||||
|
|
||||||
|
sigaction(SIGTERM, &sa, NULL);
|
||||||
|
sigaction(SIGINT, &sa, NULL);
|
||||||
|
|
||||||
|
// Ignore SIGPIPE
|
||||||
|
signal(SIGPIPE, SIG_IGN);
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// CONFIGURATION PARSING
|
// CONFIGURATION PARSING
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
|
static int parse_env_int(const char *name, int default_value) {
|
||||||
|
const char *str = getenv(name);
|
||||||
|
if (str == NULL || str[0] == '\0') {
|
||||||
|
return default_value;
|
||||||
|
}
|
||||||
|
return atoi(str);
|
||||||
|
}
|
||||||
|
|
||||||
|
static const char* parse_env_string(const char *name, const char *default_value) {
|
||||||
|
const char *str = getenv(name);
|
||||||
|
if (str == NULL || str[0] == '\0') {
|
||||||
|
return default_value;
|
||||||
|
}
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int is_trace_enabled(void) {
|
||||||
|
const char *pion_trace = getenv("PION_LOG_TRACE");
|
||||||
|
if (pion_trace == NULL) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if "audio" is in comma-separated list
|
||||||
|
if (strstr(pion_trace, "audio") != NULL) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
static void load_audio_config(audio_config_t *config) {
|
static void load_audio_config(audio_config_t *config) {
|
||||||
// ALSA device configuration
|
// ALSA device configuration
|
||||||
config->alsa_device = audio_common_parse_env_string("ALSA_PLAYBACK_DEVICE", "hw:1,0");
|
config->alsa_device = parse_env_string("ALSA_PLAYBACK_DEVICE", "hw:1,0");
|
||||||
|
|
||||||
// Opus configuration (informational only for decoder)
|
// Opus configuration (informational only for decoder)
|
||||||
config->opus_bitrate = audio_common_parse_env_int("OPUS_BITRATE", 96000);
|
config->opus_bitrate = parse_env_int("OPUS_BITRATE", 96000);
|
||||||
config->opus_complexity = audio_common_parse_env_int("OPUS_COMPLEXITY", 1);
|
config->opus_complexity = parse_env_int("OPUS_COMPLEXITY", 1);
|
||||||
|
|
||||||
// Audio format
|
// Audio format
|
||||||
config->sample_rate = audio_common_parse_env_int("AUDIO_SAMPLE_RATE", 48000);
|
config->sample_rate = parse_env_int("AUDIO_SAMPLE_RATE", 48000);
|
||||||
config->channels = audio_common_parse_env_int("AUDIO_CHANNELS", 2);
|
config->channels = parse_env_int("AUDIO_CHANNELS", 2);
|
||||||
config->frame_size = audio_common_parse_env_int("AUDIO_FRAME_SIZE", 960);
|
config->frame_size = parse_env_int("AUDIO_FRAME_SIZE", 960);
|
||||||
|
|
||||||
// Logging
|
// Logging
|
||||||
config->trace_logging = audio_common_is_trace_enabled();
|
config->trace_logging = is_trace_enabled();
|
||||||
|
|
||||||
// Log configuration
|
// Log configuration
|
||||||
printf("Audio Input Server Configuration:\n");
|
printf("Audio Input Server Configuration:\n");
|
||||||
|
|
@ -215,7 +269,7 @@ int main(int argc, char **argv) {
|
||||||
printf("JetKVM Audio Input Server Starting...\n");
|
printf("JetKVM Audio Input Server Starting...\n");
|
||||||
|
|
||||||
// Setup signal handlers
|
// Setup signal handlers
|
||||||
audio_common_setup_signal_handlers(&g_running);
|
setup_signal_handlers();
|
||||||
|
|
||||||
// Load configuration from environment
|
// Load configuration from environment
|
||||||
audio_config_t config;
|
audio_config_t config;
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "ipc_protocol.h"
|
#include "ipc_protocol.h"
|
||||||
#include "audio_common.h"
|
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
@ -52,31 +51,86 @@ typedef struct {
|
||||||
int trace_logging; // Enable trace logging (default: 0)
|
int trace_logging; // Enable trace logging (default: 0)
|
||||||
} audio_config_t;
|
} audio_config_t;
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// SIGNAL HANDLERS
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
static void signal_handler(int signo) {
|
||||||
|
if (signo == SIGTERM || signo == SIGINT) {
|
||||||
|
printf("Audio output server: Received signal %d, shutting down...\n", signo);
|
||||||
|
g_running = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void setup_signal_handlers(void) {
|
||||||
|
struct sigaction sa;
|
||||||
|
memset(&sa, 0, sizeof(sa));
|
||||||
|
sa.sa_handler = signal_handler;
|
||||||
|
sigemptyset(&sa.sa_mask);
|
||||||
|
sa.sa_flags = 0;
|
||||||
|
|
||||||
|
sigaction(SIGTERM, &sa, NULL);
|
||||||
|
sigaction(SIGINT, &sa, NULL);
|
||||||
|
|
||||||
|
// Ignore SIGPIPE (write to closed socket should return error, not crash)
|
||||||
|
signal(SIGPIPE, SIG_IGN);
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// CONFIGURATION PARSING
|
// CONFIGURATION PARSING
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
|
static int parse_env_int(const char *name, int default_value) {
|
||||||
|
const char *str = getenv(name);
|
||||||
|
if (str == NULL || str[0] == '\0') {
|
||||||
|
return default_value;
|
||||||
|
}
|
||||||
|
return atoi(str);
|
||||||
|
}
|
||||||
|
|
||||||
|
static const char* parse_env_string(const char *name, const char *default_value) {
|
||||||
|
const char *str = getenv(name);
|
||||||
|
if (str == NULL || str[0] == '\0') {
|
||||||
|
return default_value;
|
||||||
|
}
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int is_trace_enabled(void) {
|
||||||
|
const char *pion_trace = getenv("PION_LOG_TRACE");
|
||||||
|
if (pion_trace == NULL) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if "audio" is in comma-separated list
|
||||||
|
if (strstr(pion_trace, "audio") != NULL) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
static void load_audio_config(audio_config_t *config) {
|
static void load_audio_config(audio_config_t *config) {
|
||||||
// ALSA device configuration
|
// ALSA device configuration
|
||||||
config->alsa_device = audio_common_parse_env_string("ALSA_CAPTURE_DEVICE", "hw:0,0");
|
config->alsa_device = parse_env_string("ALSA_CAPTURE_DEVICE", "hw:0,0");
|
||||||
|
|
||||||
// Opus encoder configuration
|
// Opus encoder configuration
|
||||||
config->opus_bitrate = audio_common_parse_env_int("OPUS_BITRATE", 96000);
|
config->opus_bitrate = parse_env_int("OPUS_BITRATE", 96000);
|
||||||
config->opus_complexity = audio_common_parse_env_int("OPUS_COMPLEXITY", 1);
|
config->opus_complexity = parse_env_int("OPUS_COMPLEXITY", 1);
|
||||||
config->opus_vbr = audio_common_parse_env_int("OPUS_VBR", 1);
|
config->opus_vbr = parse_env_int("OPUS_VBR", 1);
|
||||||
config->opus_vbr_constraint = audio_common_parse_env_int("OPUS_VBR_CONSTRAINT", 1);
|
config->opus_vbr_constraint = parse_env_int("OPUS_VBR_CONSTRAINT", 1);
|
||||||
config->opus_signal_type = audio_common_parse_env_int("OPUS_SIGNAL_TYPE", -1000);
|
config->opus_signal_type = parse_env_int("OPUS_SIGNAL_TYPE", -1000);
|
||||||
config->opus_bandwidth = audio_common_parse_env_int("OPUS_BANDWIDTH", 1103);
|
config->opus_bandwidth = parse_env_int("OPUS_BANDWIDTH", 1103);
|
||||||
config->opus_dtx = audio_common_parse_env_int("OPUS_DTX", 0);
|
config->opus_dtx = parse_env_int("OPUS_DTX", 0);
|
||||||
config->opus_lsb_depth = audio_common_parse_env_int("OPUS_LSB_DEPTH", 16);
|
config->opus_lsb_depth = parse_env_int("OPUS_LSB_DEPTH", 16);
|
||||||
|
|
||||||
// Audio format
|
// Audio format
|
||||||
config->sample_rate = audio_common_parse_env_int("AUDIO_SAMPLE_RATE", 48000);
|
config->sample_rate = parse_env_int("AUDIO_SAMPLE_RATE", 48000);
|
||||||
config->channels = audio_common_parse_env_int("AUDIO_CHANNELS", 2);
|
config->channels = parse_env_int("AUDIO_CHANNELS", 2);
|
||||||
config->frame_size = audio_common_parse_env_int("AUDIO_FRAME_SIZE", 960);
|
config->frame_size = parse_env_int("AUDIO_FRAME_SIZE", 960);
|
||||||
|
|
||||||
// Logging
|
// Logging
|
||||||
config->trace_logging = audio_common_is_trace_enabled();
|
config->trace_logging = is_trace_enabled();
|
||||||
|
|
||||||
// Log configuration
|
// Log configuration
|
||||||
printf("Audio Output Server Configuration:\n");
|
printf("Audio Output Server Configuration:\n");
|
||||||
|
|
@ -256,7 +310,7 @@ int main(int argc, char **argv) {
|
||||||
printf("JetKVM Audio Output Server Starting...\n");
|
printf("JetKVM Audio Output Server Starting...\n");
|
||||||
|
|
||||||
// Setup signal handlers
|
// Setup signal handlers
|
||||||
audio_common_setup_signal_handlers(&g_running);
|
setup_signal_handlers();
|
||||||
|
|
||||||
// Load configuration from environment
|
// Load configuration from environment
|
||||||
audio_config_t config;
|
audio_config_t config;
|
||||||
|
|
|
||||||
|
|
@ -82,6 +82,36 @@ func GetAudioInputBinaryPath() string {
|
||||||
return audioInputBinPath
|
return audioInputBinPath
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CleanupBinaries removes extracted audio binaries (useful for cleanup/testing)
|
||||||
|
func CleanupBinaries() error {
|
||||||
|
var errs []error
|
||||||
|
|
||||||
|
if err := os.Remove(audioOutputBinPath); err != nil && !os.IsNotExist(err) {
|
||||||
|
errs = append(errs, fmt.Errorf("failed to remove audio output binary: %w", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.Remove(audioInputBinPath); err != nil && !os.IsNotExist(err) {
|
||||||
|
errs = append(errs, fmt.Errorf("failed to remove audio input binary: %w", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to remove directory (will only succeed if empty)
|
||||||
|
os.Remove(audioBinDir)
|
||||||
|
|
||||||
|
if len(errs) > 0 {
|
||||||
|
return fmt.Errorf("cleanup errors: %v", errs)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBinaryInfo returns information about embedded binaries
|
||||||
|
func GetBinaryInfo() map[string]int {
|
||||||
|
return map[string]int{
|
||||||
|
"audio_output_size": len(audioOutputBinary),
|
||||||
|
"audio_input_size": len(audioInputBinary),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// init ensures binaries are extracted when package is imported
|
// init ensures binaries are extracted when package is imported
|
||||||
func init() {
|
func init() {
|
||||||
// Extract binaries on package initialization
|
// Extract binaries on package initialization
|
||||||
|
|
|
||||||
|
|
@ -114,10 +114,9 @@ type UnifiedAudioServer struct {
|
||||||
wg sync.WaitGroup // Wait group for goroutine coordination
|
wg sync.WaitGroup // Wait group for goroutine coordination
|
||||||
|
|
||||||
// Configuration
|
// Configuration
|
||||||
socketPath string
|
socketPath string
|
||||||
magicNumber uint32
|
magicNumber uint32
|
||||||
sendBufferSize int
|
socketBufferConfig SocketBufferConfig
|
||||||
recvBufferSize int
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewUnifiedAudioServer creates a new unified audio server
|
// NewUnifiedAudioServer creates a new unified audio server
|
||||||
|
|
@ -144,8 +143,7 @@ func NewUnifiedAudioServer(isInput bool) (*UnifiedAudioServer, error) {
|
||||||
magicNumber: magicNumber,
|
magicNumber: magicNumber,
|
||||||
messageChan: make(chan *UnifiedIPCMessage, Config.ChannelBufferSize),
|
messageChan: make(chan *UnifiedIPCMessage, Config.ChannelBufferSize),
|
||||||
processChan: make(chan *UnifiedIPCMessage, Config.ChannelBufferSize),
|
processChan: make(chan *UnifiedIPCMessage, Config.ChannelBufferSize),
|
||||||
sendBufferSize: Config.SocketOptimalBuffer,
|
socketBufferConfig: DefaultSocketBufferConfig(),
|
||||||
recvBufferSize: Config.SocketOptimalBuffer,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return server, nil
|
return server, nil
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,127 @@
|
||||||
|
package audio
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MicrophoneContentionManager manages microphone access with cooldown periods
|
||||||
|
type MicrophoneContentionManager struct {
|
||||||
|
// Atomic fields MUST be first for ARM32 alignment (int64 fields need 8-byte alignment)
|
||||||
|
lastOpNano int64
|
||||||
|
cooldownNanos int64
|
||||||
|
operationID int64
|
||||||
|
|
||||||
|
lockPtr unsafe.Pointer
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMicrophoneContentionManager(cooldown time.Duration) *MicrophoneContentionManager {
|
||||||
|
return &MicrophoneContentionManager{
|
||||||
|
cooldownNanos: int64(cooldown),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type OperationResult struct {
|
||||||
|
Allowed bool
|
||||||
|
RemainingCooldown time.Duration
|
||||||
|
OperationID int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mcm *MicrophoneContentionManager) TryOperation() OperationResult {
|
||||||
|
now := time.Now().UnixNano()
|
||||||
|
cooldown := atomic.LoadInt64(&mcm.cooldownNanos)
|
||||||
|
lastOp := atomic.LoadInt64(&mcm.lastOpNano)
|
||||||
|
elapsed := now - lastOp
|
||||||
|
|
||||||
|
if elapsed >= cooldown {
|
||||||
|
if atomic.CompareAndSwapInt64(&mcm.lastOpNano, lastOp, now) {
|
||||||
|
opID := atomic.AddInt64(&mcm.operationID, 1)
|
||||||
|
return OperationResult{
|
||||||
|
Allowed: true,
|
||||||
|
RemainingCooldown: 0,
|
||||||
|
OperationID: opID,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Retry once if CAS failed
|
||||||
|
lastOp = atomic.LoadInt64(&mcm.lastOpNano)
|
||||||
|
elapsed = now - lastOp
|
||||||
|
if elapsed >= cooldown && atomic.CompareAndSwapInt64(&mcm.lastOpNano, lastOp, now) {
|
||||||
|
opID := atomic.AddInt64(&mcm.operationID, 1)
|
||||||
|
return OperationResult{
|
||||||
|
Allowed: true,
|
||||||
|
RemainingCooldown: 0,
|
||||||
|
OperationID: opID,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
remaining := time.Duration(cooldown - elapsed)
|
||||||
|
if remaining < 0 {
|
||||||
|
remaining = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return OperationResult{
|
||||||
|
Allowed: false,
|
||||||
|
RemainingCooldown: remaining,
|
||||||
|
OperationID: atomic.LoadInt64(&mcm.operationID),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mcm *MicrophoneContentionManager) SetCooldown(cooldown time.Duration) {
|
||||||
|
atomic.StoreInt64(&mcm.cooldownNanos, int64(cooldown))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mcm *MicrophoneContentionManager) GetCooldown() time.Duration {
|
||||||
|
return time.Duration(atomic.LoadInt64(&mcm.cooldownNanos))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mcm *MicrophoneContentionManager) GetLastOperationTime() time.Time {
|
||||||
|
nanos := atomic.LoadInt64(&mcm.lastOpNano)
|
||||||
|
if nanos == 0 {
|
||||||
|
return time.Time{}
|
||||||
|
}
|
||||||
|
return time.Unix(0, nanos)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mcm *MicrophoneContentionManager) GetOperationCount() int64 {
|
||||||
|
return atomic.LoadInt64(&mcm.operationID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mcm *MicrophoneContentionManager) Reset() {
|
||||||
|
atomic.StoreInt64(&mcm.lastOpNano, 0)
|
||||||
|
atomic.StoreInt64(&mcm.operationID, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
globalMicContentionManager unsafe.Pointer
|
||||||
|
micContentionInitialized int32
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetMicrophoneContentionManager() *MicrophoneContentionManager {
|
||||||
|
ptr := atomic.LoadPointer(&globalMicContentionManager)
|
||||||
|
if ptr != nil {
|
||||||
|
return (*MicrophoneContentionManager)(ptr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if atomic.CompareAndSwapInt32(&micContentionInitialized, 0, 1) {
|
||||||
|
manager := NewMicrophoneContentionManager(Config.MicContentionTimeout)
|
||||||
|
atomic.StorePointer(&globalMicContentionManager, unsafe.Pointer(manager))
|
||||||
|
return manager
|
||||||
|
}
|
||||||
|
|
||||||
|
ptr = atomic.LoadPointer(&globalMicContentionManager)
|
||||||
|
if ptr != nil {
|
||||||
|
return (*MicrophoneContentionManager)(ptr)
|
||||||
|
}
|
||||||
|
|
||||||
|
return NewMicrophoneContentionManager(Config.MicContentionTimeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TryMicrophoneOperation() OperationResult {
|
||||||
|
return GetMicrophoneContentionManager().TryOperation()
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetMicrophoneCooldown(cooldown time.Duration) {
|
||||||
|
GetMicrophoneContentionManager().SetCooldown(cooldown)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,166 @@
|
||||||
|
package audio
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Socket buffer sizes are now centralized in config_constants.go
|
||||||
|
|
||||||
|
// SocketBufferConfig holds socket buffer configuration
|
||||||
|
type SocketBufferConfig struct {
|
||||||
|
SendBufferSize int
|
||||||
|
RecvBufferSize int
|
||||||
|
Enabled bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultSocketBufferConfig returns the default socket buffer configuration
|
||||||
|
func DefaultSocketBufferConfig() SocketBufferConfig {
|
||||||
|
return SocketBufferConfig{
|
||||||
|
SendBufferSize: Config.SocketOptimalBuffer,
|
||||||
|
RecvBufferSize: Config.SocketOptimalBuffer,
|
||||||
|
Enabled: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HighLoadSocketBufferConfig returns configuration for high-load scenarios
|
||||||
|
func HighLoadSocketBufferConfig() SocketBufferConfig {
|
||||||
|
return SocketBufferConfig{
|
||||||
|
SendBufferSize: Config.SocketMaxBuffer,
|
||||||
|
RecvBufferSize: Config.SocketMaxBuffer,
|
||||||
|
Enabled: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConfigureSocketBuffers applies socket buffer configuration to a Unix socket connection
|
||||||
|
func ConfigureSocketBuffers(conn net.Conn, config SocketBufferConfig) error {
|
||||||
|
if !config.Enabled {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ValidateSocketBufferConfig(config); err != nil {
|
||||||
|
return fmt.Errorf("invalid socket buffer config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
unixConn, ok := conn.(*net.UnixConn)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("connection is not a Unix socket")
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := unixConn.File()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get socket file descriptor: %w", err)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
fd := int(file.Fd())
|
||||||
|
|
||||||
|
if config.SendBufferSize > 0 {
|
||||||
|
if err := syscall.SetsockoptInt(fd, syscall.SOL_SOCKET, syscall.SO_SNDBUF, config.SendBufferSize); err != nil {
|
||||||
|
return fmt.Errorf("failed to set SO_SNDBUF to %d: %w", config.SendBufferSize, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.RecvBufferSize > 0 {
|
||||||
|
if err := syscall.SetsockoptInt(fd, syscall.SOL_SOCKET, syscall.SO_RCVBUF, config.RecvBufferSize); err != nil {
|
||||||
|
return fmt.Errorf("failed to set SO_RCVBUF to %d: %w", config.RecvBufferSize, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSocketBufferSizes retrieves current socket buffer sizes
|
||||||
|
func GetSocketBufferSizes(conn net.Conn) (sendSize, recvSize int, err error) {
|
||||||
|
unixConn, ok := conn.(*net.UnixConn)
|
||||||
|
if !ok {
|
||||||
|
return 0, 0, fmt.Errorf("socket buffer query only supported for Unix sockets")
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := unixConn.File()
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, fmt.Errorf("failed to get socket file descriptor: %w", err)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
fd := int(file.Fd())
|
||||||
|
|
||||||
|
// Get send buffer size
|
||||||
|
sendSize, err = syscall.GetsockoptInt(fd, syscall.SOL_SOCKET, syscall.SO_SNDBUF)
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, fmt.Errorf("failed to get SO_SNDBUF: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get receive buffer size
|
||||||
|
recvSize, err = syscall.GetsockoptInt(fd, syscall.SOL_SOCKET, syscall.SO_RCVBUF)
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, fmt.Errorf("failed to get SO_RCVBUF: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return sendSize, recvSize, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateSocketBufferConfig validates socket buffer configuration parameters.
|
||||||
|
//
|
||||||
|
// Validation Rules:
|
||||||
|
// - If config.Enabled is false, no validation is performed (returns nil)
|
||||||
|
// - SendBufferSize must be >= SocketMinBuffer (default: 8192 bytes)
|
||||||
|
// - RecvBufferSize must be >= SocketMinBuffer (default: 8192 bytes)
|
||||||
|
// - SendBufferSize must be <= SocketMaxBuffer (default: 1048576 bytes)
|
||||||
|
// - RecvBufferSize must be <= SocketMaxBuffer (default: 1048576 bytes)
|
||||||
|
//
|
||||||
|
// Error Conditions:
|
||||||
|
// - Returns error if send buffer size is below minimum threshold
|
||||||
|
// - Returns error if receive buffer size is below minimum threshold
|
||||||
|
// - Returns error if send buffer size exceeds maximum threshold
|
||||||
|
// - Returns error if receive buffer size exceeds maximum threshold
|
||||||
|
//
|
||||||
|
// The validation ensures socket buffers are sized appropriately for audio streaming
|
||||||
|
// performance while preventing excessive memory usage.
|
||||||
|
func ValidateSocketBufferConfig(config SocketBufferConfig) error {
|
||||||
|
if !config.Enabled {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
minBuffer := Config.SocketMinBuffer
|
||||||
|
maxBuffer := Config.SocketMaxBuffer
|
||||||
|
|
||||||
|
if config.SendBufferSize < minBuffer {
|
||||||
|
return fmt.Errorf("send buffer size validation failed: got %d bytes, minimum required %d bytes (configured range: %d-%d)",
|
||||||
|
config.SendBufferSize, minBuffer, minBuffer, maxBuffer)
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.RecvBufferSize < minBuffer {
|
||||||
|
return fmt.Errorf("receive buffer size validation failed: got %d bytes, minimum required %d bytes (configured range: %d-%d)",
|
||||||
|
config.RecvBufferSize, minBuffer, minBuffer, maxBuffer)
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.SendBufferSize > maxBuffer {
|
||||||
|
return fmt.Errorf("send buffer size validation failed: got %d bytes, maximum allowed %d bytes (configured range: %d-%d)",
|
||||||
|
config.SendBufferSize, maxBuffer, minBuffer, maxBuffer)
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.RecvBufferSize > maxBuffer {
|
||||||
|
return fmt.Errorf("receive buffer size validation failed: got %d bytes, maximum allowed %d bytes (configured range: %d-%d)",
|
||||||
|
config.RecvBufferSize, maxBuffer, minBuffer, maxBuffer)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RecordSocketBufferMetrics records socket buffer metrics for monitoring
|
||||||
|
func RecordSocketBufferMetrics(conn net.Conn, component string) {
|
||||||
|
if conn == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get current socket buffer sizes
|
||||||
|
_, _, err := GetSocketBufferSizes(conn)
|
||||||
|
if err != nil {
|
||||||
|
// Log error but don't fail
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Socket buffer sizes recorded for debugging if needed
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
package audio
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/jetkvm/kvm/internal/logging"
|
||||||
|
)
|
||||||
|
|
||||||
|
// getEnvInt reads an integer value from environment variable with fallback to default
|
||||||
|
func getEnvInt(key string, defaultValue int) int {
|
||||||
|
if value := os.Getenv(key); value != "" {
|
||||||
|
if intValue, err := strconv.Atoi(value); err == nil {
|
||||||
|
return intValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseOpusConfig reads OPUS configuration from environment variables
|
||||||
|
// with fallback to default config values
|
||||||
|
func parseOpusConfig() (bitrate, complexity, vbr, signalType, bandwidth, dtx int) {
|
||||||
|
// Read configuration from environment variables with config defaults
|
||||||
|
bitrate = getEnvInt("JETKVM_OPUS_BITRATE", Config.CGOOpusBitrate)
|
||||||
|
complexity = getEnvInt("JETKVM_OPUS_COMPLEXITY", Config.CGOOpusComplexity)
|
||||||
|
vbr = getEnvInt("JETKVM_OPUS_VBR", Config.CGOOpusVBR)
|
||||||
|
signalType = getEnvInt("JETKVM_OPUS_SIGNAL_TYPE", Config.CGOOpusSignalType)
|
||||||
|
bandwidth = getEnvInt("JETKVM_OPUS_BANDWIDTH", Config.CGOOpusBandwidth)
|
||||||
|
dtx = getEnvInt("JETKVM_OPUS_DTX", Config.CGOOpusDTX)
|
||||||
|
|
||||||
|
return bitrate, complexity, vbr, signalType, bandwidth, dtx
|
||||||
|
}
|
||||||
|
|
||||||
|
// applyOpusConfig applies OPUS configuration to the global config
|
||||||
|
// with optional logging for the specified component
|
||||||
|
func applyOpusConfig(bitrate, complexity, vbr, signalType, bandwidth, dtx int, component string, enableLogging bool) {
|
||||||
|
config := Config
|
||||||
|
config.CGOOpusBitrate = bitrate
|
||||||
|
config.CGOOpusComplexity = complexity
|
||||||
|
config.CGOOpusVBR = vbr
|
||||||
|
config.CGOOpusSignalType = signalType
|
||||||
|
config.CGOOpusBandwidth = bandwidth
|
||||||
|
config.CGOOpusDTX = dtx
|
||||||
|
|
||||||
|
if enableLogging {
|
||||||
|
logger := logging.GetDefaultLogger().With().Str("component", component).Logger()
|
||||||
|
logger.Info().
|
||||||
|
Int("bitrate", bitrate).
|
||||||
|
Int("complexity", complexity).
|
||||||
|
Int("vbr", vbr).
|
||||||
|
Int("signal_type", signalType).
|
||||||
|
Int("bandwidth", bandwidth).
|
||||||
|
Int("dtx", dtx).
|
||||||
|
Msg("applied OPUS configuration")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,333 @@
|
||||||
|
//go:build arm && linux
|
||||||
|
|
||||||
|
package usbgadget
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Hardware integration tests for USB gadget operations
|
||||||
|
// These tests perform real hardware operations with proper cleanup and timeout handling
|
||||||
|
|
||||||
|
var (
|
||||||
|
testConfig = &Config{
|
||||||
|
VendorId: "0x1d6b", // The Linux Foundation
|
||||||
|
ProductId: "0x0104", // Multifunction Composite Gadget
|
||||||
|
SerialNumber: "",
|
||||||
|
Manufacturer: "JetKVM",
|
||||||
|
Product: "USB Emulation Device",
|
||||||
|
strictMode: false, // Disable strict mode for hardware tests
|
||||||
|
}
|
||||||
|
testDevices = &Devices{
|
||||||
|
AbsoluteMouse: true,
|
||||||
|
RelativeMouse: true,
|
||||||
|
Keyboard: true,
|
||||||
|
MassStorage: true,
|
||||||
|
}
|
||||||
|
testGadgetName = "jetkvm-test"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestUsbGadgetHardwareInit(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("Skipping hardware test in short mode")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create context with timeout to prevent hanging
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// Ensure clean state before test
|
||||||
|
cleanupUsbGadget(t, testGadgetName)
|
||||||
|
|
||||||
|
// Test USB gadget initialization with timeout
|
||||||
|
var gadget *UsbGadget
|
||||||
|
done := make(chan bool, 1)
|
||||||
|
var initErr error
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
t.Logf("USB gadget initialization panicked: %v", r)
|
||||||
|
initErr = assert.AnError
|
||||||
|
}
|
||||||
|
done <- true
|
||||||
|
}()
|
||||||
|
|
||||||
|
gadget = NewUsbGadget(testGadgetName, testDevices, testConfig, nil)
|
||||||
|
if gadget == nil {
|
||||||
|
initErr = assert.AnError
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Wait for initialization or timeout
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
if initErr != nil {
|
||||||
|
t.Fatalf("USB gadget initialization failed: %v", initErr)
|
||||||
|
}
|
||||||
|
assert.NotNil(t, gadget, "USB gadget should be initialized")
|
||||||
|
case <-ctx.Done():
|
||||||
|
t.Fatal("USB gadget initialization timed out")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup after test
|
||||||
|
defer func() {
|
||||||
|
if gadget != nil {
|
||||||
|
gadget.CloseHidFiles()
|
||||||
|
}
|
||||||
|
cleanupUsbGadget(t, testGadgetName)
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Validate gadget state
|
||||||
|
assert.NotNil(t, gadget, "USB gadget should not be nil")
|
||||||
|
validateHardwareState(t, gadget)
|
||||||
|
|
||||||
|
// Test UDC binding state
|
||||||
|
bound, err := gadget.IsUDCBound()
|
||||||
|
assert.NoError(t, err, "Should be able to check UDC binding state")
|
||||||
|
t.Logf("UDC bound state: %v", bound)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUsbGadgetHardwareReconfiguration(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("Skipping hardware test in short mode")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create context with timeout
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 45*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// Ensure clean state
|
||||||
|
cleanupUsbGadget(t, testGadgetName)
|
||||||
|
|
||||||
|
// Initialize first gadget
|
||||||
|
gadget1 := createUsbGadgetWithTimeout(t, ctx, testGadgetName, testDevices, testConfig)
|
||||||
|
defer func() {
|
||||||
|
if gadget1 != nil {
|
||||||
|
gadget1.CloseHidFiles()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Validate initial state
|
||||||
|
assert.NotNil(t, gadget1, "First USB gadget should be initialized")
|
||||||
|
|
||||||
|
// Close first gadget properly
|
||||||
|
gadget1.CloseHidFiles()
|
||||||
|
gadget1 = nil
|
||||||
|
|
||||||
|
// Wait for cleanup to complete
|
||||||
|
time.Sleep(500 * time.Millisecond)
|
||||||
|
|
||||||
|
// Test reconfiguration with different report descriptor
|
||||||
|
altGadgetConfig := make(map[string]gadgetConfigItem)
|
||||||
|
for k, v := range defaultGadgetConfig {
|
||||||
|
altGadgetConfig[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modify absolute mouse configuration
|
||||||
|
oldAbsoluteMouseConfig := altGadgetConfig["absolute_mouse"]
|
||||||
|
oldAbsoluteMouseConfig.reportDesc = absoluteMouseCombinedReportDesc
|
||||||
|
altGadgetConfig["absolute_mouse"] = oldAbsoluteMouseConfig
|
||||||
|
|
||||||
|
// Create second gadget with modified configuration
|
||||||
|
gadget2 := createUsbGadgetWithTimeoutAndConfig(t, ctx, testGadgetName, altGadgetConfig, testDevices, testConfig)
|
||||||
|
defer func() {
|
||||||
|
if gadget2 != nil {
|
||||||
|
gadget2.CloseHidFiles()
|
||||||
|
}
|
||||||
|
cleanupUsbGadget(t, testGadgetName)
|
||||||
|
}()
|
||||||
|
|
||||||
|
assert.NotNil(t, gadget2, "Second USB gadget should be initialized")
|
||||||
|
validateHardwareState(t, gadget2)
|
||||||
|
|
||||||
|
// Validate UDC binding after reconfiguration
|
||||||
|
udcs := getUdcs()
|
||||||
|
assert.NotEmpty(t, udcs, "Should have at least one UDC")
|
||||||
|
|
||||||
|
if len(udcs) > 0 {
|
||||||
|
udc := udcs[0]
|
||||||
|
t.Logf("Available UDC: %s", udc)
|
||||||
|
|
||||||
|
// Check UDC binding state
|
||||||
|
udcStr, err := os.ReadFile("/sys/kernel/config/usb_gadget/" + testGadgetName + "/UDC")
|
||||||
|
if err == nil {
|
||||||
|
t.Logf("UDC binding: %s", strings.TrimSpace(string(udcStr)))
|
||||||
|
} else {
|
||||||
|
t.Logf("Could not read UDC binding: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUsbGadgetHardwareStressTest(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("Skipping stress test in short mode")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create context with longer timeout for stress test
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// Ensure clean state
|
||||||
|
cleanupUsbGadget(t, testGadgetName)
|
||||||
|
|
||||||
|
// Perform multiple rapid reconfigurations
|
||||||
|
for i := 0; i < 3; i++ {
|
||||||
|
t.Logf("Stress test iteration %d", i+1)
|
||||||
|
|
||||||
|
// Create gadget
|
||||||
|
gadget := createUsbGadgetWithTimeout(t, ctx, testGadgetName, testDevices, testConfig)
|
||||||
|
if gadget == nil {
|
||||||
|
t.Fatalf("Failed to create USB gadget in iteration %d", i+1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate gadget
|
||||||
|
assert.NotNil(t, gadget, "USB gadget should be created in iteration %d", i+1)
|
||||||
|
validateHardwareState(t, gadget)
|
||||||
|
|
||||||
|
// Test basic operations
|
||||||
|
bound, err := gadget.IsUDCBound()
|
||||||
|
assert.NoError(t, err, "Should be able to check UDC state in iteration %d", i+1)
|
||||||
|
t.Logf("Iteration %d: UDC bound = %v", i+1, bound)
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
gadget.CloseHidFiles()
|
||||||
|
gadget = nil
|
||||||
|
|
||||||
|
// Wait between iterations
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
|
||||||
|
// Check for timeout
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
t.Fatal("Stress test timed out")
|
||||||
|
default:
|
||||||
|
// Continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Final cleanup
|
||||||
|
cleanupUsbGadget(t, testGadgetName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper functions for hardware tests
|
||||||
|
|
||||||
|
// createUsbGadgetWithTimeout creates a USB gadget with timeout protection
|
||||||
|
func createUsbGadgetWithTimeout(t *testing.T, ctx context.Context, name string, devices *Devices, config *Config) *UsbGadget {
|
||||||
|
return createUsbGadgetWithTimeoutAndConfig(t, ctx, name, defaultGadgetConfig, devices, config)
|
||||||
|
}
|
||||||
|
|
||||||
|
// createUsbGadgetWithTimeoutAndConfig creates a USB gadget with custom config and timeout protection
|
||||||
|
func createUsbGadgetWithTimeoutAndConfig(t *testing.T, ctx context.Context, name string, gadgetConfig map[string]gadgetConfigItem, devices *Devices, config *Config) *UsbGadget {
|
||||||
|
var gadget *UsbGadget
|
||||||
|
done := make(chan bool, 1)
|
||||||
|
var createErr error
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
t.Logf("USB gadget creation panicked: %v", r)
|
||||||
|
createErr = assert.AnError
|
||||||
|
}
|
||||||
|
done <- true
|
||||||
|
}()
|
||||||
|
|
||||||
|
gadget = newUsbGadget(name, gadgetConfig, devices, config, nil)
|
||||||
|
if gadget == nil {
|
||||||
|
createErr = assert.AnError
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Wait for creation or timeout
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
if createErr != nil {
|
||||||
|
t.Logf("USB gadget creation failed: %v", createErr)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return gadget
|
||||||
|
case <-ctx.Done():
|
||||||
|
t.Logf("USB gadget creation timed out")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// cleanupUsbGadget ensures clean state by removing any existing USB gadget configuration
|
||||||
|
func cleanupUsbGadget(t *testing.T, name string) {
|
||||||
|
t.Logf("Cleaning up USB gadget: %s", name)
|
||||||
|
|
||||||
|
// Try to unbind UDC first
|
||||||
|
udcPath := "/sys/kernel/config/usb_gadget/" + name + "/UDC"
|
||||||
|
if _, err := os.Stat(udcPath); err == nil {
|
||||||
|
// Read current UDC binding
|
||||||
|
if udcData, err := os.ReadFile(udcPath); err == nil && len(strings.TrimSpace(string(udcData))) > 0 {
|
||||||
|
// Unbind UDC
|
||||||
|
if err := os.WriteFile(udcPath, []byte(""), 0644); err != nil {
|
||||||
|
t.Logf("Failed to unbind UDC: %v", err)
|
||||||
|
} else {
|
||||||
|
t.Logf("Successfully unbound UDC")
|
||||||
|
// Wait for unbinding to complete
|
||||||
|
time.Sleep(200 * time.Millisecond)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove gadget directory if it exists
|
||||||
|
gadgetPath := "/sys/kernel/config/usb_gadget/" + name
|
||||||
|
if _, err := os.Stat(gadgetPath); err == nil {
|
||||||
|
// Try to remove configuration links first
|
||||||
|
configPath := gadgetPath + "/configs/c.1"
|
||||||
|
if entries, err := os.ReadDir(configPath); err == nil {
|
||||||
|
for _, entry := range entries {
|
||||||
|
if entry.Type()&os.ModeSymlink != 0 {
|
||||||
|
linkPath := configPath + "/" + entry.Name()
|
||||||
|
if err := os.Remove(linkPath); err != nil {
|
||||||
|
t.Logf("Failed to remove config link %s: %v", linkPath, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the gadget directory (this should cascade remove everything)
|
||||||
|
if err := os.RemoveAll(gadgetPath); err != nil {
|
||||||
|
t.Logf("Failed to remove gadget directory: %v", err)
|
||||||
|
} else {
|
||||||
|
t.Logf("Successfully removed gadget directory")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for cleanup to complete
|
||||||
|
time.Sleep(300 * time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateHardwareState checks the current hardware state
|
||||||
|
func validateHardwareState(t *testing.T, gadget *UsbGadget) {
|
||||||
|
if gadget == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check UDC binding state
|
||||||
|
bound, err := gadget.IsUDCBound()
|
||||||
|
if err != nil {
|
||||||
|
t.Logf("Warning: Could not check UDC binding state: %v", err)
|
||||||
|
} else {
|
||||||
|
t.Logf("UDC bound: %v", bound)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check available UDCs
|
||||||
|
udcs := getUdcs()
|
||||||
|
t.Logf("Available UDCs: %v", udcs)
|
||||||
|
|
||||||
|
// Check configfs mount
|
||||||
|
if _, err := os.Stat("/sys/kernel/config"); err != nil {
|
||||||
|
t.Logf("Warning: configfs not available: %v", err)
|
||||||
|
} else {
|
||||||
|
t.Logf("configfs is available")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,437 @@
|
||||||
|
package usbgadget
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Unit tests for USB gadget configuration logic without hardware dependencies
|
||||||
|
// These tests follow the pattern of audio tests - testing business logic and validation
|
||||||
|
|
||||||
|
func TestUsbGadgetConfigValidation(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
config *Config
|
||||||
|
devices *Devices
|
||||||
|
expected bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "ValidConfig",
|
||||||
|
config: &Config{
|
||||||
|
VendorId: "0x1d6b",
|
||||||
|
ProductId: "0x0104",
|
||||||
|
Manufacturer: "JetKVM",
|
||||||
|
Product: "USB Emulation Device",
|
||||||
|
},
|
||||||
|
devices: &Devices{
|
||||||
|
Keyboard: true,
|
||||||
|
AbsoluteMouse: true,
|
||||||
|
RelativeMouse: true,
|
||||||
|
MassStorage: true,
|
||||||
|
},
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "InvalidVendorId",
|
||||||
|
config: &Config{
|
||||||
|
VendorId: "invalid",
|
||||||
|
ProductId: "0x0104",
|
||||||
|
Manufacturer: "JetKVM",
|
||||||
|
Product: "USB Emulation Device",
|
||||||
|
},
|
||||||
|
devices: &Devices{
|
||||||
|
Keyboard: true,
|
||||||
|
},
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "EmptyManufacturer",
|
||||||
|
config: &Config{
|
||||||
|
VendorId: "0x1d6b",
|
||||||
|
ProductId: "0x0104",
|
||||||
|
Manufacturer: "",
|
||||||
|
Product: "USB Emulation Device",
|
||||||
|
},
|
||||||
|
devices: &Devices{
|
||||||
|
Keyboard: true,
|
||||||
|
},
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
err := validateUsbGadgetConfiguration(tt.config, tt.devices)
|
||||||
|
if tt.expected {
|
||||||
|
assert.NoError(t, err, "Configuration should be valid")
|
||||||
|
} else {
|
||||||
|
assert.Error(t, err, "Configuration should be invalid")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUsbGadgetDeviceConfiguration(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
devices *Devices
|
||||||
|
expectedConfigs []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "AllDevicesEnabled",
|
||||||
|
devices: &Devices{
|
||||||
|
Keyboard: true,
|
||||||
|
AbsoluteMouse: true,
|
||||||
|
RelativeMouse: true,
|
||||||
|
MassStorage: true,
|
||||||
|
Audio: true,
|
||||||
|
},
|
||||||
|
expectedConfigs: []string{"keyboard", "absolute_mouse", "relative_mouse", "mass_storage_base", "audio"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "OnlyKeyboard",
|
||||||
|
devices: &Devices{
|
||||||
|
Keyboard: true,
|
||||||
|
},
|
||||||
|
expectedConfigs: []string{"keyboard"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "MouseOnly",
|
||||||
|
devices: &Devices{
|
||||||
|
AbsoluteMouse: true,
|
||||||
|
RelativeMouse: true,
|
||||||
|
},
|
||||||
|
expectedConfigs: []string{"absolute_mouse", "relative_mouse"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
configs := getEnabledGadgetConfigs(tt.devices)
|
||||||
|
assert.ElementsMatch(t, tt.expectedConfigs, configs, "Enabled configs should match expected")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUsbGadgetStateTransition(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("Skipping state transition test in short mode")
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
initialDevices *Devices
|
||||||
|
newDevices *Devices
|
||||||
|
expectedTransition string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "EnableAudio",
|
||||||
|
initialDevices: &Devices{
|
||||||
|
Keyboard: true,
|
||||||
|
AbsoluteMouse: true,
|
||||||
|
Audio: false,
|
||||||
|
},
|
||||||
|
newDevices: &Devices{
|
||||||
|
Keyboard: true,
|
||||||
|
AbsoluteMouse: true,
|
||||||
|
Audio: true,
|
||||||
|
},
|
||||||
|
expectedTransition: "audio_enabled",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "DisableKeyboard",
|
||||||
|
initialDevices: &Devices{
|
||||||
|
Keyboard: true,
|
||||||
|
AbsoluteMouse: true,
|
||||||
|
},
|
||||||
|
newDevices: &Devices{
|
||||||
|
Keyboard: false,
|
||||||
|
AbsoluteMouse: true,
|
||||||
|
},
|
||||||
|
expectedTransition: "keyboard_disabled",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "NoChange",
|
||||||
|
initialDevices: &Devices{
|
||||||
|
Keyboard: true,
|
||||||
|
AbsoluteMouse: true,
|
||||||
|
},
|
||||||
|
newDevices: &Devices{
|
||||||
|
Keyboard: true,
|
||||||
|
AbsoluteMouse: true,
|
||||||
|
},
|
||||||
|
expectedTransition: "no_change",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
transition := simulateUsbGadgetStateTransition(ctx, tt.initialDevices, tt.newDevices)
|
||||||
|
assert.Equal(t, tt.expectedTransition, transition, "State transition should match expected")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUsbGadgetConfigurationTimeout(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("Skipping timeout test in short mode")
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// Test that configuration validation completes within reasonable time
|
||||||
|
start := time.Now()
|
||||||
|
|
||||||
|
// Simulate multiple rapid configuration changes
|
||||||
|
for i := 0; i < 20; i++ {
|
||||||
|
devices := &Devices{
|
||||||
|
Keyboard: i%2 == 0,
|
||||||
|
AbsoluteMouse: i%3 == 0,
|
||||||
|
RelativeMouse: i%4 == 0,
|
||||||
|
MassStorage: i%5 == 0,
|
||||||
|
Audio: i%6 == 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
config := &Config{
|
||||||
|
VendorId: "0x1d6b",
|
||||||
|
ProductId: "0x0104",
|
||||||
|
Manufacturer: "JetKVM",
|
||||||
|
Product: "USB Emulation Device",
|
||||||
|
}
|
||||||
|
|
||||||
|
err := validateUsbGadgetConfiguration(config, devices)
|
||||||
|
assert.NoError(t, err, "Configuration validation should not fail")
|
||||||
|
|
||||||
|
// Ensure we don't timeout
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
t.Fatal("USB gadget configuration test timed out")
|
||||||
|
default:
|
||||||
|
// Continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
elapsed := time.Since(start)
|
||||||
|
t.Logf("USB gadget configuration test completed in %v", elapsed)
|
||||||
|
assert.Less(t, elapsed, 2*time.Second, "Configuration validation should complete quickly")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReportDescriptorValidation(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
reportDesc []byte
|
||||||
|
expected bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "ValidKeyboardReportDesc",
|
||||||
|
reportDesc: keyboardReportDesc,
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ValidAbsoluteMouseReportDesc",
|
||||||
|
reportDesc: absoluteMouseCombinedReportDesc,
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ValidRelativeMouseReportDesc",
|
||||||
|
reportDesc: relativeMouseCombinedReportDesc,
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "EmptyReportDesc",
|
||||||
|
reportDesc: []byte{},
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "InvalidReportDesc",
|
||||||
|
reportDesc: []byte{0xFF, 0xFF, 0xFF},
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
err := validateReportDescriptor(tt.reportDesc)
|
||||||
|
if tt.expected {
|
||||||
|
assert.NoError(t, err, "Report descriptor should be valid")
|
||||||
|
} else {
|
||||||
|
assert.Error(t, err, "Report descriptor should be invalid")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper functions for simulation (similar to audio tests)
|
||||||
|
|
||||||
|
// validateUsbGadgetConfiguration simulates the validation that happens in production
|
||||||
|
func validateUsbGadgetConfiguration(config *Config, devices *Devices) error {
|
||||||
|
if config == nil {
|
||||||
|
return assert.AnError
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate vendor ID format
|
||||||
|
if config.VendorId == "" || len(config.VendorId) < 4 {
|
||||||
|
return assert.AnError
|
||||||
|
}
|
||||||
|
if config.VendorId != "" && config.VendorId[:2] != "0x" {
|
||||||
|
return assert.AnError
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate product ID format
|
||||||
|
if config.ProductId == "" || len(config.ProductId) < 4 {
|
||||||
|
return assert.AnError
|
||||||
|
}
|
||||||
|
if config.ProductId != "" && config.ProductId[:2] != "0x" {
|
||||||
|
return assert.AnError
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate required fields
|
||||||
|
if config.Manufacturer == "" {
|
||||||
|
return assert.AnError
|
||||||
|
}
|
||||||
|
if config.Product == "" {
|
||||||
|
return assert.AnError
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: Allow configurations with no devices enabled for testing purposes
|
||||||
|
// In production, this would typically be validated at a higher level
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getEnabledGadgetConfigs returns the list of enabled gadget configurations
|
||||||
|
func getEnabledGadgetConfigs(devices *Devices) []string {
|
||||||
|
var configs []string
|
||||||
|
|
||||||
|
if devices.Keyboard {
|
||||||
|
configs = append(configs, "keyboard")
|
||||||
|
}
|
||||||
|
if devices.AbsoluteMouse {
|
||||||
|
configs = append(configs, "absolute_mouse")
|
||||||
|
}
|
||||||
|
if devices.RelativeMouse {
|
||||||
|
configs = append(configs, "relative_mouse")
|
||||||
|
}
|
||||||
|
if devices.MassStorage {
|
||||||
|
configs = append(configs, "mass_storage_base")
|
||||||
|
}
|
||||||
|
if devices.Audio {
|
||||||
|
configs = append(configs, "audio")
|
||||||
|
}
|
||||||
|
|
||||||
|
return configs
|
||||||
|
}
|
||||||
|
|
||||||
|
// simulateUsbGadgetStateTransition simulates the state management during USB reconfiguration
|
||||||
|
func simulateUsbGadgetStateTransition(ctx context.Context, initial, new *Devices) string {
|
||||||
|
// Check for audio changes
|
||||||
|
if initial.Audio != new.Audio {
|
||||||
|
if new.Audio {
|
||||||
|
// Simulate enabling audio device
|
||||||
|
time.Sleep(5 * time.Millisecond)
|
||||||
|
return "audio_enabled"
|
||||||
|
} else {
|
||||||
|
// Simulate disabling audio device
|
||||||
|
time.Sleep(5 * time.Millisecond)
|
||||||
|
return "audio_disabled"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for keyboard changes
|
||||||
|
if initial.Keyboard != new.Keyboard {
|
||||||
|
if new.Keyboard {
|
||||||
|
time.Sleep(5 * time.Millisecond)
|
||||||
|
return "keyboard_enabled"
|
||||||
|
} else {
|
||||||
|
time.Sleep(5 * time.Millisecond)
|
||||||
|
return "keyboard_disabled"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for mouse changes
|
||||||
|
if initial.AbsoluteMouse != new.AbsoluteMouse || initial.RelativeMouse != new.RelativeMouse {
|
||||||
|
time.Sleep(5 * time.Millisecond)
|
||||||
|
return "mouse_changed"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for mass storage changes
|
||||||
|
if initial.MassStorage != new.MassStorage {
|
||||||
|
time.Sleep(5 * time.Millisecond)
|
||||||
|
return "mass_storage_changed"
|
||||||
|
}
|
||||||
|
|
||||||
|
return "no_change"
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateReportDescriptor simulates HID report descriptor validation
|
||||||
|
func validateReportDescriptor(reportDesc []byte) error {
|
||||||
|
if len(reportDesc) == 0 {
|
||||||
|
return assert.AnError
|
||||||
|
}
|
||||||
|
|
||||||
|
// Basic HID report descriptor validation
|
||||||
|
// Check for valid usage page (0x05)
|
||||||
|
found := false
|
||||||
|
for i := 0; i < len(reportDesc)-1; i++ {
|
||||||
|
if reportDesc[i] == 0x05 {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
return assert.AnError
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Benchmark tests
|
||||||
|
|
||||||
|
func BenchmarkValidateUsbGadgetConfiguration(b *testing.B) {
|
||||||
|
config := &Config{
|
||||||
|
VendorId: "0x1d6b",
|
||||||
|
ProductId: "0x0104",
|
||||||
|
Manufacturer: "JetKVM",
|
||||||
|
Product: "USB Emulation Device",
|
||||||
|
}
|
||||||
|
devices := &Devices{
|
||||||
|
Keyboard: true,
|
||||||
|
AbsoluteMouse: true,
|
||||||
|
RelativeMouse: true,
|
||||||
|
MassStorage: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
_ = validateUsbGadgetConfiguration(config, devices)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkGetEnabledGadgetConfigs(b *testing.B) {
|
||||||
|
devices := &Devices{
|
||||||
|
Keyboard: true,
|
||||||
|
AbsoluteMouse: true,
|
||||||
|
RelativeMouse: true,
|
||||||
|
MassStorage: true,
|
||||||
|
Audio: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
_ = getEnabledGadgetConfigs(devices)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkValidateReportDescriptor(b *testing.B) {
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
_ = validateReportDescriptor(keyboardReportDesc)
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "kvm-ui",
|
"name": "kvm-ui",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "2025.10.01.1900",
|
"version": "2025.09.26.01300",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^22.15.0"
|
"node": "^22.15.0"
|
||||||
|
|
@ -42,7 +42,7 @@
|
||||||
"react-hot-toast": "^2.6.0",
|
"react-hot-toast": "^2.6.0",
|
||||||
"react-icons": "^5.5.0",
|
"react-icons": "^5.5.0",
|
||||||
"react-router": "^7.9.3",
|
"react-router": "^7.9.3",
|
||||||
"react-simple-keyboard": "^3.8.125",
|
"react-simple-keyboard": "^3.8.122",
|
||||||
"react-use-websocket": "^4.13.0",
|
"react-use-websocket": "^4.13.0",
|
||||||
"react-xtermjs": "^1.0.10",
|
"react-xtermjs": "^1.0.10",
|
||||||
"recharts": "^3.2.1",
|
"recharts": "^3.2.1",
|
||||||
|
|
@ -56,15 +56,15 @@
|
||||||
"@eslint/eslintrc": "^3.3.1",
|
"@eslint/eslintrc": "^3.3.1",
|
||||||
"@eslint/js": "^9.36.0",
|
"@eslint/js": "^9.36.0",
|
||||||
"@tailwindcss/forms": "^0.5.10",
|
"@tailwindcss/forms": "^0.5.10",
|
||||||
"@tailwindcss/postcss": "^4.1.14",
|
"@tailwindcss/postcss": "^4.1.13",
|
||||||
"@tailwindcss/typography": "^0.5.19",
|
"@tailwindcss/typography": "^0.5.19",
|
||||||
"@tailwindcss/vite": "^4.1.14",
|
"@tailwindcss/vite": "^4.1.13",
|
||||||
"@types/react": "^19.1.17",
|
"@types/react": "^19.1.14",
|
||||||
"@types/react-dom": "^19.1.10",
|
"@types/react-dom": "^19.1.9",
|
||||||
"@types/semver": "^7.7.1",
|
"@types/semver": "^7.7.1",
|
||||||
"@types/validator": "^13.15.3",
|
"@types/validator": "^13.15.3",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.45.0",
|
"@typescript-eslint/eslint-plugin": "^8.44.1",
|
||||||
"@typescript-eslint/parser": "^8.45.0",
|
"@typescript-eslint/parser": "^8.44.1",
|
||||||
"@vitejs/plugin-react-swc": "^4.1.0",
|
"@vitejs/plugin-react-swc": "^4.1.0",
|
||||||
"autoprefixer": "^10.4.21",
|
"autoprefixer": "^10.4.21",
|
||||||
"eslint": "^9.36.0",
|
"eslint": "^9.36.0",
|
||||||
|
|
@ -77,8 +77,8 @@
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.6",
|
||||||
"prettier": "^3.6.2",
|
"prettier": "^3.6.2",
|
||||||
"prettier-plugin-tailwindcss": "^0.6.14",
|
"prettier-plugin-tailwindcss": "^0.6.14",
|
||||||
"tailwindcss": "^4.1.14",
|
"tailwindcss": "^4.1.13",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.2",
|
||||||
"vite": "^7.1.7",
|
"vite": "^7.1.7",
|
||||||
"vite-tsconfig-paths": "^5.1.4"
|
"vite-tsconfig-paths": "^5.1.4"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -153,13 +153,13 @@ body {
|
||||||
|
|
||||||
@property --grid-color-start {
|
@property --grid-color-start {
|
||||||
syntax: "<color>";
|
syntax: "<color>";
|
||||||
initial-value: oklch(97% 0.014 254.604 / 10); /* var(--color-blue-50/10) */
|
initial-value: var(--color-blue-50/10);
|
||||||
inherits: false;
|
inherits: false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@property --grid-color-end {
|
@property --grid-color-end {
|
||||||
syntax: "<color>";
|
syntax: "<color>";
|
||||||
initial-value: oklch(97% 0.014 254.604 / 100); /* var(--color-blue-50/100) */
|
initial-value: var(--color-blue-50/100);
|
||||||
inherits: false;
|
inherits: false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -175,8 +175,8 @@ body {
|
||||||
}
|
}
|
||||||
|
|
||||||
.group:hover .grid-card {
|
.group:hover .grid-card {
|
||||||
--grid-color-start: oklch(from var(--color-blue-100) l c h / 50);
|
--grid-color-start: var(--color-blue-100/50);
|
||||||
--grid-color-end: oklch(from var(--color-blue-50) l c h / 50);
|
--grid-color-end: var(--color-blue-50/50);
|
||||||
}
|
}
|
||||||
|
|
||||||
video::-webkit-media-controls {
|
video::-webkit-media-controls {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue